1
0
mirror of https://github.com/upscayl/upscayl.git synced 2024-11-23 23:21:05 +01:00

New models selection menu

This commit is contained in:
Nayam Amarshe 2024-10-06 12:45:44 +05:30
parent a0af4fe02a
commit a6f344f3b8
23 changed files with 509 additions and 245 deletions

View File

@ -1,13 +1,32 @@
export const defaultModelsList = [
{ label: "General Photo (Real-ESRGAN)", value: "realesrgan-x4plus" },
{
label: "General Photo (Fast Real-ESRGAN)",
value: "realesrgan-x4fast",
export const MODELS = {
"realesrgan-x4plus": {
id: "realesrgan-x4plus",
name: "Upscayl Standard",
},
{ label: "General Photo (Remacri)", value: "remacri" },
{ label: "General Photo (Ultramix Balanced)", value: "ultramix_balanced" },
{ label: "General Photo (Ultrasharp)", value: "ultrasharp" },
{ label: "Digital Art", value: "realesrgan-x4plus-anime" },
];
"realesrgan-x4fast": {
id: "realesrgan-x4fast",
name: "Upscayl Lite",
},
remacri: {
id: "remacri",
name: "Remacri (Non-Commercial)",
},
ultramix_balanced: {
id: "ultramix_balanced",
name: "Ultramix (Non-Commercial)",
},
ultrasharp: {
id: "ultrasharp",
name: "Ultrasharp (Non-Commercial)",
},
"realesrgan-x4plus-anime": {
id: "realesrgan-x4plus-anime",
name: "Digital Art",
},
};
export const DEFAULT_MODELS = defaultModelsList.map((model) => model.value);
export type ModelId = keyof typeof MODELS;
export const DEFAULT_MODELS_ID_LIST = Object.keys(MODELS).map(
(modelId) => modelId,
);

View File

@ -11,10 +11,10 @@ import { spawnUpscayl } from "../utils/spawn-upscayl";
import { getBatchArguments } from "../utils/get-arguments";
import slash from "../utils/slash";
import { modelsPath } from "../utils/get-resource-paths";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { ELECTRON_COMMANDS } from "../../common/electron-commands";
import { BatchUpscaylPayload } from "../../common/types/types";
import showNotification from "../utils/show-notification";
import { DEFAULT_MODELS } from "../../common/models-list";
import { DEFAULT_MODELS_ID_LIST } from "../../common/models-list";
const batchUpscayl = async (event, payload: BatchUpscaylPayload) => {
const mainWindow = getMainWindow();
@ -41,7 +41,7 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => {
fs.mkdirSync(outputFolderPath, { recursive: true });
}
const isDefaultModel = DEFAULT_MODELS.includes(model);
const isDefaultModel = DEFAULT_MODELS_ID_LIST.includes(model);
// UPSCALE
const upscayl = spawnUpscayl(

View File

@ -5,7 +5,7 @@ import {
} from "../utils/config-variables";
import logit from "../utils/logit";
import slash from "../utils/slash";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { ELECTRON_COMMANDS } from "../../common/electron-commands";
import getModels from "../utils/get-models";
import { getMainWindow } from "../main-window";
import settings from "electron-settings";

View File

@ -14,11 +14,11 @@ import {
} from "../utils/get-arguments";
import { modelsPath } from "../utils/get-resource-paths";
import logit from "../utils/logit";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { ELECTRON_COMMANDS } from "../../common/electron-commands";
import { DoubleUpscaylPayload } from "../../common/types/types";
import { ImageFormat } from "../types/types";
import showNotification from "../utils/show-notification";
import { DEFAULT_MODELS } from "../../common/models-list";
import { DEFAULT_MODELS_ID_LIST } from "../../common/models-list";
import getFilenameFromPath from "../../common/get-file-name";
import decodePath from "../../common/decode-path";
import getDirectoryFromPath from "../../common/get-directory-from-path";
@ -41,7 +41,7 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
const fullfileName = getFilenameFromPath(imagePath);
const fileName = parse(fullfileName).name;
const isDefaultModel = DEFAULT_MODELS.includes(model);
const isDefaultModel = DEFAULT_MODELS_ID_LIST.includes(model);
// COPY IMAGE TO TMP FOLDER

View File

@ -1,4 +1,4 @@
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { ELECTRON_COMMANDS } from "../../common/electron-commands";
import { getMainWindow } from "../main-window";
import {
savedCustomModelsPath,

View File

@ -1,6 +1,6 @@
import fs from "fs";
import { modelsPath } from "../utils/get-resource-paths";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { ELECTRON_COMMANDS } from "../../common/electron-commands";
import {
savedCustomModelsPath,
setChildProcesses,
@ -16,7 +16,7 @@ import { getMainWindow } from "../main-window";
import { ImageUpscaylPayload } from "../../common/types/types";
import { ImageFormat } from "../types/types";
import showNotification from "../utils/show-notification";
import { DEFAULT_MODELS } from "../../common/models-list";
import { DEFAULT_MODELS_ID_LIST } from "../../common/models-list";
import getFilenameFromPath from "../../common/get-file-name";
import decodePath from "../../common/decode-path";
import getDirectoryFromPath from "../../common/get-directory-from-path";
@ -55,7 +55,7 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
"." +
saveImageAs;
const isDefaultModel = DEFAULT_MODELS.includes(model);
const isDefaultModel = DEFAULT_MODELS_ID_LIST.includes(model);
// Check if windows can write the new filename to the file system
if (outFile.length >= 255) {

View File

@ -1,6 +1,6 @@
import log from "electron-log";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { getMainWindow } from "../main-window";
import { ELECTRON_COMMANDS } from "../../common/electron-commands";
const logit = (...args: any) => {
const mainWindow = getMainWindow();

View File

@ -1,8 +0,0 @@
[Desktop Entry]
X-Desktop-File-Install-Version=0.26
Exec=upscayl %U --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations
Comment=Upscale Images with AI
Name=Upscayl
StartupNotify=false
Categories=ImageProcessing;RasterGraphics;Graphics;
Type=Application

173
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
@ -1739,6 +1740,11 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@radix-ui/number": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="
},
"node_modules/@radix-ui/primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
@ -1866,6 +1872,20 @@
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
@ -2092,6 +2112,159 @@
}
}
},
"node_modules/@radix-ui/react-scroll-area": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.0.tgz",
"integrity": "sha512-q2jMBdsJ9zB7QG6ngQNzNwlvxLQqONyL58QbEGwuyRZZb/ARQwk3uQVbCF7GvQVOtV6EU/pDxAw3zRzJZI3rpQ==",
"dependencies": {
"@radix-ui/number": "1.1.0",
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-context": "1.1.1",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-use-callback-ref": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0",
"@radix-ui/react-use-layout-effect": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
"dependencies": {
"@radix-ui/react-slot": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",

View File

@ -225,6 +225,7 @@
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.2.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",

View File

@ -1,9 +1,3 @@
import { defaultModelsList } from "@common/models-list";
import { atom } from "jotai";
export type TModelsList = {
label: string;
value: string;
}[];
export const modelsListAtom = atom<TModelsList>(defaultModelsList);
export const customModelIdsAtom = atom([] as string[]);

View File

@ -1,3 +1,5 @@
import { ImageFormat } from "@/lib/valid-formats";
import { ModelId } from "@common/models-list";
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
@ -6,6 +8,17 @@ export const customModelsPathAtom = atomWithStorage<string | null>(
null,
);
export const selectedModelIdAtom = atomWithStorage<ModelId | string>(
"selectedModelId",
"realesrgan-x4plus",
);
export const doubleUpscaylAtom = atomWithStorage("selectedModelId", false);
export const gpuIdAtom = atomWithStorage("gpuId", "");
export const saveImageAsAtom = atomWithStorage<ImageFormat>(
"saveImageAs",
"png",
);
export const scaleAtom = atomWithStorage<string>("scale", "4");
export const batchModeAtom = atom<boolean>(false);

View File

@ -15,7 +15,7 @@ import { useToast } from "@/components/ui/use-toast";
import { sanitizePath } from "@common/sanitize-path";
import getDirectoryFromPath from "@common/get-directory-from-path";
import { FEATURE_FLAGS } from "@common/feature-flags";
import { VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
import { ImageFormat, VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
import ProgressBar from "./progress-bar";
import InstructionsCard from "./instructions-card";
import ImageViewSettings from "./image-view-settings";
@ -176,12 +176,12 @@ const MainContent = ({
}
const type = items[0].type;
const filePath = files[0].path;
const extension = files[0].name.split(".").at(-1);
const extension = files[0].name
.split(".")
.at(-1)
.toLowerCase() as ImageFormat;
logit("📋 Pasted file: ", JSON.stringify({ type, filePath, extension }));
if (
!type.includes("image") &&
!VALID_IMAGE_FORMATS.includes(extension.toLowerCase())
) {
if (!type.includes("image") && !VALID_IMAGE_FORMATS.includes(extension)) {
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),

View File

@ -14,6 +14,10 @@ import {
useCustomWidthAtom,
tileSizeAtom,
showSidebarAtom,
selectedModelIdAtom,
doubleUpscaylAtom,
gpuIdAtom,
saveImageAsAtom,
} from "../../atoms/user-settings-atom";
import useLogger from "../hooks/use-logger";
import {
@ -63,16 +67,16 @@ const Sidebar = ({
// LOCAL STATES
// TODO: Add electron handler for os
const [model, setModel] = useState("realesrgan-x4plus");
const [doubleUpscayl, setDoubleUpscayl] = useState(false);
const overwrite = useAtomValue(overwriteAtom);
const [gpuId, setGpuId] = useState("");
const [saveImageAs, setSaveImageAs] = useState("png");
const [selectedModelId, setSelectedModelId] = useAtom(selectedModelIdAtom);
const [doubleUpscayl, setDoubleUpscayl] = useAtom(doubleUpscaylAtom);
const [gpuId, setGpuId] = useAtom(gpuIdAtom);
const [saveImageAs, setSaveImageAs] = useAtom(saveImageAsAtom);
const [selectedTab, setSelectedTab] = useState(0);
const [showCloudModal, setShowCloudModal] = useState(false);
// ATOMIC STATES
const overwrite = useAtomValue(overwriteAtom);
const outputPath = useAtomValue(savedOutputPathAtom);
const [compression, setCompression] = useAtom(compressionAtom);
const setProgress = useSetAtom(progressAtom);
@ -87,7 +91,7 @@ const Sidebar = ({
const [showSidebar, setShowSidebar] = useAtom(showSidebarAtom);
const handleModelChange = (e: any) => {
setModel(e.value);
setSelectedModelId(e.value);
logit("🔀 Model changed: ", e.value);
localStorage.setItem(
"model",
@ -108,7 +112,7 @@ const Sidebar = ({
{
imagePath,
outputPath,
model,
model: selectedModelId,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
@ -128,7 +132,7 @@ const Sidebar = ({
{
batchFolderPath,
outputPath,
model,
model: selectedModelId,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
@ -145,7 +149,7 @@ const Sidebar = ({
window.electron.send<ImageUpscaylPayload>(ELECTRON_COMMANDS.UPSCAYL, {
imagePath,
outputPath,
model,
model: selectedModelId,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
@ -181,7 +185,7 @@ const Sidebar = ({
className={`relative flex h-screen min-w-[350px] max-w-[350px] flex-col bg-base-100 ${showSidebar ? "" : "hidden"}`}
>
<button
className="absolute -right-0 top-1/2 z-[999] -translate-y-1/2 translate-x-1/2 rounded-full bg-base-100 p-4"
className="absolute -right-0 top-1/2 z-50 -translate-y-1/2 translate-x-1/2 rounded-full bg-base-100 p-4"
onClick={() => setShowSidebar((prev) => !prev)}
>
<ChevronLeftIcon />
@ -201,7 +205,6 @@ const Sidebar = ({
<UpscaylSteps
selectImageHandler={selectImageHandler}
selectFolderHandler={selectFolderHandler}
handleModelChange={handleModelChange}
upscaylHandler={upscaylHandler}
batchMode={batchMode}
setBatchMode={setBatchMode}
@ -210,8 +213,6 @@ const Sidebar = ({
setDoubleUpscayl={setDoubleUpscayl}
dimensions={dimensions}
setGpuId={setGpuId}
model={model}
setModel={setModel}
setSaveImageAs={setSaveImageAs}
/>
)}
@ -219,7 +220,6 @@ const Sidebar = ({
{selectedTab === 1 && (
<SettingsTab
batchMode={batchMode}
setModel={setModel}
compression={compression}
setCompression={setCompression}
gpuId={gpuId}

View File

@ -10,7 +10,7 @@ import React, { useEffect, useState } from "react";
import { themeChange } from "theme-change";
import { useAtom, useAtomValue } from "jotai";
import { customModelsPathAtom, scaleAtom } from "@/atoms/user-settings-atom";
import { modelsListAtom } from "@/atoms/models-list-atom";
import { customModelIdsAtom } from "@/atoms/models-list-atom";
import useLogger from "@/components/hooks/use-logger";
import { InputCompression } from "./input-compression";
import OverwriteToggle from "./overwrite-toggle";
@ -23,12 +23,12 @@ import { InputCustomResolution } from "./input-custom-resolution";
import { InputTileSize } from "./input-tile-size";
import LanguageSwitcher from "./language-switcher";
import { translationAtom } from "@/atoms/translations-atom";
import { ImageFormat } from "@/lib/valid-formats";
interface IProps {
batchMode: boolean;
setModel: React.Dispatch<React.SetStateAction<string>>;
saveImageAs: string;
setSaveImageAs: React.Dispatch<React.SetStateAction<string>>;
saveImageAs: ImageFormat;
setSaveImageAs: React.Dispatch<React.SetStateAction<ImageFormat>>;
compression: number;
setCompression: React.Dispatch<React.SetStateAction<number>>;
gpuId: string;
@ -41,7 +41,6 @@ interface IProps {
function SettingsTab({
batchMode,
setModel,
compression,
setCompression,
gpuId,
@ -49,7 +48,6 @@ function SettingsTab({
saveImageAs,
setSaveImageAs,
logData,
show,
setShow,
setDontShowCloudModal,
@ -57,65 +55,17 @@ function SettingsTab({
const [isCopied, setIsCopied] = useState(false);
const [customModelsPath, setCustomModelsPath] = useAtom(customModelsPathAtom);
const modelOptions = useAtomValue(modelsListAtom);
const [scale, setScale] = useAtom(scaleAtom);
const [enableScrollbar, setEnableScrollbar] = useState(true);
const [timeoutId, setTimeoutId] = useState(null);
const t = useAtomValue(translationAtom);
const logit = useLogger();
useEffect(() => {
themeChange(false);
if (!localStorage.getItem("saveImageAs")) {
logit("⚙️ Setting saveImageAs to png");
localStorage.setItem("saveImageAs", "png");
} else {
const currentlySavedImageFormat = localStorage.getItem("saveImageAs");
logit(
"⚙️ Getting saveImageAs from localStorage: ",
currentlySavedImageFormat,
);
setSaveImageAs(currentlySavedImageFormat);
}
if (!localStorage.getItem("model")) {
setModel(modelOptions[0].value);
localStorage.setItem("model", JSON.stringify(modelOptions[0]));
logit("🔀 Setting model to", modelOptions[0].value);
} else {
let currentlySavedModel = JSON.parse(
localStorage.getItem("model"),
) as (typeof modelOptions)[0];
if (
modelOptions.find(
(model) => model.value === currentlySavedModel.value,
) === undefined
) {
localStorage.setItem("model", JSON.stringify(modelOptions[0]));
logit("🔀 Setting model to", modelOptions[0].value);
currentlySavedModel = modelOptions[0];
}
setModel(currentlySavedModel.value);
logit(
"⚙️ Getting model from localStorage: ",
JSON.stringify(currentlySavedModel),
);
}
if (!localStorage.getItem("gpuId")) {
localStorage.setItem("gpuId", "");
logit("⚙️ Setting gpuId to empty string");
} else {
const currentlySavedGpuId = localStorage.getItem("gpuId");
setGpuId(currentlySavedGpuId);
logit("⚙️ Getting gpuId from localStorage: ", currentlySavedGpuId);
}
}, []);
// HANDLERS
const setExportType = (format: string) => {
const setExportType = (format: ImageFormat) => {
setSaveImageAs(format);
localStorage.setItem("saveImageAs", format);
};

View File

@ -12,7 +12,7 @@ const SidebarToggleButton = ({
return (
<button
className={cn(
"fixed left-0 top-1/2 z-[999] -translate-y-1/2 rounded-r-full bg-base-100 p-4 ",
"fixed left-0 top-1/2 z-50 -translate-y-1/2 rounded-r-full bg-base-100 p-4 ",
showSidebar ? "hidden" : "",
)}
onClick={() => setShowSidebar((prev) => !prev)}

View File

@ -0,0 +1,158 @@
"use client";
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Maximize2, SwatchBookIcon, X } from "lucide-react";
import { ModelId, MODELS } from "@common/models-list";
import { useAtom, useAtomValue } from "jotai";
import { selectedModelIdAtom } from "@/atoms/user-settings-atom";
import { customModelIdsAtom } from "@/atoms/models-list-atom";
export default function SelectModel() {
const [selectedModelId, setSelectedModelId] = useAtom(selectedModelIdAtom);
const customModelIds = useAtomValue(customModelIdsAtom);
const [open, setOpen] = useState(false);
const [zoomedModel, setZoomedModel] = useState<ModelId | null>(null);
const handleModelSelect = (model: ModelId | string) => {
setSelectedModelId(model);
setOpen(false);
};
const handleZoom = (event: React.MouseEvent, model: ModelId) => {
event.stopPropagation();
setZoomedModel(model);
};
return (
<div className="flex flex-col gap-4">
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<button className="btn btn-primary justify-start border-border">
<SwatchBookIcon className="mr-2 h-5 w-5" />
{MODELS[selectedModelId]?.name || selectedModelId}
</button>
</DialogTrigger>
<DialogContent className="z-50 sm:max-w-lg">
<DialogHeader>
<DialogTitle>Select AI Model</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[600px] pr-4">
<div className="grid gap-4">
{Object.entries(MODELS).map((modelData) => {
const modelId = modelData[0] as ModelId;
const model = modelData[1];
return (
<Button
key={modelId}
variant="outline"
className="h-auto w-full flex-col items-start p-4"
onClick={() => handleModelSelect(modelId)}
>
<div className="mb-2 font-semibold">{model.name}</div>
<div className="relative h-52 w-full overflow-hidden rounded-md">
<div className="flex h-full w-full">
<img
src={`/model-comparison/${model.name}/before.webp`}
alt={`${model.name} Before`}
className="h-full w-1/2 object-cover"
/>
<img
src={`/model-comparison/${model.id}/after.webp`}
alt={`${model.name} After`}
className="h-full w-1/2 object-cover"
/>
</div>
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-full w-px bg-white opacity-50"></div>
</div>
<div className="absolute bottom-2 left-2 rounded bg-black bg-opacity-50 px-1 text-xs text-white">
Before
</div>
<div className="absolute bottom-2 right-2 rounded bg-black bg-opacity-50 px-1 text-xs text-white">
After
</div>
<Button
variant="secondary"
size="icon"
className="absolute right-2 top-2"
onClick={(e) => handleZoom(e, modelId)}
>
<Maximize2 className="h-4 w-4" />
<span className="sr-only">Zoom</span>
</Button>
</div>
</Button>
);
})}
{customModelIds.map((customModel) => {
return (
<Button
key={customModel}
variant="outline"
className="h-auto w-full flex-col items-start p-4"
onClick={() => handleModelSelect(customModel)}
>
{customModel}
</Button>
);
})}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
<Dialog
open={!!zoomedModel}
onOpenChange={(open) => !open && setZoomedModel(null)}
>
<DialogContent
className="h-screen w-screen max-w-full p-0"
hideCloseButton
>
<div className="relative flex h-full w-full items-center justify-center bg-black">
<div className="flex h-full w-full">
<div className="relative h-full w-1/2">
<img
src={`/model-comparison/${MODELS[zoomedModel]?.id}/before.webp`}
alt={`${MODELS[zoomedModel]?.name} Before`}
className="h-full w-full object-contain"
/>
<div className="absolute bottom-4 left-4 rounded bg-black bg-opacity-50 px-2 py-1 text-sm text-white">
Before
</div>
</div>
<div className="relative h-full w-1/2">
<img
src={`/model-comparison/${MODELS[zoomedModel]?.id}/after.webp`}
alt={`${MODELS[zoomedModel]?.name} After`}
className="h-full w-full object-contain"
/>
<div className="absolute bottom-4 right-4 rounded bg-black bg-opacity-50 px-2 py-1 text-sm text-white">
After
</div>
</div>
</div>
<Button
variant="secondary"
size="icon"
className="absolute right-4 top-4"
onClick={() => setZoomedModel(null)}
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -1,8 +1,7 @@
import { useAtom, useAtomValue } from "jotai";
import React, { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo } from "react";
import { Tooltip } from "react-tooltip";
import { themeChange } from "theme-change";
import { TModelsList, modelsListAtom } from "../../../atoms/models-list-atom";
import useLogger from "../../hooks/use-logger";
import {
savedOutputPathAtom,
@ -14,16 +13,15 @@ import {
} from "../../../atoms/user-settings-atom";
import { FEATURE_FLAGS } from "@common/feature-flags";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import Select from "react-select";
import { cn } from "@/lib/utils";
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 { ImageFormat } from "@/lib/valid-formats";
interface IProps {
selectImageHandler: () => Promise<void>;
selectFolderHandler: () => Promise<void>;
handleModelChange: (e: any) => void;
upscaylHandler: () => Promise<void>;
batchMode: boolean;
setBatchMode: React.Dispatch<React.SetStateAction<boolean>>;
@ -34,16 +32,13 @@ interface IProps {
width: number | null;
height: number | null;
};
setSaveImageAs: React.Dispatch<React.SetStateAction<string>>;
model: string;
setModel: React.Dispatch<React.SetStateAction<string>>;
setSaveImageAs: React.Dispatch<React.SetStateAction<ImageFormat>>;
setGpuId: React.Dispatch<React.SetStateAction<string>>;
}
function UpscaylSteps({
selectImageHandler,
selectFolderHandler,
handleModelChange,
upscaylHandler,
batchMode,
setBatchMode,
@ -51,22 +46,11 @@ function UpscaylSteps({
doubleUpscayl,
setDoubleUpscayl,
dimensions,
setSaveImageAs,
model,
setModel,
setGpuId,
}: IProps) {
const [currentModel, setCurrentModel] = useState<TModelsList[0]>({
label: null,
value: null,
});
const modelOptions = useAtomValue(modelsListAtom);
const [scale, setScale] = useAtom(scaleAtom);
const [outputPath, setOutputPath] = useAtom(savedOutputPathAtom);
const [progress, setProgress] = useAtom(progressAtom);
const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const [open, setOpen] = React.useState(false);
const customWidth = useAtomValue(customWidthAtom);
const useCustomWidth = useAtomValue(useCustomWidthAtom);
@ -86,50 +70,8 @@ function UpscaylSteps({
useEffect(() => {
themeChange(false);
if (!localStorage.getItem("saveImageAs")) {
logit("⚙️ Setting saveImageAs to png");
localStorage.setItem("saveImageAs", "png");
} else {
const currentlySavedImageFormat = localStorage.getItem("saveImageAs");
logit(
"⚙️ Getting saveImageAs from localStorage: ",
currentlySavedImageFormat,
);
setSaveImageAs(currentlySavedImageFormat);
}
if (!localStorage.getItem("model")) {
setCurrentModel(modelOptions[0]);
setModel(modelOptions[0].value);
localStorage.setItem("model", JSON.stringify(modelOptions[0]));
logit("🔀 Setting model to", modelOptions[0].value);
} else {
const currentlySavedModel = JSON.parse(
localStorage.getItem("model"),
) as (typeof modelOptions)[0];
setCurrentModel(currentlySavedModel);
setModel(currentlySavedModel.value);
logit(
"⚙️ Getting model from localStorage: ",
JSON.stringify(currentlySavedModel),
);
}
if (!localStorage.getItem("gpuId")) {
localStorage.setItem("gpuId", "");
logit("⚙️ Setting gpuId to empty string");
} else {
const currentlySavedGpuId = localStorage.getItem("gpuId");
setGpuId(currentlySavedGpuId);
logit("⚙️ Getting gpuId from localStorage: ", currentlySavedGpuId);
}
}, []);
useEffect(() => {
logit("🔀 Setting model to", currentModel.value);
}, [currentModel]);
const upscaylResolution = useMemo(() => {
const newDimensions = {
width: dimensions.width,
@ -214,25 +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>
<Select
onMenuOpen={() => setOpen(true)}
onMenuClose={() => setOpen(false)}
options={modelOptions}
components={{
IndicatorSeparator: () => null,
DropdownIndicator: () => null,
}}
onChange={(e) => {
handleModelChange(e);
setCurrentModel({ label: e.label, value: e.value });
}}
className={cn(
"react-select-container transition-all group-hover:w-full group-active:w-full focus:w-full",
open && "!w-full",
)}
classNamePrefix="react-select"
value={currentModel}
/>
<SelectModel />
</div>
{!batchMode && (

View File

@ -4,8 +4,7 @@ import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useAtomValue } from "jotai";
import { translationAtom } from "@/atoms/translations-atom";
const t = useAtomValue(translationAtom);
import useTranslation from "../hooks/use-translation";
const Dialog = DialogPrimitive.Root;
@ -32,26 +31,32 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">{t("APP.DIALOG_BOX.CLOSE")}</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
hideCloseButton?: boolean;
}
>(({ className, children, ...props }, ref) => {
const t = useTranslation();
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-accent data-[state=open]:text-muted-foreground hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
<X className="h-4 w-4" />
<span className="sr-only">{t("APP.DIALOG_BOX.CLOSE")}</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({

View File

@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -1 +1,9 @@
export const VALID_IMAGE_FORMATS = ["png", "jpg", "jpeg", "jfif", "webp"];
export const VALID_IMAGE_FORMATS = [
"png",
"jpg",
"jpeg",
"jfif",
"webp",
] as const;
export type ImageFormat = (typeof VALID_IMAGE_FORMATS)[number];

View File

@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

@ -1,8 +1,8 @@
"use client";
import { useState, useEffect } from "react";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { modelsListAtom } from "../atoms/models-list-atom";
import { useAtomValue, useSetAtom } from "jotai";
import { customModelIdsAtom } from "../atoms/models-list-atom";
import {
batchModeAtom,
savedOutputPathAtom,
@ -10,7 +10,6 @@ import {
rememberOutputFolderAtom,
} from "../atoms/user-settings-atom";
import useLogger from "../components/hooks/use-logger";
import { newsAtom, showNewsModalAtom } from "@/atoms/news-atom";
import { useToast } from "@/components/ui/use-toast";
import { ToastAction } from "@/components/ui/toast";
import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
@ -19,7 +18,7 @@ import Sidebar from "@/components/sidebar";
import MainContent from "@/components/main-content";
import getDirectoryFromPath from "@common/get-directory-from-path";
import { FEATURE_FLAGS } from "@common/feature-flags";
import { VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
import { ImageFormat, VALID_IMAGE_FORMATS } from "@/lib/valid-formats";
import { initCustomModels } from "@/components/hooks/use-custom-models";
const Home = () => {
@ -30,7 +29,6 @@ const Home = () => {
initCustomModels();
const [isLoading, setIsLoading] = useState(true);
const [imagePath, setImagePath] = useState("");
const [upscaledImagePath, setUpscaledImagePath] = useState("");
const [dimensions, setDimensions] = useState({
@ -39,18 +37,12 @@ const Home = () => {
});
const setOutputPath = useSetAtom(savedOutputPathAtom);
const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const batchMode = useAtomValue(batchModeAtom);
const [batchFolderPath, setBatchFolderPath] = useState("");
const [upscaledBatchFolderPath, setUpscaledBatchFolderPath] = useState("");
const setProgress = useSetAtom(progressAtom);
const [doubleUpscaylCounter, setDoubleUpscaylCounter] = useState(0);
const [modelOptions, setModelOptions] = useAtom(modelsListAtom);
const [news, setNews] = useAtom(newsAtom);
const [showNewsModal, setShowNewsModal] = useAtom(showNewsModalAtom);
const setModelIds = useSetAtom(customModelIdsAtom);
const selectImageHandler = async () => {
resetImagePaths();
@ -89,7 +81,7 @@ const Home = () => {
const validateImagePath = (path: string) => {
if (path.length > 0) {
logit("🖼 imagePath: ", path);
const extension = path.split(".").pop().toLowerCase();
const extension = path.split(".").pop().toLowerCase() as ImageFormat;
logit("🔤 Extension: ", extension);
if (!VALID_IMAGE_FORMATS.includes(extension)) {
toast({
@ -260,19 +252,8 @@ const Home = () => {
ELECTRON_COMMANDS.CUSTOM_MODEL_FILES_LIST,
(_, data: string[]) => {
logit(`📜 CUSTOM_MODEL_FILES_LIST: `, data);
const newModelOptions = data.map((model) => {
return {
value: model,
label: model,
};
});
// Add newModelsList to modelOptions and remove duplicates
const modelMap = new Map();
[...modelOptions, ...newModelOptions].forEach((model) => {
modelMap.set(model.value, model);
});
const uniqueModelOptions = Array.from(modelMap.values());
setModelOptions(uniqueModelOptions);
console.log("🚀 => data:", data);
setModelIds(data);
},
);
}, []);