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

Refactor Renderer Code (#987)

* Initial refactor

* Remove unused imports

* Update code

* Refactor and Update Code

- Change file names to kebab-caase
- Add new useTranslation Hook
- Change useLog hook name to useLogger
- Update translation hook to provide autocomplete

* Update import and component name

* Rename files and components

* Update locales

* Update electron commands

* Update var

* Change Lowercase

* Replace filter with map

* Add props

* Update flag check

* Add validate paths

* Update formats

* Update import

* Update function

* Update function and translation

* Update handlePaste
This commit is contained in:
NayamAmarshe 2024-10-04 14:45:54 +05:30 committed by GitHub
parent bf62c684c2
commit 95843ded88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 1415 additions and 1283 deletions

View File

@ -1,4 +1,4 @@
const COMMAND = {
const ELECTRON_COMMANDS = {
SELECT_FILE: "Select a File",
SELECT_FOLDER: "Select a Folder",
UPSCAYL: "Upscale the Image",
@ -25,6 +25,6 @@ const COMMAND = {
OS: "Get OS",
SCALING_AND_CONVERTING: "Adding some finishing touches",
UPSCAYL_ERROR: "Upscaling Error",
};
} as const;
export default COMMAND;
export { ELECTRON_COMMANDS };

View File

@ -3,7 +3,7 @@ type FeatureFlags = {
SHOW_UPSCAYL_CLOUD_INFO: boolean;
};
export const featureFlags: FeatureFlags = {
export const FEATURE_FLAGS: FeatureFlags = {
APP_STORE_BUILD: false,
SHOW_UPSCAYL_CLOUD_INFO: false,
};

View File

@ -11,7 +11,7 @@ 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 COMMAND from "../../common/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";
@ -72,28 +72,28 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => {
if (!mainWindow) return;
data = data.toString();
mainWindow.webContents.send(
COMMAND.FOLDER_UPSCAYL_PROGRESS,
ELECTRON_COMMANDS.FOLDER_UPSCAYL_PROGRESS,
data.toString(),
);
if ((data as string).includes("Error")) {
logit("❌ ", data);
encounteredError = true;
} else if (data.includes("Resizing")) {
mainWindow.webContents.send(COMMAND.SCALING_AND_CONVERTING);
mainWindow.webContents.send(ELECTRON_COMMANDS.SCALING_AND_CONVERTING);
}
};
const onError = (data: any) => {
if (!mainWindow) return;
mainWindow.setProgressBar(-1);
mainWindow.webContents.send(
COMMAND.FOLDER_UPSCAYL_PROGRESS,
ELECTRON_COMMANDS.FOLDER_UPSCAYL_PROGRESS,
data.toString(),
);
failed = true;
upscayl.kill();
mainWindow &&
mainWindow.webContents.send(
COMMAND.UPSCAYL_ERROR,
ELECTRON_COMMANDS.UPSCAYL_ERROR,
`Error upscaling images! ${data}`,
);
return;
@ -104,7 +104,7 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => {
logit("💯 Done upscaling");
upscayl.kill();
mainWindow.webContents.send(
COMMAND.FOLDER_UPSCAYL_DONE,
ELECTRON_COMMANDS.FOLDER_UPSCAYL_DONE,
outputFolderPath,
);
if (!encounteredError) {

View File

@ -5,11 +5,11 @@ import {
} from "../utils/config-variables";
import logit from "../utils/logit";
import slash from "../utils/slash";
import COMMAND from "../../common/commands";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import getModels from "../utils/get-models";
import { getMainWindow } from "../main-window";
import settings from "electron-settings";
import { featureFlags } from "../../common/feature-flags";
import { FEATURE_FLAGS } from "../../common/feature-flags";
const customModelsSelect = async (event, message) => {
const mainWindow = getMainWindow();
@ -27,7 +27,7 @@ const customModelsSelect = async (event, message) => {
message: "Select Custom Models Folder that is named 'models'",
});
if (featureFlags.APP_STORE_BUILD && bookmarks && bookmarks.length > 0) {
if (FEATURE_FLAGS.APP_STORE_BUILD && bookmarks && bookmarks.length > 0) {
console.log("🚨 Setting Bookmark: ", bookmarks);
await settings.set("custom-models-bookmarks", bookmarks[0]);
}
@ -55,7 +55,10 @@ const customModelsSelect = async (event, message) => {
}
const models = await getModels(savedCustomModelsPath);
mainWindow.webContents.send(COMMAND.CUSTOM_MODEL_FILES_LIST, models);
mainWindow.webContents.send(
ELECTRON_COMMANDS.CUSTOM_MODEL_FILES_LIST,
models,
);
logit("📁 Custom Folder Path: ", savedCustomModelsPath);
return savedCustomModelsPath;

View File

@ -14,7 +14,7 @@ import {
} from "../utils/get-arguments";
import { modelsPath } from "../utils/get-resource-paths";
import logit from "../utils/logit";
import COMMAND from "../../common/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";
@ -87,12 +87,15 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
if (!mainWindow) return;
data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER
mainWindow.webContents.send(COMMAND.DOUBLE_UPSCAYL_PROGRESS, data);
mainWindow.webContents.send(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_PROGRESS,
data,
);
// SET FAILED TO TRUE
failed2 = true;
mainWindow &&
mainWindow.webContents.send(
COMMAND.UPSCAYL_ERROR,
ELECTRON_COMMANDS.UPSCAYL_ERROR,
"Error upscaling image. Error: " + data,
);
showNotification("Upscayl Failure", "Failed to upscale image!");
@ -105,13 +108,16 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
// CONVERT DATA TO STRING
data = data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER
mainWindow.webContents.send(COMMAND.DOUBLE_UPSCAYL_PROGRESS, data);
mainWindow.webContents.send(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_PROGRESS,
data,
);
// IF PROGRESS HAS ERROR, UPSCAYL FAILED
if (data.includes("Error")) {
upscayl2.kill();
failed2 = true;
} else if (data.includes("Resizing")) {
mainWindow.webContents.send(COMMAND.SCALING_AND_CONVERTING);
mainWindow.webContents.send(ELECTRON_COMMANDS.SCALING_AND_CONVERTING);
}
};
@ -121,7 +127,10 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
logit("💯 Done upscaling");
mainWindow.setProgressBar(-1);
mainWindow.webContents.send(COMMAND.DOUBLE_UPSCAYL_DONE, outFile);
mainWindow.webContents.send(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_DONE,
outFile,
);
showNotification("Upscayled", "Image upscayled successfully!");
}
};
@ -132,12 +141,15 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
mainWindow.setProgressBar(-1);
data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER
mainWindow.webContents.send(COMMAND.DOUBLE_UPSCAYL_PROGRESS, data);
mainWindow.webContents.send(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_PROGRESS,
data,
);
// SET FAILED TO TRUE
failed = true;
mainWindow &&
mainWindow.webContents.send(
COMMAND.UPSCAYL_ERROR,
ELECTRON_COMMANDS.UPSCAYL_ERROR,
"Error upscaling image. Error: " + data,
);
showNotification("Upscayl Failure", "Failed to upscale image!");
@ -150,13 +162,16 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
// CONVERT DATA TO STRING
data = data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER
mainWindow.webContents.send(COMMAND.DOUBLE_UPSCAYL_PROGRESS, data);
mainWindow.webContents.send(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_PROGRESS,
data,
);
// IF PROGRESS HAS ERROR, UPSCAYL FAILED
if (data.includes("Error") || data.includes("failed")) {
upscayl.kill();
failed = true;
} else if (data.includes("Resizing")) {
mainWindow.webContents.send(COMMAND.SCALING_AND_CONVERTING);
mainWindow.webContents.send(ELECTRON_COMMANDS.SCALING_AND_CONVERTING);
}
};

View File

@ -1,4 +1,4 @@
import COMMAND from "../../common/commands";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { getMainWindow } from "../main-window";
import {
savedCustomModelsPath,
@ -17,7 +17,10 @@ const getModelsList = async (event, payload) => {
logit("📁 Custom Models Folder Path: ", savedCustomModelsPath);
const models = await getModels(payload);
mainWindow.webContents.send(COMMAND.CUSTOM_MODEL_FILES_LIST, models);
mainWindow.webContents.send(
ELECTRON_COMMANDS.CUSTOM_MODEL_FILES_LIST,
models,
);
}
};

View File

@ -1,6 +1,6 @@
import fs from "fs";
import { modelsPath } from "../utils/get-resource-paths";
import COMMAND from "../../common/commands";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import {
savedCustomModelsPath,
setChildProcesses,
@ -60,14 +60,17 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
// Check if windows can write the new filename to the file system
if (outFile.length >= 255) {
logit("Filename too long for Windows.");
mainWindow.webContents.send(COMMAND.UPSCAYL_ERROR, "The filename exceeds the maximum path length allowed by Windows. Please shorten the filename or choose a different save location.");
mainWindow.webContents.send(
ELECTRON_COMMANDS.UPSCAYL_ERROR,
"The filename exceeds the maximum path length allowed by Windows. Please shorten the filename or choose a different save location.",
);
}
// UPSCALE
if (fs.existsSync(outFile) && !overwrite) {
// If already upscayled, just output that file
logit("✅ Already upscayled at: ", outFile);
mainWindow.webContents.send(COMMAND.UPSCAYL_DONE, outFile);
mainWindow.webContents.send(ELECTRON_COMMANDS.UPSCAYL_DONE, outFile);
} else {
logit(
"✅ Upscayl Variables: ",
@ -115,18 +118,24 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
logit(data.toString());
mainWindow.setProgressBar(parseFloat(data.slice(0, data.length)) / 100);
data = data.toString();
mainWindow.webContents.send(COMMAND.UPSCAYL_PROGRESS, data.toString());
mainWindow.webContents.send(
ELECTRON_COMMANDS.UPSCAYL_PROGRESS,
data.toString(),
);
if (data.includes("Error")) {
upscayl.kill();
failed = true;
} else if (data.includes("Resizing")) {
mainWindow.webContents.send(COMMAND.SCALING_AND_CONVERTING);
mainWindow.webContents.send(ELECTRON_COMMANDS.SCALING_AND_CONVERTING);
}
};
const onError = (data) => {
if (!mainWindow) return;
mainWindow.setProgressBar(-1);
mainWindow.webContents.send(COMMAND.UPSCAYL_ERROR, data.toString());
mainWindow.webContents.send(
ELECTRON_COMMANDS.UPSCAYL_ERROR,
data.toString(),
);
failed = true;
upscayl.kill();
return;
@ -137,7 +146,7 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
// Free up memory
upscayl.kill();
mainWindow.setProgressBar(-1);
mainWindow.webContents.send(COMMAND.UPSCAYL_DONE, outFile);
mainWindow.webContents.send(ELECTRON_COMMANDS.UPSCAYL_DONE, outFile);
showNotification("Upscayl", "Image upscayled successfully!");
}
};

View File

@ -3,7 +3,7 @@ import { getMainWindow } from "../main-window";
import { savedImagePath, setSavedImagePath } from "../utils/config-variables";
import logit from "../utils/logit";
import settings from "electron-settings";
import { featureFlags } from "../../common/feature-flags";
import { FEATURE_FLAGS } from "../../common/feature-flags";
const selectFile = async () => {
const mainWindow = getMainWindow();
@ -33,7 +33,7 @@ const selectFile = async () => {
],
});
if (featureFlags.APP_STORE_BUILD && bookmarks && bookmarks.length > 0) {
if (FEATURE_FLAGS.APP_STORE_BUILD && bookmarks && bookmarks.length > 0) {
console.log("🚨 Setting Bookmark: ", bookmarks);
settings.set("file-bookmarks", bookmarks[0]);
}

View File

@ -5,12 +5,12 @@ import {
} from "../utils/config-variables";
import logit from "../utils/logit";
import settings from "electron-settings";
import { featureFlags } from "../../common/feature-flags";
import { FEATURE_FLAGS } from "../../common/feature-flags";
const selectFolder = async (event, message) => {
let closeAccess;
const folderBookmarks = await settings.get("folder-bookmarks");
if (featureFlags.APP_STORE_BUILD && folderBookmarks) {
if (FEATURE_FLAGS.APP_STORE_BUILD && folderBookmarks) {
logit("🚨 Folder Bookmarks: ", folderBookmarks);
try {
closeAccess = app.startAccessingSecurityScopedResource(
@ -31,7 +31,7 @@ const selectFolder = async (event, message) => {
securityScopedBookmarks: true,
});
if (featureFlags.APP_STORE_BUILD && bookmarks && bookmarks.length > 0) {
if (FEATURE_FLAGS.APP_STORE_BUILD && bookmarks && bookmarks.length > 0) {
console.log("🚨 Setting folder Bookmark: ", bookmarks);
await settings.set("folder-bookmarks", bookmarks[0]);
}

View File

@ -2,7 +2,7 @@ import prepareNext from "electron-next";
import { autoUpdater } from "electron-updater";
import log from "electron-log";
import { app, ipcMain, protocol } from "electron";
import COMMAND from "../common/commands";
import { ELECTRON_COMMANDS } from "../common/electron-commands";
import logit from "./utils/logit";
import openFolder from "./commands/open-folder";
import stop from "./commands/stop";
@ -17,7 +17,7 @@ import { execPath, modelsPath } from "./utils/get-resource-paths";
import batchUpscayl from "./commands/batch-upscayl";
import doubleUpscayl from "./commands/double-upscayl";
import autoUpdate from "./commands/auto-update";
import { featureFlags } from "../common/feature-flags";
import { FEATURE_FLAGS } from "../common/feature-flags";
import settings from "electron-settings";
// INITIALIZATION
@ -43,14 +43,14 @@ app.on("ready", async () => {
log.info(
"🆙 Upscayl version:",
app.getVersion(),
featureFlags.APP_STORE_BUILD && "MAC-APP-STORE",
FEATURE_FLAGS.APP_STORE_BUILD && "MAC-APP-STORE",
);
log.info("🚀 UPSCAYL EXEC PATH: ", execPath);
log.info("🚀 MODELS PATH: ", modelsPath);
let closeAccess;
const folderBookmarks = await settings.get("folder-bookmarks");
if (featureFlags.APP_STORE_BUILD && folderBookmarks) {
if (FEATURE_FLAGS.APP_STORE_BUILD && folderBookmarks) {
logit("🚨 Folder Bookmarks: ", folderBookmarks);
try {
closeAccess = app.startAccessingSecurityScopedResource(
@ -68,29 +68,32 @@ app.on("window-all-closed", () => {
});
// ! ENABLE THIS FOR MACOS APP STORE BUILD
if (featureFlags.APP_STORE_BUILD) {
if (FEATURE_FLAGS.APP_STORE_BUILD) {
logit("🚀 APP STORE BUILD ENABLED");
app.commandLine.appendSwitch("in-process-gpu");
}
ipcMain.on(COMMAND.STOP, stop);
ipcMain.on(ELECTRON_COMMANDS.STOP, stop);
ipcMain.on(COMMAND.OPEN_FOLDER, openFolder);
ipcMain.on(ELECTRON_COMMANDS.OPEN_FOLDER, openFolder);
ipcMain.handle(COMMAND.SELECT_FOLDER, selectFolder);
ipcMain.handle(ELECTRON_COMMANDS.SELECT_FOLDER, selectFolder);
ipcMain.handle(COMMAND.SELECT_FILE, selectFile);
ipcMain.handle(ELECTRON_COMMANDS.SELECT_FILE, selectFile);
ipcMain.on(COMMAND.GET_MODELS_LIST, getModelsList);
ipcMain.on(ELECTRON_COMMANDS.GET_MODELS_LIST, getModelsList);
ipcMain.handle(COMMAND.SELECT_CUSTOM_MODEL_FOLDER, customModelsSelect);
ipcMain.handle(
ELECTRON_COMMANDS.SELECT_CUSTOM_MODEL_FOLDER,
customModelsSelect,
);
ipcMain.on(COMMAND.UPSCAYL, imageUpscayl);
ipcMain.on(ELECTRON_COMMANDS.UPSCAYL, imageUpscayl);
ipcMain.on(COMMAND.FOLDER_UPSCAYL, batchUpscayl);
ipcMain.on(ELECTRON_COMMANDS.FOLDER_UPSCAYL, batchUpscayl);
ipcMain.on(COMMAND.DOUBLE_UPSCAYL, doubleUpscayl);
ipcMain.on(ELECTRON_COMMANDS.DOUBLE_UPSCAYL, doubleUpscayl);
if (!featureFlags.APP_STORE_BUILD) {
if (!FEATURE_FLAGS.APP_STORE_BUILD) {
autoUpdater.on("update-downloaded", autoUpdate);
}

View File

@ -1,7 +1,7 @@
import { BrowserWindow, shell } from "electron";
import { getPlatform } from "./utils/get-device-specs";
import { join } from "path";
import COMMAND from "../common/commands";
import { ELECTRON_COMMANDS } from "../common/electron-commands";
import { fetchLocalStorage } from "./utils/config-variables";
import electronIsDev from "electron-is-dev";
import { format } from "url";
@ -49,7 +49,7 @@ const createMainWindow = () => {
fetchLocalStorage();
mainWindow.webContents.send(COMMAND.OS, getPlatform());
mainWindow.webContents.send(ELECTRON_COMMANDS.OS, getPlatform());
mainWindow.setMenuBarVisibility(false);
};

View File

@ -2,7 +2,7 @@ import fs from "fs";
import logit from "./logit";
import { MessageBoxOptions, app, dialog } from "electron";
import settings from "electron-settings";
import { featureFlags } from "../../common/feature-flags";
import { FEATURE_FLAGS } from "../../common/feature-flags";
const getModels = async (folderPath: string | undefined) => {
let models: string[] = [];
@ -11,14 +11,14 @@ const getModels = async (folderPath: string | undefined) => {
// SECURITY SCOPED BOOKMARKS
let closeAccess;
const customModelsBookmarks = await settings.get("custom-models-bookmarks");
if (featureFlags.APP_STORE_BUILD && customModelsBookmarks) {
if (FEATURE_FLAGS.APP_STORE_BUILD && customModelsBookmarks) {
console.log(
"🚀 => file: get-models.ts:18 => customModelsBookmarks:",
customModelsBookmarks
customModelsBookmarks,
);
try {
closeAccess = app.startAccessingSecurityScopedResource(
customModelsBookmarks as string
customModelsBookmarks as string,
);
} catch (error) {
logit("📁 Custom Models Bookmarks Error: ", error);

View File

@ -1,5 +1,5 @@
import log from "electron-log";
import COMMAND from "../../common/commands";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { getMainWindow } from "../main-window";
const logit = (...args: any) => {
@ -9,7 +9,7 @@ const logit = (...args: any) => {
if (process.env.NODE_ENV === "development") {
return;
}
mainWindow.webContents.send(COMMAND.LOG, args.join(" "));
mainWindow.webContents.send(ELECTRON_COMMANDS.LOG, args.join(" "));
};
export default logit;

View File

@ -11,19 +11,6 @@ import { atomWithStorage } from "jotai/utils";
type Translations = typeof en;
type Locales = "en" | "ru" | "ja" | "zh" | "es" | "fr";
// Utility function to access nested translation keys
const getNestedTranslation = (obj: Translations, key: string): string => {
return (
key.split(".").reduce((acc, part) => acc && (acc as any)[part], obj) || key
);
};
// Atom to store the current locale
export const localeAtom = atomWithStorage<Locales>("language", "en");
// Atom to get the translation function based on the current locale
export const translationAtom = atom((get) => {
const locale = get(localeAtom);
const translations: Record<Locales, Translations> = {
en,
ru,
@ -33,7 +20,44 @@ export const translationAtom = atom((get) => {
fr,
};
return (key: string, params: Record<string, string> = {}): string => {
// Create a type for nested key paths
type NestedKeyOf<Object> = Object extends object
? {
[Key in keyof Object]: Key extends string | number
? Key | `${Key}.${NestedKeyOf<Object[Key]>}`
: never;
}[keyof Object]
: never;
// Utility function to access nested translation keys
const getNestedTranslation = (
obj: Translations,
key: NestedKeyOf<Translations>,
): string => {
// Split the key into an array of nested parts
const keyParts = key.split(".");
// Traverse the object using the key parts
const result = keyParts.reduce((currentObj, part) => {
// If currentObj is falsy or doesn't have the property, return undefined
return currentObj && currentObj[part];
}, obj);
// Return the found translation or the original key if not found
return result || key;
};
// Atom to store the current locale
export const localeAtom = atomWithStorage<Locales>("language", "en");
// Atom to get the translation function based on the current locale
export const translationAtom = atom((get) => {
const locale = get(localeAtom);
return (
key: NestedKeyOf<Translations>,
params: Record<string, string> = {},
): string => {
const template = getNestedTranslation(translations[locale], key);
// Replace placeholders with parameters, e.g., {name} => John

View File

@ -1,4 +1,4 @@
import { newsAtom, showNewsModalAtom } from "@/atoms/newsAtom";
import { newsAtom, showNewsModalAtom } from "@/atoms/news-atom";
import { translationAtom } from "@/atoms/translations-atom";
import { useAtomValue, useSetAtom } from "jotai";
import React from "react";

View File

@ -1,6 +1,6 @@
import { featureFlags } from "@common/feature-flags";
import { FEATURE_FLAGS } from "@common/feature-flags";
import React from "react";
import Logo from "./icons/Logo";
import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
import { useAtomValue } from "jotai";
import { translationAtom } from "@/atoms/translations-atom";
@ -16,12 +16,12 @@ export default function Header({ version }: { version: string }) {
data-tooltip-content={t("HEADER.GITHUB_BUTTON_TITLE")}
>
<div className="flex items-center gap-3 px-5 py-5">
<Logo className="inline-block h-14 w-14" />
<UpscaylSVGLogo className="inline-block h-14 w-14" />
<div className="flex flex-col justify-center">
<h1 className="text-3xl font-bold">
{t("TITLE")}{" "}
<span className="text-xs">
{version} {featureFlags.APP_STORE_BUILD && "Mac"}
{version} {FEATURE_FLAGS.APP_STORE_BUILD && "Mac"}
</span>
</h1>
<p className="">{t("HEADER.DESCRIPTION")}</p>

View File

@ -1,66 +0,0 @@
import { translationAtom } from "@/atoms/translations-atom";
import { GrayMatterFile } from "gray-matter";
import { useAtomValue } from "jotai";
import React from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
export const NewsModal = ({
show,
setShow,
news,
}: {
show: boolean;
setShow: React.Dispatch<React.SetStateAction<boolean>>;
news: GrayMatterFile<string>;
}) => {
const t = useAtomValue(translationAtom);
return (
<dialog className={`modal ${show && "modal-open"}`}>
<div className="modal-box flex flex-col items-center gap-4 text-center">
<button
className="btn btn-circle absolute right-4 top-2"
onClick={() => setShow(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<rect
x="0"
y="0"
width="24"
height="24"
fill="none"
stroke="none"
/>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="1.5"
d="m8.464 15.535l7.072-7.07m-7.072 0l7.072 7.07"
/>
</svg>
</button>
<div>
{news && (
<Markdown remarkPlugins={[remarkGfm]} className="prose">
{news.content}
</Markdown>
)}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={() => setShow(false)}>
{t("APP.DIALOG_BOX.CLOSE")}
</button>
</form>
</dialog>
);
};

View File

@ -0,0 +1,17 @@
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { useEffect } from "react";
import useLogger from "./use-logger";
export const initCustomModels = () => {
const logit = useLogger();
useEffect(() => {
const customModelsPath = JSON.parse(
localStorage.getItem("customModelsPath"),
);
if (customModelsPath !== null) {
window.electron.send(ELECTRON_COMMANDS.GET_MODELS_LIST, customModelsPath);
logit("🎯 GET_MODELS_LIST: ", customModelsPath);
}
}, []);
};

View File

@ -0,0 +1,17 @@
import { useEffect } from "react";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
const useElectron = ({
command,
func,
}: {
command: (typeof ELECTRON_COMMANDS)[keyof typeof ELECTRON_COMMANDS];
func: (...args: any[]) => void;
}) => {
useEffect(() => {
window.electron.on(command, func);
return () => {
window.electron.off(command, func);
};
}, []);
};

View File

@ -1,9 +1,9 @@
import { logAtom } from "../../atoms/logAtom";
import { logAtom } from "../../atoms/log-atom";
import log from "electron-log/renderer";
import { useSetAtom } from "jotai";
import React from "react";
const useLog = () => {
const useLogger = () => {
const setLogData = useSetAtom(logAtom);
const logit = (...args: any) => {
@ -13,9 +13,7 @@ const useLog = () => {
setLogData((prevLogData) => [...prevLogData, data]);
};
return {
logit,
};
return logit;
};
export default useLog;
export default useLogger;

View File

@ -0,0 +1,9 @@
import { translationAtom } from "@/atoms/translations-atom";
import { useAtomValue } from "jotai";
const useTranslation = () => {
const t = useAtomValue(translationAtom);
return t;
};
export default useTranslation;

View File

@ -0,0 +1,16 @@
import { useState, useEffect } from "react";
const useUpscaylVersion = () => {
const [version, setVersion] = useState<string | null>(null);
useEffect(() => {
const upscaylVersion = navigator?.userAgent?.match(
/Upscayl\/([\d\.]+\d+)/,
)?.[1];
setVersion(upscaylVersion);
}, []);
return version;
};
export default useUpscaylVersion;

View File

@ -1,7 +1,3 @@
// React SVG Component
import React from "react";
function Spinner() {
return (
// By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL
@ -10,7 +6,8 @@ function Spinner() {
fill="currentColor"
stroke="currentColor"
xmlns="http://www.w3.org/2000/svg"
className="h-16 w-16 rounded-full bg-base-300 p-2 text-base-content">
className="h-16 w-16 rounded-full bg-base-300 p-2 text-base-content"
>
<g fill="none" fill-rule="evenodd">
<g transform="translate(2 1)" stroke="currentColor" stroke-width="1.5">
<circle
@ -18,7 +15,8 @@ function Spinner() {
cy="11.462"
r="5"
fill-opacity="1"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"
@ -33,7 +31,8 @@ function Spinner() {
cy="27.063"
r="5"
fill-opacity="0"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"
@ -48,7 +47,8 @@ function Spinner() {
cy="42.663"
r="5"
fill-opacity="0"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"
@ -63,7 +63,8 @@ function Spinner() {
cy="49.125"
r="5"
fill-opacity="0"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"
@ -78,7 +79,8 @@ function Spinner() {
cy="42.663"
r="5"
fill-opacity="0"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"
@ -93,7 +95,8 @@ function Spinner() {
cy="27.063"
r="5"
fill-opacity="0"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"
@ -108,7 +111,8 @@ function Spinner() {
cy="11.462"
r="5"
fill-opacity="0"
fill="currentColor">
fill="currentColor"
>
<animate
attributeName="fill-opacity"
begin="0s"

View File

@ -1,6 +1,6 @@
import React from "react";
const Logo = ({ ...rest }) => {
const UpscaylSVGLogo = ({ ...rest }) => {
return (
<svg
viewBox="0 0 256 256"
@ -166,4 +166,4 @@ const Logo = ({ ...rest }) => {
);
};
export default Logo;
export default UpscaylSVGLogo;

View File

@ -1,20 +1,18 @@
import { translationAtom } from "@/atoms/translations-atom";
import { lensSizeAtom, viewTypeAtom } from "@/atoms/userSettingsAtom";
import { lensSizeAtom, viewTypeAtom } from "@/atoms/user-settings-atom";
import { cn } from "@/lib/utils";
import { useAtom, useAtomValue } from "jotai";
import { WrenchIcon } from "lucide-react";
import { useEffect, useState } from "react";
const ImageOptions = ({
const ImageViewSettings = ({
zoomAmount,
setZoomAmount,
resetImagePaths,
hideZoomOptions,
}: {
zoomAmount: string;
setZoomAmount: (arg: any) => void;
resetImagePaths: () => void;
hideZoomOptions?: boolean;
}) => {
const [openSidebar, setOpenSidebar] = useState(false);
const [viewType, setViewType] = useAtom(viewTypeAtom);
@ -113,4 +111,4 @@ const ImageOptions = ({
);
};
export default ImageOptions;
export default ImageViewSettings;

View File

@ -0,0 +1,26 @@
import { sanitizePath } from "@common/sanitize-path";
const ImageViewer = ({
imagePath,
setDimensions,
}: {
imagePath: string;
setDimensions: (dimensions: { width: number; height: number }) => void;
}) => {
return (
<img
src={"file:///" + sanitizePath(imagePath)}
onLoad={(e: React.SyntheticEvent<HTMLImageElement>) => {
setDimensions({
width: e.currentTarget.naturalWidth,
height: e.currentTarget.naturalHeight,
});
}}
draggable="false"
alt=""
className="h-full w-full bg-gradient-to-br from-base-300 to-base-100 object-contain"
/>
);
};
export default ImageViewer;

View File

@ -0,0 +1,292 @@
"use client";
import useLogger from "../hooks/use-logger";
import { useState, useMemo } from "react";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { useAtomValue, useSetAtom } from "jotai";
import {
batchModeAtom,
lensSizeAtom,
savedOutputPathAtom,
progressAtom,
viewTypeAtom,
rememberOutputFolderAtom,
} from "../../atoms/user-settings-atom";
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 ProgressBar from "./progress-bar";
import InstructionsCard from "./instructions-card";
import ImageViewSettings from "./image-view-settings";
import useUpscaylVersion from "../hooks/use-upscayl-version";
import MacTitlebarDragRegion from "./mac-titlebar-drag-region";
import LensViewer from "./lens-view";
import ImageViewer from "./image-viewer";
import useTranslation from "../hooks/use-translation";
import SliderView from "./slider-view";
type MainContentProps = {
imagePath: string;
resetImagePaths: () => void;
upscaledBatchFolderPath: string;
setImagePath: React.Dispatch<React.SetStateAction<string>>;
validateImagePath: (path: string) => void;
selectFolderHandler: () => void;
selectImageHandler: () => void;
upscaledImagePath: string;
batchFolderPath: string;
doubleUpscaylCounter: number;
setDimensions: React.Dispatch<
React.SetStateAction<{
width: number;
height: number;
}>
>;
};
const MainContent = ({
imagePath,
resetImagePaths,
upscaledBatchFolderPath,
setImagePath,
validateImagePath,
selectFolderHandler,
selectImageHandler,
upscaledImagePath,
batchFolderPath,
doubleUpscaylCounter,
setDimensions,
}: MainContentProps) => {
const t = useTranslation();
const logit = useLogger();
const { toast } = useToast();
const version = useUpscaylVersion();
const setOutputPath = useSetAtom(savedOutputPathAtom);
const progress = useAtomValue(progressAtom);
const batchMode = useAtomValue(batchModeAtom);
const viewType = useAtomValue(viewTypeAtom);
const lensSize = useAtomValue(lensSizeAtom);
const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const [zoomAmount, setZoomAmount] = useState("100");
const sanitizedUpscaledImagePath = useMemo(
() => sanitizePath(upscaledImagePath),
[upscaledImagePath],
);
const showInformationCard = useMemo(() => {
if (!batchMode) {
return imagePath.length === 0 && upscaledImagePath.length === 0;
} else {
return (
batchFolderPath.length === 0 && upscaledBatchFolderPath.length === 0
);
}
}, [
batchMode,
imagePath,
upscaledImagePath,
batchFolderPath,
upscaledBatchFolderPath,
]);
// DRAG AND DROP HANDLERS
const handleDragEnter = (e) => {
e.preventDefault();
console.log("drag enter");
};
const handleDragLeave = (e) => {
e.preventDefault();
console.log("drag leave");
};
const handleDragOver = (e) => {
e.preventDefault();
console.log("drag over");
};
const openFolderHandler = (e) => {
const logit = useLogger();
logit("📂 OPEN_FOLDER: ", upscaledBatchFolderPath);
window.electron.send(
ELECTRON_COMMANDS.OPEN_FOLDER,
upscaledBatchFolderPath,
);
};
const sanitizedImagePath = useMemo(
() => sanitizePath(imagePath),
[imagePath],
);
const handleDrop = (e) => {
e.preventDefault();
resetImagePaths();
if (
e.dataTransfer.items.length === 0 ||
e.dataTransfer.files.length === 0
) {
logit("👎 No valid files dropped");
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
return;
}
const type = e.dataTransfer.items[0].type;
const filePath = e.dataTransfer.files[0].path;
const extension = e.dataTransfer.files[0].name.split(".").at(-1);
logit("⤵️ Dropped file: ", JSON.stringify({ type, filePath, extension }));
if (
!type.includes("image") ||
!VALID_IMAGE_FORMATS.includes(extension.toLowerCase())
) {
logit("🚫 Invalid file dropped");
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
} else {
logit("🖼 Setting image path: ", filePath);
setImagePath(filePath);
const dirname = getDirectoryFromPath(filePath);
logit("🗂 Setting output path: ", dirname);
if (!FEATURE_FLAGS.APP_STORE_BUILD) {
if (!rememberOutputFolder) {
setOutputPath(dirname);
}
}
validateImagePath(filePath);
}
};
const handlePaste = (e: React.ClipboardEvent<HTMLDivElement>) => {
resetImagePaths();
e.preventDefault();
const items = e.clipboardData.items;
const files = e.clipboardData.files;
if (items.length === 0 || files.length === 0) {
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
return;
}
const type = items[0].type;
const filePath = files[0].path;
const extension = files[0].name.split(".").at(-1);
logit("📋 Pasted file: ", JSON.stringify({ type, filePath, extension }));
if (
!type.includes("image") &&
!VALID_IMAGE_FORMATS.includes(extension.toLowerCase())
) {
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
} else {
setImagePath(filePath);
const dirname = getDirectoryFromPath(filePath);
logit("🗂 Setting output path: ", dirname);
if (!FEATURE_FLAGS.APP_STORE_BUILD) {
if (!rememberOutputFolder) {
setOutputPath(dirname);
}
}
validateImagePath(filePath);
}
};
return (
<div
className="relative flex h-screen w-full flex-col items-center justify-center"
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDoubleClick={batchMode ? selectFolderHandler : selectImageHandler}
onPaste={handlePaste}
>
<MacTitlebarDragRegion />
{progress.length > 0 &&
upscaledImagePath.length === 0 &&
upscaledBatchFolderPath.length === 0 && (
<ProgressBar
batchMode={batchMode}
progress={progress}
doubleUpscaylCounter={doubleUpscaylCounter}
resetImagePaths={resetImagePaths}
/>
)}
{/* DEFAULT PANE INFO */}
{showInformationCard && (
<InstructionsCard version={version} batchMode={batchMode} />
)}
<ImageViewSettings
zoomAmount={zoomAmount}
setZoomAmount={setZoomAmount}
resetImagePaths={resetImagePaths}
/>
{/* SHOW SELECTED IMAGE */}
{!batchMode && upscaledImagePath.length === 0 && imagePath.length > 0 && (
<ImageViewer imagePath={imagePath} setDimensions={setDimensions} />
)}
{/* BATCH UPSCALE SHOW SELECTED FOLDER */}
{batchMode &&
upscaledBatchFolderPath.length === 0 &&
batchFolderPath.length > 0 && (
<p className="select-none text-base-content">
<span className="font-bold">
{t("APP.PROGRESS.BATCH.SELECTED_FOLDER_TITLE")}
</span>{" "}
{batchFolderPath}
</p>
)}
{/* BATCH UPSCALE DONE INFO */}
{batchMode && upscaledBatchFolderPath.length > 0 && (
<div className="z-50 flex flex-col items-center">
<p className="select-none py-4 font-bold text-base-content">
{t("APP.PROGRESS.BATCH.DONE_TITLE")}
</p>
<button
className="bg-gradient-blue btn btn-primary rounded-btn p-3 font-medium text-white/90 transition-colors"
onClick={openFolderHandler}
>
{t("APP.PROGRESS.BATCH.OPEN_UPSCAYLED_FOLDER_TITLE")}
</button>
</div>
)}
{!batchMode && viewType === "lens" && upscaledImagePath && imagePath && (
<LensViewer
zoomAmount={zoomAmount}
lensSize={lensSize}
sanitizedImagePath={sanitizedImagePath}
sanitizedUpscaledImagePath={sanitizedUpscaledImagePath}
/>
)}
{/* COMPARISON SLIDER */}
{!batchMode &&
viewType === "slider" &&
imagePath.length > 0 &&
upscaledImagePath.length > 0 && (
<SliderView
sanitizedImagePath={sanitizedImagePath}
sanitizedUpscaledImagePath={sanitizedUpscaledImagePath}
zoomAmount={zoomAmount}
/>
)}
</div>
);
};
export default MainContent;

View File

@ -2,7 +2,7 @@ import { translationAtom } from "@/atoms/translations-atom";
import { useAtomValue } from "jotai";
import React from "react";
function RightPaneInfo({ version, batchMode }) {
function InstructionsCard({ version, batchMode }) {
const t = useAtomValue(translationAtom);
return (
@ -26,4 +26,4 @@ function RightPaneInfo({ version, batchMode }) {
);
}
export default RightPaneInfo;
export default InstructionsCard;

View File

@ -0,0 +1,104 @@
import React, { useRef, useState } from "react";
const LensImage = ({
src,
alt,
lensPosition,
zoomAmount,
}: {
src: string;
alt: string;
lensPosition: { x: number; y: number };
zoomAmount: number;
}) => (
<div className="h-full w-full overflow-hidden">
<img
src={src}
alt={alt}
className="h-full w-full"
style={{
objectFit: "contain",
objectPosition: `${-lensPosition.x}px ${-lensPosition.y}px`,
transform: `scale(${zoomAmount / 100})`,
transformOrigin: "top left",
}}
/>
</div>
);
const LensViewer = ({
zoomAmount,
lensSize,
sanitizedImagePath,
sanitizedUpscaledImagePath,
}: {
zoomAmount: string;
lensSize: number;
sanitizedImagePath: string;
sanitizedUpscaledImagePath: string;
}) => {
const upscaledImageRef = useRef<HTMLImageElement>(null);
const [lensPosition, setLensPosition] = useState({ x: 0, y: 0 });
const handleMouseMoveCompare = (e: React.MouseEvent) => {
if (upscaledImageRef.current) {
const { left, top, width, height } =
upscaledImageRef.current.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
setLensPosition({
x: Math.max(0, Math.min(x - lensSize, width - lensSize * 2)),
y: Math.max(0, Math.min(y - lensSize / 2, height - lensSize)),
});
}
};
return (
<div
className="group relative h-full w-full overflow-hidden"
onMouseMove={handleMouseMoveCompare}
>
{/* UPSCALED IMAGE */}
<img
className="h-full w-full object-contain"
src={"file:///" + sanitizedUpscaledImagePath}
alt="Upscaled"
ref={upscaledImageRef}
/>
{/* LENS */}
<div
className="pointer-events-none absolute opacity-0 transition-opacity before:absolute before:left-1/2 before:h-full before:w-[2px] before:bg-white group-hover:opacity-100"
style={{
left: `${lensPosition.x}px`,
top: `${lensPosition.y}px`,
width: lensSize * 2,
height: lensSize,
border: "2px solid white",
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.5)",
}}
>
<div className="flex h-full w-full">
<LensImage
src={"file:///" + sanitizedImagePath}
alt="Original"
lensPosition={lensPosition}
zoomAmount={parseInt(zoomAmount)}
/>
<LensImage
src={"file:///" + sanitizedUpscaledImagePath}
alt="Upscaled"
lensPosition={lensPosition}
zoomAmount={parseInt(zoomAmount)}
/>
</div>
<div className="absolute bottom-0 left-0 flex w-full items-center justify-around bg-black bg-opacity-50 p-1 px-2 text-center text-xs text-white backdrop-blur-sm">
<span>Original</span>
<span>Upscayl</span>
</div>
</div>
</div>
);
};
export default LensViewer;

View File

@ -0,0 +1,7 @@
const MacTitlebarDragRegion = () => {
return window.electron.platform === "mac" ? (
<div className="mac-titlebar absolute top-0 h-8 w-full"></div>
) : null;
};
export default MacTitlebarDragRegion;

View File

@ -1,22 +1,24 @@
import React, { CSSProperties, useEffect, useMemo } from "react";
import Spinner from "../../icons/Spinner";
import Logo from "@/components/icons/Logo";
import React, { useEffect } from "react";
import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
import { useAtomValue } from "jotai";
import { translationAtom } from "@/atoms/translations-atom";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import useLogger from "../hooks/use-logger";
function ProgressBar({
progress,
doubleUpscaylCounter,
stopHandler,
batchMode,
resetImagePaths,
}: {
progress: string;
doubleUpscaylCounter: number;
stopHandler: () => void;
batchMode: boolean;
resetImagePaths: () => void;
}) {
const [batchProgress, setBatchProgress] = React.useState(0);
const t = useAtomValue(translationAtom);
const logit = useLogger();
useEffect(() => {
const progressString = progress.trim().replace(/\n/g, "");
@ -26,6 +28,12 @@ function ProgressBar({
}
}, [progress]);
const stopHandler = () => {
window.electron.send(ELECTRON_COMMANDS.STOP);
logit("🛑 Stopping Upscayl");
resetImagePaths();
};
// const progressStyle = useMemo(() => {
// if (progress.includes("%")) {
// return {
@ -44,11 +52,13 @@ function ProgressBar({
return (
<div className="absolute z-50 flex h-full w-full flex-col items-center justify-center bg-base-300/50 backdrop-blur-lg">
<div className="flex flex-col items-center gap-2 rounded-btn bg-base-100/50 p-4 backdrop-blur-lg">
<Logo className="spinner h-12 w-12" />
<UpscaylSVGLogo className="spinner h-12 w-12" />
<p className="rounded-full px-2 pb-2 font-bold">
{batchMode &&
`${t("APP.PROGRESS_BAR.BATCH_UPSCAYL_IN_PROGRESS_TITLE")} ${batchProgress}`}
</p>
<div className="flex flex-col items-center gap-1">
{progress !== "Hold on..." ? (
<p className="text-sm font-bold">
@ -60,10 +70,12 @@ function ProgressBar({
) : (
<p className="text-sm font-bold">{progress}</p>
)}
<p className="animate-pulse rounded-full px-2 pb-3 text-xs font-medium text-neutral-content/50">
{t("APP.PROGRESS_BAR.IN_PROGRESS_TITLE")}
</p>
</div>
<button onClick={stopHandler} className="btn btn-outline">
{t("APP.PROGRESS_BAR.STOP_BUTTON_TITLE")}
</button>

View File

@ -0,0 +1,73 @@
import React, { useCallback, useState } from "react";
import { ReactCompareSlider } from "react-compare-slider";
import useTranslation from "../hooks/use-translation";
const SliderView = ({
sanitizedImagePath,
sanitizedUpscaledImagePath,
zoomAmount,
}: {
sanitizedImagePath: string;
sanitizedUpscaledImagePath: string;
zoomAmount: string;
}) => {
const t = useTranslation();
const [backgroundPosition, setBackgroundPosition] = useState("0% 0%");
const handleMouseMove = useCallback((e: any) => {
const { left, top, width, height } = e.target.getBoundingClientRect();
const x = ((e.pageX - left) / width) * 100;
const y = ((e.pageY - top) / height) * 100;
setBackgroundPosition(`${x}% ${y}%`);
}, []);
return (
<ReactCompareSlider
itemOne={
<>
<p className="absolute bottom-1 left-1 rounded-md bg-black p-1 text-sm font-medium text-white opacity-30">
{t("APP.SLIDER.ORIGINAL_TITLE")}
</p>
<img
/* USE REGEX TO GET THE FILENAME AND ENCODE IT INTO PROPER FORM IN ORDER TO AVOID ERRORS DUE TO SPECIAL CHARACTERS */
src={"file:///" + sanitizedImagePath}
alt={t("APP.SLIDER.ORIGINAL_TITLE")}
onMouseMove={handleMouseMove}
style={{
objectFit: "contain",
backgroundPosition: "0% 0%",
transformOrigin: backgroundPosition,
}}
className={`h-full w-full bg-gradient-to-br from-base-300 to-base-100 transition-transform group-hover:scale-[${zoomAmount}%]`}
/>
</>
}
itemTwo={
<>
<p className="absolute bottom-1 right-1 rounded-md bg-black p-1 text-sm font-medium text-white opacity-30">
{t("APP.SLIDER.UPSCAYLED_TITLE")}
</p>
<img
/* USE REGEX TO GET THE FILENAME AND ENCODE IT INTO PROPER FORM IN ORDER TO AVOID ERRORS DUE TO SPECIAL CHARACTERS */
src={"file:///" + sanitizedUpscaledImagePath}
alt={t("APP.SLIDER.UPSCAYLED_TITLE")}
style={{
objectFit: "contain",
backgroundPosition: "0% 0%",
transformOrigin: backgroundPosition,
}}
onMouseMove={handleMouseMove}
className={`h-full w-full bg-gradient-to-br from-base-300 to-base-100 transition-transform group-hover:scale-[${
zoomAmount || "100%"
}%]`}
/>
</>
}
className="group h-screen"
/>
);
};
export default SliderView;

View File

@ -0,0 +1,110 @@
import { newsAtom, showNewsModalAtom } from "@/atoms/news-atom";
import { translationAtom } from "@/atoms/translations-atom";
import matter, { GrayMatterFile } from "gray-matter";
import { useAtom, useAtomValue } from "jotai";
import React, { useEffect } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
export const NewsModal = () => {
const t = useAtomValue(translationAtom);
const [news, setNews] = useAtom(newsAtom);
const [showNewsModal, setShowNewsModal] = useAtom(showNewsModalAtom);
useEffect(() => {
// TODO: ADD AN ABOUT TAB
if (window && window.navigator.onLine === false) return;
try {
fetch("https://raw.githubusercontent.com/upscayl/upscayl/main/news.md", {
cache: "no-cache",
})
.then((res) => {
return res.text();
})
.then((result) => {
const newsData = result;
if (!newsData) {
console.log("📰 Could not fetch news data");
return;
}
const markdownData = matter(newsData);
if (!markdownData) return;
if (markdownData && markdownData.data.dontShow) {
return;
}
if (
markdownData &&
news &&
markdownData?.data?.version === news?.data?.version
) {
console.log("📰 News is up to date");
if (showNewsModal === false) {
setShowNewsModal(false);
}
} else if (markdownData) {
setNews(matter(newsData));
setShowNewsModal(true);
}
});
} catch (error) {
console.log("Could not fetch Upscayl News");
}
}, [news]);
return (
<dialog className={`modal ${showNewsModal && "modal-open"}`}>
<div className="modal-box flex flex-col items-center gap-4 text-center">
<button
className="btn btn-circle absolute right-4 top-2"
onClick={() => {
setShowNewsModal(false);
setNews((prev) => ({ ...prev, seen: true }));
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<rect
x="0"
y="0"
width="24"
height="24"
fill="none"
stroke="none"
/>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-width="1.5"
d="m8.464 15.535l7.072-7.07m-7.072 0l7.072 7.07"
/>
</svg>
</button>
<div>
{news && (
<Markdown remarkPlugins={[remarkGfm]} className="prose">
{news.content}
</Markdown>
)}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button
onClick={() => {
setShowNewsModal(false);
setNews((prev) => ({ ...prev, seen: true }));
}}
>
{t("APP.DIALOG_BOX.CLOSE")}
</button>
</form>
</dialog>
);
};

View File

@ -0,0 +1,241 @@
"use client";
import { useState } from "react";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import {
batchModeAtom,
compressionAtom,
dontShowCloudModalAtom,
noImageProcessingAtom,
savedOutputPathAtom,
overwriteAtom,
progressAtom,
scaleAtom,
customWidthAtom,
useCustomWidthAtom,
tileSizeAtom,
showSidebarAtom,
} from "../../atoms/user-settings-atom";
import useLogger from "../hooks/use-logger";
import {
BatchUpscaylPayload,
DoubleUpscaylPayload,
ImageUpscaylPayload,
} from "@common/types/types";
import { useToast } from "@/components/ui/use-toast";
import UpscaylSteps from "./upscayl-tab/upscayl-steps";
import SettingsTab from "./settings-tab";
import Footer from "../footer";
import { NewsModal } from "../news-modal";
import Tabs from "../tabs";
import Header from "../header";
import { ChevronLeftIcon } from "lucide-react";
import { logAtom } from "@/atoms/log-atom";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import useUpscaylVersion from "../hooks/use-upscayl-version";
import useTranslation from "../hooks/use-translation";
import UpscaylLogo from "./upscayl-logo";
import SidebarToggleButton from "./sidebar-button";
const Sidebar = ({
setUpscaledImagePath,
batchFolderPath,
setUpscaledBatchFolderPath,
dimensions,
imagePath,
selectImageHandler,
selectFolderHandler,
}: {
setUpscaledImagePath: React.Dispatch<React.SetStateAction<string>>;
batchFolderPath: string;
setUpscaledBatchFolderPath: React.Dispatch<React.SetStateAction<string>>;
dimensions: {
width: number | null;
height: number | null;
};
imagePath: string;
selectImageHandler: () => Promise<void>;
selectFolderHandler: () => Promise<void>;
}) => {
const t = useTranslation();
const logit = useLogger();
const { toast } = useToast();
const version = useUpscaylVersion();
// 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 [selectedTab, setSelectedTab] = useState(0);
const [showCloudModal, setShowCloudModal] = useState(false);
// ATOMIC STATES
const outputPath = useAtomValue(savedOutputPathAtom);
const [compression, setCompression] = useAtom(compressionAtom);
const setProgress = useSetAtom(progressAtom);
const [batchMode, setBatchMode] = useAtom(batchModeAtom);
const logData = useAtomValue(logAtom);
const [scale] = useAtom(scaleAtom);
const setDontShowCloudModal = useSetAtom(dontShowCloudModalAtom);
const noImageProcessing = useAtomValue(noImageProcessingAtom);
const customWidth = useAtomValue(customWidthAtom);
const useCustomWidth = useAtomValue(useCustomWidthAtom);
const tileSize = useAtomValue(tileSizeAtom);
const [showSidebar, setShowSidebar] = useAtom(showSidebarAtom);
const handleModelChange = (e: any) => {
setModel(e.value);
logit("🔀 Model changed: ", e.value);
localStorage.setItem(
"model",
JSON.stringify({ label: e.label, value: e.value }),
);
};
const upscaylHandler = async () => {
logit("🔄 Resetting Upscaled Image Path");
setUpscaledImagePath("");
setUpscaledBatchFolderPath("");
if (imagePath !== "" || batchFolderPath !== "") {
setProgress(t("APP.PROGRESS.WAIT_TITLE"));
// Double Upscayl
if (doubleUpscayl) {
window.electron.send<DoubleUpscaylPayload>(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL,
{
imagePath,
outputPath,
model,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
noImageProcessing,
compression: compression.toString(),
customWidth: customWidth > 0 ? customWidth.toString() : null,
useCustomWidth,
tileSize,
},
);
logit("🏁 DOUBLE_UPSCAYL");
} else if (batchMode) {
// Batch Upscayl
setDoubleUpscayl(false);
window.electron.send<BatchUpscaylPayload>(
ELECTRON_COMMANDS.FOLDER_UPSCAYL,
{
batchFolderPath,
outputPath,
model,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
noImageProcessing,
compression: compression.toString(),
customWidth: customWidth > 0 ? customWidth.toString() : null,
useCustomWidth,
tileSize,
},
);
logit("🏁 FOLDER_UPSCAYL");
} else {
// Single Image Upscayl
window.electron.send<ImageUpscaylPayload>(ELECTRON_COMMANDS.UPSCAYL, {
imagePath,
outputPath,
model,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
overwrite,
noImageProcessing,
compression: compression.toString(),
customWidth: customWidth > 0 ? customWidth.toString() : null,
useCustomWidth,
tileSize,
});
logit("🏁 UPSCAYL");
}
} else {
toast({
title: t("ERRORS.NO_IMAGE_ERROR.TITLE"),
description: t("ERRORS.NO_IMAGE_ERROR.DESCRIPTION"),
});
logit("🚫 No valid image selected");
}
};
return (
<>
{/* TOP LOGO WHEN SIDEBAR IS HIDDEN */}
{!showSidebar && <UpscaylLogo />}
<SidebarToggleButton
showSidebar={showSidebar}
setShowSidebar={setShowSidebar}
/>
<div
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"
onClick={() => setShowSidebar((prev) => !prev)}
>
<ChevronLeftIcon />
</button>
{window.electron.platform === "mac" && (
<div className="mac-titlebar pt-8"></div>
)}
<Header version={version} />
<NewsModal />
<Tabs selectedTab={selectedTab} setSelectedTab={setSelectedTab} />
{selectedTab === 0 && (
<UpscaylSteps
selectImageHandler={selectImageHandler}
selectFolderHandler={selectFolderHandler}
handleModelChange={handleModelChange}
upscaylHandler={upscaylHandler}
batchMode={batchMode}
setBatchMode={setBatchMode}
imagePath={imagePath}
doubleUpscayl={doubleUpscayl}
setDoubleUpscayl={setDoubleUpscayl}
dimensions={dimensions}
setGpuId={setGpuId}
model={model}
setModel={setModel}
setSaveImageAs={setSaveImageAs}
/>
)}
{selectedTab === 1 && (
<SettingsTab
batchMode={batchMode}
setModel={setModel}
compression={compression}
setCompression={setCompression}
gpuId={gpuId}
setGpuId={setGpuId}
saveImageAs={saveImageAs}
setSaveImageAs={setSaveImageAs}
logData={logData}
show={showCloudModal}
setShow={setShowCloudModal}
setDontShowCloudModal={setDontShowCloudModal}
/>
)}
<Footer />
</div>
</>
);
};
export default Sidebar;

View File

@ -1,26 +1,26 @@
import { ThemeSelect } from "./ThemeSelect";
import { SaveOutputFolderToggle } from "./SaveOutputFolderToggle";
import { GpuIdInput } from "./GpuIdInput";
import { CustomModelsFolderSelect } from "./CustomModelsFolderSelect";
import { LogArea } from "./LogArea";
import { ImageScaleSelect } from "./ImageScaleSelect";
import { ImageFormatSelect } from "./ImageFormatSelect";
import { DonateButton } from "./DonateButton";
import { SelectTheme } from "./select-theme";
import { SaveOutputFolderToggle } from "./save-output-folder-toggle";
import { InputGpuId } from "./input-gpu-id";
import { CustomModelsFolderSelect } from "./select-custom-models-folder";
import { LogArea } from "./log-area";
import { SelectImageScale } from "./select-image-scale";
import { SelectImageFormat } from "./select-image-format";
import { DonateButton } from "./donate-button";
import React, { useEffect, useState } from "react";
import { themeChange } from "theme-change";
import { useAtom, useAtomValue } from "jotai";
import { customModelsPathAtom, scaleAtom } from "../../atoms/userSettingsAtom";
import { modelsListAtom } from "../../atoms/modelsListAtom";
import useLog from "../hooks/useLog";
import { CompressionInput } from "./CompressionInput";
import OverwriteToggle from "./OverwriteToggle";
import { UpscaylCloudModal } from "../UpscaylCloudModal";
import { ResetSettings } from "./ResetSettings";
import { featureFlags } from "@common/feature-flags";
import TurnOffNotificationsToggle from "./TurnOffNotificationsToggle";
import { customModelsPathAtom, scaleAtom } from "@/atoms/user-settings-atom";
import { modelsListAtom } from "@/atoms/models-list-atom";
import useLogger from "@/components/hooks/use-logger";
import { InputCompression } from "./input-compression";
import OverwriteToggle from "./overwrite-toggle";
import { UpscaylCloudModal } from "@/components/upscayl-cloud-modal";
import { ResetSettingsButton } from "./reset-settings-button";
import { FEATURE_FLAGS } from "@common/feature-flags";
import TurnOffNotificationsToggle from "./turn-off-notifications-toggle";
import { cn } from "@/lib/utils";
import { CustomResolutionInput } from "./CustomResolutionInput";
import { TileSizeInput } from "./TileSizeInput";
import { InputCustomResolution } from "./input-custom-resolution";
import { InputTileSize } from "./input-tile-size";
import LanguageSwitcher from "./language-switcher";
import { translationAtom } from "@/atoms/translations-atom";
@ -34,7 +34,6 @@ interface IProps {
gpuId: string;
setGpuId: React.Dispatch<React.SetStateAction<string>>;
logData: string[];
os: "linux" | "mac" | "win" | undefined;
show: boolean;
setShow: React.Dispatch<React.SetStateAction<boolean>>;
setDontShowCloudModal: React.Dispatch<React.SetStateAction<boolean>>;
@ -50,19 +49,11 @@ function SettingsTab({
saveImageAs,
setSaveImageAs,
logData,
os,
show,
setShow,
setDontShowCloudModal,
}: IProps) {
// STATES
const [currentModel, setCurrentModel] = useState<{
label: string;
value: string;
}>({
label: null,
value: null,
});
const [isCopied, setIsCopied] = useState(false);
const [customModelsPath, setCustomModelsPath] = useAtom(customModelsPathAtom);
@ -72,7 +63,7 @@ function SettingsTab({
const [timeoutId, setTimeoutId] = useState(null);
const t = useAtomValue(translationAtom);
const { logit } = useLog();
const logit = useLogger();
useEffect(() => {
themeChange(false);
@ -90,7 +81,6 @@ function SettingsTab({
}
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);
@ -107,7 +97,6 @@ function SettingsTab({
logit("🔀 Setting model to", modelOptions[0].value);
currentlySavedModel = modelOptions[0];
}
setCurrentModel(currentlySavedModel);
setModel(currentlySavedModel.value);
logit(
"⚙️ Getting model from localStorage: ",
@ -194,7 +183,7 @@ function SettingsTab({
>
{t("SETTINGS.SUPPORT.DOCS_BUTTON_TITLE")}
</a>
{featureFlags.APP_STORE_BUILD && (
{FEATURE_FLAGS.APP_STORE_BUILD && (
<a
className="btn btn-primary"
href={`mailto:upscayl@gmail.com?subject=Upscayl%20Issue%3A%20%3CIssue%20name%20here%3E&body=Device%20Name%3A%20%3CYOUR%20DEVICE%20MODEL%3E%0AOperating%20System%3A%20%3CYOUR%20OPERATING%20SYSTEM%20VERSION%3E%0AUpscayl%20Version%3A%20${upscaylVersion}%0A%0AHi%2C%20I'm%20having%20an%20issue%20with%20Upscayl.%20%3CDESCRIBE%20ISSUE%20HERE%3E`}
@ -203,7 +192,7 @@ function SettingsTab({
{t("SETTINGS.SUPPORT.EMAIL_BUTTON_TITLE")}
</a>
)}
{!featureFlags.APP_STORE_BUILD && <DonateButton />}
{!FEATURE_FLAGS.APP_STORE_BUILD && <DonateButton />}
</div>
<LogArea
@ -213,23 +202,23 @@ function SettingsTab({
/>
{/* THEME SELECTOR */}
<ThemeSelect />
<SelectTheme />
<LanguageSwitcher />
{/* IMAGE FORMAT BUTTONS */}
<ImageFormatSelect
<SelectImageFormat
batchMode={batchMode}
saveImageAs={saveImageAs}
setExportType={setExportType}
/>
{/* IMAGE SCALE */}
<ImageScaleSelect scale={scale} setScale={setScale} />
<SelectImageScale scale={scale} setScale={setScale} />
<CustomResolutionInput />
<InputCustomResolution />
<CompressionInput
<InputCompression
compression={compression}
handleCompressionChange={handleCompressionChange}
/>
@ -240,9 +229,9 @@ function SettingsTab({
<TurnOffNotificationsToggle />
{/* GPU ID INPUT */}
<GpuIdInput gpuId={gpuId} handleGpuIdChange={handleGpuIdChange} />
<InputGpuId gpuId={gpuId} handleGpuIdChange={handleGpuIdChange} />
<TileSizeInput />
<InputTileSize />
{/* CUSTOM MODEL */}
<CustomModelsFolderSelect
@ -251,9 +240,9 @@ function SettingsTab({
/>
{/* RESET SETTINGS */}
<ResetSettings />
<ResetSettingsButton />
{featureFlags.SHOW_UPSCAYL_CLOUD_INFO && (
{FEATURE_FLAGS.SHOW_UPSCAYL_CLOUD_INFO && (
<>
<button
className="mx-5 mb-5 animate-pulse rounded-btn bg-success p-1 text-sm text-slate-50 shadow-lg shadow-success/40"

View File

@ -6,7 +6,7 @@ type CompressionInputProps = {
handleCompressionChange: (arg: any) => void;
};
export function CompressionInput({
export function InputCompression({
compression,
handleCompressionChange,
}: CompressionInputProps) {

View File

@ -1,9 +1,12 @@
import { customWidthAtom, useCustomWidthAtom } from "@/atoms/userSettingsAtom";
import {
customWidthAtom,
useCustomWidthAtom,
} from "@/atoms/user-settings-atom";
import { useAtom, useAtomValue } from "jotai";
import React from "react";
import { translationAtom } from "@/atoms/translations-atom";
export function CustomResolutionInput() {
export function InputCustomResolution() {
const [useCustomWidth, setUseCustomWidth] = useAtom(useCustomWidthAtom);
const [customWidth, setCustomWidth] = useAtom(customWidthAtom);
const t = useAtomValue(translationAtom);

View File

@ -7,7 +7,7 @@ type GpuIdInputProps = {
handleGpuIdChange: (arg: string) => void;
};
export function GpuIdInput({ gpuId, handleGpuIdChange }) {
export function InputGpuId({ gpuId, handleGpuIdChange }) {
const t = useAtomValue(translationAtom);
return (

View File

@ -1,9 +1,9 @@
import { translationAtom } from "@/atoms/translations-atom";
import { tileSizeAtom } from "@/atoms/userSettingsAtom";
import { tileSizeAtom } from "@/atoms/user-settings-atom";
import { useAtom, useAtomValue } from "jotai";
import React from "react";
export function TileSizeInput() {
export function InputTileSize() {
const [tileSize, setTileSize] = useAtom(tileSizeAtom);
const t = useAtomValue(translationAtom);

View File

@ -1,5 +1,5 @@
import { translationAtom } from "@/atoms/translations-atom";
import { overwriteAtom } from "@/atoms/userSettingsAtom";
import { overwriteAtom } from "@/atoms/user-settings-atom";
import { useAtom, useAtomValue } from "jotai";
import React, { useEffect } from "react";

View File

@ -2,7 +2,7 @@ import { translationAtom } from "@/atoms/translations-atom";
import { useAtomValue } from "jotai";
import React from "react";
export function ResetSettings() {
export function ResetSettingsButton() {
const t = useAtomValue(translationAtom);
return (
<div className="flex flex-col items-start gap-2">

View File

@ -2,7 +2,7 @@ import { translationAtom } from "@/atoms/translations-atom";
import {
savedOutputPathAtom,
rememberOutputFolderAtom,
} from "@/atoms/userSettingsAtom";
} from "@/atoms/user-settings-atom";
import { useAtom, useAtomValue } from "jotai";
export function SaveOutputFolderToggle() {

View File

@ -1,5 +1,5 @@
import React from "react";
import commands from "../../../common/commands";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { useAtomValue } from "jotai";
import { translationAtom } from "@/atoms/translations-atom";
@ -32,12 +32,15 @@ export function CustomModelsFolderSelect({
className="btn btn-primary"
onClick={async () => {
const customModelPath = await window.electron.invoke(
commands.SELECT_CUSTOM_MODEL_FOLDER,
ELECTRON_COMMANDS.SELECT_CUSTOM_MODEL_FOLDER,
);
if (customModelPath !== null) {
setCustomModelsPath(customModelPath);
window.electron.send(commands.GET_MODELS_LIST, customModelPath);
window.electron.send(
ELECTRON_COMMANDS.GET_MODELS_LIST,
customModelPath,
);
} else {
setCustomModelsPath("");
}

View File

@ -7,7 +7,7 @@ type ImageFormatSelectProps = {
setExportType: (arg: string) => void;
};
export function ImageFormatSelect({
export function SelectImageFormat({
batchMode,
saveImageAs,
setExportType,

View File

@ -1,5 +1,5 @@
import { translationAtom } from "@/atoms/translations-atom";
import { useCustomWidthAtom } from "@/atoms/userSettingsAtom";
import { useCustomWidthAtom } from "@/atoms/user-settings-atom";
import { useAtomValue } from "jotai";
type ImageScaleSelectProps = {
@ -8,7 +8,7 @@ type ImageScaleSelectProps = {
hideInfo?: boolean;
};
export function ImageScaleSelect({
export function SelectImageScale({
scale,
setScale,
hideInfo,

View File

@ -1,7 +1,8 @@
import { translationAtom } from "@/atoms/translations-atom";
import { useAtomValue } from "jotai";
import React from "react";
export function ThemeSelect() {
export function SelectTheme() {
const availableThemes = [
{ label: "upscayl", value: "upscayl" },
{ label: "light", value: "light" },

View File

@ -1,5 +1,5 @@
import { translationAtom } from "@/atoms/translations-atom";
import { turnOffNotificationsAtom } from "@/atoms/userSettingsAtom";
import { turnOffNotificationsAtom } from "@/atoms/user-settings-atom";
import { useAtom, useAtomValue } from "jotai";
const TurnOffNotificationsToggle = () => {

View File

@ -0,0 +1,25 @@
import { cn } from "@/lib/utils";
import { ChevronRightIcon } from "lucide-react";
import React from "react";
const SidebarToggleButton = ({
showSidebar,
setShowSidebar,
}: {
showSidebar: boolean;
setShowSidebar: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
return (
<button
className={cn(
"fixed left-0 top-1/2 z-[999] -translate-y-1/2 rounded-r-full bg-base-100 p-4 ",
showSidebar ? "hidden" : "",
)}
onClick={() => setShowSidebar((prev) => !prev)}
>
<ChevronRightIcon />
</button>
);
};
export default SidebarToggleButton;

View File

@ -0,0 +1,15 @@
import UpscaylSVGLogo from "../icons/upscayl-logo-svg";
import useTranslation from "../hooks/use-translation";
const UpscaylLogo = () => {
const t = useTranslation();
return (
<div className="fixed right-2 top-2 z-50 flex items-center justify-center gap-2 rounded-[7px] bg-base-300 px-2 py-1 font-medium text-base-content ">
<UpscaylSVGLogo className="w-5" />
{t("TITLE")}
</div>
);
};
export default UpscaylLogo;

View File

@ -1,26 +1,24 @@
import { useAtom, useAtomValue } from "jotai";
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Tooltip } from "react-tooltip";
import { themeChange } from "theme-change";
import { TModelsList, modelsListAtom } from "../../../atoms/modelsListAtom";
import useLog from "../../hooks/useLog";
import { TModelsList, modelsListAtom } from "../../../atoms/models-list-atom";
import useLogger from "../../hooks/use-logger";
import {
noImageProcessingAtom,
savedOutputPathAtom,
progressAtom,
rememberOutputFolderAtom,
scaleAtom,
customWidthAtom,
useCustomWidthAtom,
} from "../../../atoms/userSettingsAtom";
import { featureFlags } from "@common/feature-flags";
import getModelScale from "@common/check-model-scale";
import COMMAND from "@common/commands";
} 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 { ImageScaleSelect } from "@/components/settings-tab/ImageScaleSelect";
import { translationAtom } from "@/atoms/translations-atom";
import { SelectImageScale } from "../settings-tab/select-image-scale";
interface IProps {
selectImageHandler: () => Promise<void>;
@ -42,7 +40,7 @@ interface IProps {
setGpuId: React.Dispatch<React.SetStateAction<string>>;
}
function LeftPaneImageSteps({
function UpscaylSteps({
selectImageHandler,
selectFolderHandler,
handleModelChange,
@ -65,23 +63,19 @@ function LeftPaneImageSteps({
const modelOptions = useAtomValue(modelsListAtom);
const [scale, setScale] = useAtom(scaleAtom);
const noImageProcessing = useAtomValue(noImageProcessingAtom);
const [outputPath, setOutputPath] = useAtom(savedOutputPathAtom);
const [progress, setProgress] = useAtom(progressAtom);
const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const [open, setOpen] = React.useState(false);
const [customWidth, setCustomWidth] = useAtom(customWidthAtom);
const [useCustomWidth, setUseCustomWidth] = useAtom(useCustomWidthAtom);
const customWidth = useAtomValue(customWidthAtom);
const useCustomWidth = useAtomValue(useCustomWidthAtom);
const [targetWidth, setTargetWidth] = useState<number>(null);
const [targetHeight, setTargetHeight] = useState<number>(null);
const { logit } = useLog();
const logit = useLogger();
const { toast } = useToast();
const t = useAtomValue(translationAtom);
const outputHandler = async () => {
var path = await window.electron.invoke(COMMAND.SELECT_FOLDER);
const path = await window.electron.invoke(ELECTRON_COMMANDS.SELECT_FOLDER);
if (path !== null) {
logit("🗂 Setting Output Path: ", path);
setOutputPath(path);
@ -136,12 +130,7 @@ function LeftPaneImageSteps({
logit("🔀 Setting model to", currentModel.value);
}, [currentModel]);
useEffect(() => {
setTargetWidth(getUpscaleResolution().width);
setTargetHeight(getUpscaleResolution().height);
}, [dimensions.width, dimensions.height, doubleUpscayl, scale]);
const getUpscaleResolution = useCallback(() => {
const upscaylResolution = useMemo(() => {
const newDimensions = {
width: dimensions.width,
height: dimensions.height,
@ -278,7 +267,7 @@ function LeftPaneImageSteps({
</div>
)}
<ImageScaleSelect scale={scale} setScale={setScale} hideInfo />
<SelectImageScale scale={scale} setScale={setScale} hideInfo />
</div>
{/* STEP 3 */}
@ -288,7 +277,7 @@ function LeftPaneImageSteps({
<span className="leading-none">
{t("APP.OUTPUT_PATH_SELECTION.TITLE")}
</span>
{featureFlags.APP_STORE_BUILD && (
{FEATURE_FLAGS.APP_STORE_BUILD && (
<button
className="badge badge-outline badge-sm cursor-pointer"
onClick={() =>
@ -299,7 +288,7 @@ function LeftPaneImageSteps({
</button>
)}
</div>
{!outputPath && featureFlags.APP_STORE_BUILD && (
{!outputPath && FEATURE_FLAGS.APP_STORE_BUILD && (
<div className="text-xs">
<span className="rounded-btn bg-base-200 px-2 font-medium uppercase text-base-content/50">
{t("APP.OUTPUT_PATH_SELECTION.NOT_SELECTED")}
@ -307,7 +296,7 @@ function LeftPaneImageSteps({
</div>
)}
</div>
{!batchMode && !featureFlags.APP_STORE_BUILD && (
{!batchMode && !FEATURE_FLAGS.APP_STORE_BUILD && (
<p className="mb-2 text-sm">
{!batchMode
? t("APP.OUTPUT_PATH_SELECTION.DEFAULT_IMG_PATH")
@ -335,7 +324,7 @@ function LeftPaneImageSteps({
</span>
{t("APP.SCALE_SELECTION.TO_TITLE")}
<span className="font-bold">
{getUpscaleResolution().width}x{getUpscaleResolution().height}
{upscaylResolution.width}x{upscaylResolution.height}
</span>
</p>
)}
@ -366,4 +355,4 @@ function LeftPaneImageSteps({
);
}
export default LeftPaneImageSteps;
export default UpscaylSteps;

View File

@ -0,0 +1 @@
export const VALID_IMAGE_FORMATS = ["png", "jpg", "jpeg", "jfif", "webp"];

View File

@ -12,7 +12,7 @@
"LINK_TITLE": "The Upscayl Team"
},
"SETTINGS": {
"TITLE": "SETTINGS",
"TITLE": "Settings",
"CHANGE_LANGUAGE": { "TITLE": "Change Language" },
"IMAGE_COMPRESSION": {
"TITLE": "Image Compression",
@ -127,7 +127,7 @@
"FROM_TITLE": "Upscayl from ",
"TO_TITLE": " to ",
"NO_OUTPUT_FOLDER_ALERT": "Please select an output folder first",
"START_BUTTON_TITLE": "Upscayl",
"START_BUTTON_TITLE": "Upscayl 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Upscayling ⏳"
},
"IMAGE_OPTIONS": {

View File

@ -12,7 +12,7 @@
"LINK_TITLE": "El equipo de Upscayl"
},
"SETTINGS": {
"TITLE": "AJUSTES",
"TITLE": "Ajustes",
"CHANGE_LANGUAGE": { "TITLE": "Cambiar idioma" },
"IMAGE_COMPRESSION": {
"TITLE": "Compresión de imagen",
@ -127,7 +127,7 @@
"FROM_TITLE": "Aumentar desde ",
"TO_TITLE": " a ",
"NO_OUTPUT_FOLDER_ALERT": "Por favor, selecciona primero una carpeta de salida",
"START_BUTTON_TITLE": "Upscayl",
"START_BUTTON_TITLE": "Upscayl 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Aumentando ⏳"
},
"IMAGE_OPTIONS": {

View File

@ -12,7 +12,7 @@
"LINK_TITLE": "L'équipe Upscayl"
},
"SETTINGS": {
"TITLE": "PARAMÈTRES",
"TITLE": "Paramètres",
"CHANGE_LANGUAGE": { "TITLE": "Changer de langue" },
"IMAGE_COMPRESSION": {
"TITLE": "Compression d'image",
@ -127,7 +127,7 @@
"FROM_TITLE": "Suréchantillonner de ",
"TO_TITLE": " à ",
"NO_OUTPUT_FOLDER_ALERT": "Veuillez d'abord sélectionner un dossier de sortie",
"START_BUTTON_TITLE": "Suréchantillonner",
"START_BUTTON_TITLE": "Suréchantillonner 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Suréchantillonnage ⏳"
},
"IMAGE_OPTIONS": {

View File

@ -127,7 +127,7 @@
"FROM_TITLE": "Upscayl元 ",
"TO_TITLE": " から ",
"NO_OUTPUT_FOLDER_ALERT": "まず出力フォルダを選択してください",
"START_BUTTON_TITLE": "Upscayl",
"START_BUTTON_TITLE": "Upscayl 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Upscayl中 ⏳"
},
"IMAGE_OPTIONS": {

View File

@ -1,215 +0,0 @@
// !!!!!!!!!!!!!!!!!!!!DO NOT DELETE THIS FILE!!!!!!!!!!!!!!
// Copy this to a new file
// Name the file {language}.json like en-US.json, ru-RU.json, etc
// Replace the english strings present below with relevant languages
// !!!!!!!!!!!!!!!!!!!!!KEEP ANYTHING PRESENT WITHIN FLOWER BRACES {variable} - THEY ARE VARIABLES!!!!!!!!!!!!!!!!!!!!!!!
// Delete these comments starting with "//" as json format does not accept comments
{
"TITLE": "Upscayl",
"INTRO": "Introducing Upscayl Cloud!",
"HEADER": {
"GITHUB_BUTTON_TITLE": "Star us on GitHub 😁",
"DESCRIPTION": "AI Image Upscaler"
},
"FOOTER": {
"NEWS_TITLE": "UPSCAYL NEWS",
"COPYRIGHT": "Copyright ©",
"TITLE": "By ",
"LINK_TITLE": "The Upscayl Team"
},
"SETTINGS": {
"TITLE": "SETTINGS",
"CHANGE_LANGUAGE": { "TITLE": "Change Language" },
"IMAGE_COMPRESSION": {
"TITLE": "Image Compression",
"DESCRIPTION": "PNG compression is lossless, so it might not reduce the file size significantly and higher compression values might affect the performance. JPG and WebP compression is lossy."
},
"CUSTOM_MODELS": {
"TITLE": "ADD CUSTOM MODELS",
"BUTTON_FOLDER": "Select Folder",
"DESCRIPTION": "You can add your own models easily. For more details:",
"LINK_TITLE": "Custom Models Repository"
},
"CUSTOM_INPUT_RESOLUTION": {
"TITLE": "CUSTOM OUTPUT WIDTH",
"RESTART": "REQUIRES RESTART",
"DESCRIPTION": "Use a custom width for the output images. The height will be adjusted automatically. Enabling this will override the scale setting."
},
"DONATE": {
"DESCRIPTION": "If you like what we do :)",
"BUTTON_TITLE": "💎 DONATE"
},
"GPU_ID_INPUT": {
"TITLE": "GPU ID",
"DESCRIPTION": "Please read the Upscayl Documentation for more information.",
"ADDITIONAL_DESCRIPTION": "Enable performance mode on Windows for better results."
},
"IMAGE_FORMAT": {
"TITLE": "SAVE IMAGE AS",
"PNG": "PNG",
"JPG": "JPG",
"WEBP": "WEBP"
},
"IMAGE_SCALE": {
"TITLE": "Image Scale",
"DESCRIPTION": "Anything above 4X (except 16X Double Upscayl) only resizes the image and does not use AI upscaling.",
"WARNING": "Anything above 5X may cause performance issues on some devices!",
"ADDITIONAL_WARNING": "This may cause performance issues on some devices!"
},
"LOG_AREA": {
"ON_COPY": "COPIED ✅",
"BUTTON_TITLE": "COPY LOGS 📋",
"NO_LOGS": "No logs to show"
},
"OVERWRITE_TOGGLE": {
"TITLE": "OVERWRITE PREVIOUS UPSCALE",
"DESCRIPTION": "If enabled, Upscayl will process the image again instead of loading it directly."
},
"RESET_SETTINGS": {
"BUTTON_TITLE": "RESET UPSCAYL",
"ALERT": "Upscayl has been reset. Please restart the app."
},
"SAVE_OUTPUT_FOLDER": {
"TITLE": "SAVE OUTPUT FOLDER",
"DESCRIPTION": "If enabled, the output folder will be remembered between sessions."
},
"THEME": {
"TITLE": "UPSCAYL THEME"
},
"LANGUAGE": {
"TITLE": "UPSCAYL LANGUAGE"
},
"CUSTOM_TILE_SIZE": {
"TITLE": "CUSTOM TILE SIZE",
"DESCRIPTION": "Use a custom tile size for segmenting the image. This can help process images faster by reducing the number of tiles generated."
},
"TURN_OFF_NOTIFICATIONS": {
"TITLE": "TURN OFF NOTIFICATIONS",
"DESCRIPTION": "If enabled, Upscayl will not send any system notifications on success or failure."
},
"SUPPORT": {
"TITLE": "Having issues?",
"DOCS_BUTTON_TITLE": "🙏 GET HELP",
"EMAIL_BUTTON_TITLE": "📧 EMAIL DEVELOPER"
}
},
"APP": {
"TITLE": "Upscayl",
"BATCH_MODE": {
"TITLE": "Batch Upscayl",
"DESCRIPTION": "This will let you Upscayl all files in a folder at once"
},
"FILE_SELECTION": {
"TITLE": "Step 1",
"BATCH_MODE_TYPE": "Select Folder",
"SINGLE_MODE_TYPE": "Select Image"
},
"MODEL_SELECTION": {
"TITLE": "Step 2",
"DESCRIPTION": "Select Model"
},
"DOUBLE_UPSCAYL": {
"TITLE": "Double Upscayl",
"DESCRIPTION": "Enable this option to run upscayl twice on an image. Note that this may cause a significant increase in processing time and possibly performance issues for scales greater than 4X."
},
"OUTPUT_PATH_SELECTION": {
"TITLE": "Step 3",
"MAC_APP_STORE_ALERT": "Due to MacOS App Store security restrictions, Upscayl requires you to select an output folder everytime you start it.\n\nTo avoid this, you can permanently save a default output folder in the Upscayl 'Settings' tab.",
"NOT_SELECTED": "Not Selected",
"DEFAULT_IMG_PATH": "Defaults to Image's path",
"DEFAULT_FOLDER_PATH": "Defaults to Folder's path",
"BUTTON_TITLE": "Set Output Folder"
},
"SCALE_SELECTION": {
"TITLE": "Step 4",
"FROM_TITLE": "Upscayl from ",
"TO_TITLE": " to ",
"NO_OUTPUT_FOLDER_ALERT": "Please select an output folder first",
"START_BUTTON_TITLE": "Upscayl",
"IN_PROGRESS_BUTTON_TITLE": "Upscayling⏳"
},
"IMAGE_OPTIONS": {
"RESET_BUTTON_TITLE": "Reset Image",
"LENS_VIEW_TITLE": "Lens View",
"SLIDER_VIEW_TITLE": "Slider View",
"ZOOM_AMOUNT_TITLE": "Zoom Amount",
"LENS_SIZE_TITLE": "Lens Size"
},
"PROGRESS_BAR": {
"BATCH_UPSCAYL_IN_PROGRESS_TITLE": "Batch Upscayl In Progress:",
"IN_PROGRESS_TITLE": "Doing the Upscayl magic...",
"STOP_BUTTON_TITLE": "STOP"
},
"RESET_BUTTON_TITLE": "Reset",
"RIGHT_PANE_INFO": {
"SELECT_FOLDER": "Select a Folder to Upscayl",
"SELECT_IMAGE": "Select an Image to Upscayl",
"SELECT_FOLDER_DESCRIPTION": "Make sure that the folder doesn't contain anything except PNG, JPG, JPEG & WEBP images.",
"SELECT_IMAGES_DESCRIPTION": "Select or drag and drop a PNG, JPG, JPEG or WEBP image."
},
"PROGRESS": {
"PROCESSING_TITLE": "Processing the image...",
"SCALING_CONVERTING_TITLE": "Scaling and converting image...",
"WAIT_TITLE": "Hold on...",
"SUCCESS_TITLE": "Upscayl Successful!",
"BATCH": {
"SELECTED_FOLDER_TITLE": "Selected folder:",
"DONE_TITLE": "All done!",
"OPEN_UPSCAYLED_FOLDER_TITLE": "Open Upscayled Folder"
}
},
"SLIDER": {
"ORIGINAL_TITLE": "Original",
"UPSCAYLED_TITLE": "Upscayled"
},
"DIALOG_BOX": {
"CLOSE": "Close"
}
},
"ERRORS": {
"GPU_ERROR": {
"TITLE": "GPU Error",
"DESCRIPTION": "Ran into an issue with the GPU. Please read the docs for troubleshooting! ({data})"
},
"COPY_ERROR": {
"TITLE": "Copy Error",
"DESCRIPTION": ""
},
"READ_WRITE_ERROR": {
"TITLE": "Read/Write Error",
"DESCRIPTION": "Make sure that the path is correct and you have proper read/write permissions \n({data})"
},
"TILE_SIZE_ERROR": {
"TITLE": "Error",
"DESCRIPTION": "The tile size is wrong. Please change the tile size in the settings or set to 0 ({data})"
},
"EXCEPTION_ERROR": {
"TITLE": "Exception Error",
"DESCRIPTION": "Upscayl encountered an error. Possibly, the upscayl binary failed to execute the commands properly. Try checking the logs to see if you get any information. You can post an issue on Upscayl's GitHub repository for more help."
},
"GENERIC_ERROR": {
"TITLE": "Error"
},
"INVALID_IMAGE_ERROR": {
"TITLE": "Invalid Image",
"DESCRIPTION": "Please select an image with a valid extension like PNG, JPG, JPEG, JFIF or WEBP.",
"ADDITIONAL_DESCRIPTION": "Please drag and drop an image"
},
"NO_IMAGE_ERROR": {
"TITLE": "No image selected",
"DESCRIPTION": "Please select an image to upscale"
},
"OPEN_DOCS_TITLE": "Open Docs",
"OPEN_DOCS_BUTTON_TITLE": "Troubleshoot"
},
"UPSCAYL_CLOUD": {
"COMING_SOON": "Coming soon!",
"CATCHY_PHRASE_1": "No more errors, hardware issues, quality compromises or long loading times!",
"CATCHY_PHRASE_2": "🌐 Upscayl anywhere, anytime, any device\n☁ No Graphics Card or hardware required\n👩 Face Enhancement\n🦋 10+ models to choose from\n🏎 5x faster than Upscayl Desktop\n🎞 Video Upscaling\n💰 Commercial Usage\n😴 Upscayl while you sleep",
"ALREADY_REGISTERED_ALERT": "Thank you {name}! It seems that your email has already been registered :D If that's not the case, please try again.",
"ADD_SUCCESS": "Thank you for joining the waitlist! We will notify you when Upscayl Cloud is ready for you.",
"INCORRECT_FIELDS_ALERT": "Please fill in all the fields correctly.",
"JOIN_WAITLIST": "Join the waitlist",
"DONT_SHOW_AGAIN": "DON'T SHOW AGAIN"
}
}

View File

@ -12,7 +12,7 @@
"LINK_TITLE": "Команда Upscayl"
},
"SETTINGS": {
"TITLE": "НАСТРОЙКИ",
"TITLE": "Настройки",
"CHANGE_LANGUAGE": { "TITLE": "Сменить язык" },
"IMAGE_COMPRESSION": {
"TITLE": "Сжатие изображения",
@ -127,7 +127,7 @@
"FROM_TITLE": "Увеличить с ",
"TO_TITLE": " до ",
"NO_OUTPUT_FOLDER_ALERT": "Пожалуйста, сначала выберите папку вывода",
"START_BUTTON_TITLE": "Увеличить",
"START_BUTTON_TITLE": "Увеличить 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Увеличение ⏳"
},
"IMAGE_OPTIONS": {

View File

@ -127,7 +127,7 @@
"FROM_TITLE": "从 ",
"TO_TITLE": " 升图到 ",
"NO_OUTPUT_FOLDER_ALERT": "请先选择一个输出文件夹",
"START_BUTTON_TITLE": "升图!",
"START_BUTTON_TITLE": "升图!🚀",
"IN_PROGRESS_BUTTON_TITLE": "正在升图 ⏳"
},
"IMAGE_OPTIONS": {

View File

@ -1,143 +1,107 @@
"use client";
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import COMMAND from "../../common/commands";
import { ReactCompareSlider } from "react-compare-slider";
import Header from "../components/Header";
import Footer from "../components/Footer";
import ProgressBar from "../components/upscayl-tab/view/ProgressBar";
import RightPaneInfo from "../components/upscayl-tab/view/RightPaneInfo";
import ImageOptions from "../components/upscayl-tab/view/ImageOptions";
import LeftPaneImageSteps from "../components/upscayl-tab/config/LeftPaneImageSteps";
import Tabs from "../components/Tabs";
import SettingsTab from "../components/settings-tab";
import { useAtom, useAtomValue } from "jotai";
import { logAtom } from "../atoms/logAtom";
import { modelsListAtom } from "../atoms/modelsListAtom";
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 {
batchModeAtom,
lensSizeAtom,
compressionAtom,
dontShowCloudModalAtom,
noImageProcessingAtom,
savedOutputPathAtom,
overwriteAtom,
progressAtom,
scaleAtom,
viewTypeAtom,
rememberOutputFolderAtom,
showSidebarAtom,
customWidthAtom,
useCustomWidthAtom,
tileSizeAtom,
} from "../atoms/userSettingsAtom";
import useLog from "../components/hooks/useLog";
import { UpscaylCloudModal } from "../components/UpscaylCloudModal";
import { featureFlags } from "@common/feature-flags";
import {
BatchUpscaylPayload,
DoubleUpscaylPayload,
ImageUpscaylPayload,
} from "@common/types/types";
import { NewsModal } from "@/components/NewsModal";
import { newsAtom, showNewsModalAtom } from "@/atoms/newsAtom";
import matter from "gray-matter";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";
} 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 Logo from "@/components/icons/Logo";
import { sanitizePath } from "@common/sanitize-path";
import getDirectoryFromPath from "@common/get-directory-from-path";
import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
import { translationAtom } from "@/atoms/translations-atom";
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 { initCustomModels } from "@/components/hooks/use-custom-models";
const Home = () => {
const allowedFileTypes = ["png", "jpg", "jpeg", "jfif", "webp"];
const t = useAtomValue(translationAtom);
const logit = useLogger();
const { toast } = useToast();
initCustomModels();
const [isLoading, setIsLoading] = useState(true);
// LOCAL STATES
const [os, setOs] = useState<"linux" | "mac" | "win" | undefined>(undefined);
const [imagePath, setImagePath] = useState("");
const [upscaledImagePath, setUpscaledImagePath] = useState("");
const [model, setModel] = useState("realesrgan-x4plus");
const [version, setVersion] = useState("");
const [batchFolderPath, setBatchFolderPath] = useState("");
const [doubleUpscayl, setDoubleUpscayl] = useState(false);
const overwrite = useAtomValue(overwriteAtom);
const [upscaledBatchFolderPath, setUpscaledBatchFolderPath] = useState("");
const [doubleUpscaylCounter, setDoubleUpscaylCounter] = useState(0);
const [gpuId, setGpuId] = useState("");
const [saveImageAs, setSaveImageAs] = useState("png");
const [zoomAmount, setZoomAmount] = useState("100");
const [backgroundPosition, setBackgroundPosition] = useState("0% 0%");
const [dimensions, setDimensions] = useState({
width: null,
height: null,
});
const [selectedTab, setSelectedTab] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [showCloudModal, setShowCloudModal] = useState(false);
const [minSize, setMinSize] = useState(22);
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
const upscaledImageRef = useRef<HTMLImageElement>(null);
const [lensPosition, setLensPosition] = useState({ x: 0, y: 0 });
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);
// ATOMIC STATES
const [outputPath, setOutputPath] = useAtom(savedOutputPathAtom);
const [compression, setCompression] = useAtom(compressionAtom);
const [progress, setProgress] = useAtom(progressAtom);
const [batchMode, setBatchMode] = useAtom(batchModeAtom);
const [logData, setLogData] = useAtom(logAtom);
const [modelOptions, setModelOptions] = useAtom(modelsListAtom);
const [scale] = useAtom(scaleAtom);
const [dontShowCloudModal, setDontShowCloudModal] = useAtom(
dontShowCloudModalAtom,
);
const noImageProcessing = useAtomValue(noImageProcessingAtom);
const [news, setNews] = useAtom(newsAtom);
const [showNewsModal, setShowNewsModal] = useAtom(showNewsModalAtom);
const viewType = useAtomValue(viewTypeAtom);
const lensSize = useAtomValue(lensSizeAtom);
const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const [showSidebar, setShowSidebar] = useAtom(showSidebarAtom);
const customWidth = useAtomValue(customWidthAtom);
const useCustomWidth = useAtomValue(useCustomWidthAtom);
const tileSize = useAtomValue(tileSizeAtom);
const { logit } = useLog();
const { toast } = useToast();
const selectImageHandler = async () => {
resetImagePaths();
const path = await window.electron.invoke(ELECTRON_COMMANDS.SELECT_FILE);
if (path === null) return;
logit("🖼 Selected Image Path: ", path);
setImagePath(path);
const dirname = getDirectoryFromPath(path);
logit("📁 Selected Image Directory: ", dirname);
if (!FEATURE_FLAGS.APP_STORE_BUILD) {
if (!rememberOutputFolder) {
setOutputPath(dirname);
}
}
validateImagePath(path);
};
const sanitizedImagePath = useMemo(
() => sanitizePath(imagePath),
[imagePath],
);
const sanitizedUpscaledImagePath = useMemo(
() => sanitizePath(upscaledImagePath),
[upscaledImagePath],
);
const handleMouseMoveCompare = (e: React.MouseEvent) => {
if (upscaledImageRef.current) {
const { left, top, width, height } =
upscaledImageRef.current.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
setLensPosition({
x: Math.max(0, Math.min(x - lensSize, width - lensSize * 2)),
y: Math.max(0, Math.min(y - lensSize / 2, height - lensSize)),
});
const selectFolderHandler = async () => {
resetImagePaths();
const path = await window.electron.invoke(ELECTRON_COMMANDS.SELECT_FOLDER);
if (path !== null) {
logit("🖼 Selected Folder Path: ", path);
setBatchFolderPath(path);
if (!rememberOutputFolder) {
setOutputPath(path);
}
} else {
logit("🚫 Folder selection cancelled");
setBatchFolderPath("");
if (!rememberOutputFolder) {
setOutputPath("");
}
}
};
// SET CONFIG VARIABLES ON FIRST RUN
useEffect(() => {
// UPSCAYL VERSION
const upscaylVersion = navigator?.userAgent?.match(
/Upscayl\/([\d\.]+\d+)/,
)[1];
setVersion(upscaylVersion);
}, []);
const validateImagePath = (path: string) => {
if (path.length > 0) {
logit("🖼 imagePath: ", path);
const extension = path.split(".").pop().toLowerCase();
logit("🔤 Extension: ", extension);
if (!VALID_IMAGE_FORMATS.includes(extension)) {
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.DESCRIPTION"),
});
resetImagePaths();
}
} else {
resetImagePaths();
}
};
// ELECTRON EVENT LISTENERS
useEffect(() => {
@ -203,25 +167,19 @@ const Home = () => {
resetImagePaths();
}
};
// OS
window.electron.on(
COMMAND.OS,
(_, data: "linux" | "mac" | "win" | undefined) => {
if (data) {
setOs(data);
}
},
);
// LOG
window.electron.on(COMMAND.LOG, (_, data: string) => {
window.electron.on(ELECTRON_COMMANDS.LOG, (_, data: string) => {
logit(`🎒 BACKEND REPORTED: `, data);
});
// SCALING AND CONVERTING
window.electron.on(COMMAND.SCALING_AND_CONVERTING, (_, data: string) => {
window.electron.on(
ELECTRON_COMMANDS.SCALING_AND_CONVERTING,
(_, data: string) => {
setProgress(t("APP.PROGRESS.PROCESSING_TITLE"));
});
},
);
// UPSCAYL ERROR
window.electron.on(COMMAND.UPSCAYL_ERROR, (_, data: string) => {
window.electron.on(ELECTRON_COMMANDS.UPSCAYL_ERROR, (_, data: string) => {
toast({
title: t("ERRORS.GENERIC_ERROR.TITLE"),
description: data,
@ -229,7 +187,9 @@ const Home = () => {
resetImagePaths();
});
// UPSCAYL PROGRESS
window.electron.on(COMMAND.UPSCAYL_PROGRESS, (_, data: string) => {
window.electron.on(
ELECTRON_COMMANDS.UPSCAYL_PROGRESS,
(_, data: string) => {
if (data.length > 0 && data.length < 10) {
setProgress(data);
} else if (data.includes("converting")) {
@ -239,9 +199,12 @@ const Home = () => {
}
handleErrors(data);
logit(`🚧 UPSCAYL_PROGRESS: `, data);
});
},
);
// FOLDER UPSCAYL PROGRESS
window.electron.on(COMMAND.FOLDER_UPSCAYL_PROGRESS, (_, data: string) => {
window.electron.on(
ELECTRON_COMMANDS.FOLDER_UPSCAYL_PROGRESS,
(_, data: string) => {
if (data.includes("Successful")) {
setProgress(t("APP.PROGRESS.SUCCESS_TITLE"));
}
@ -250,9 +213,12 @@ const Home = () => {
}
handleErrors(data);
logit(`🚧 FOLDER_UPSCAYL_PROGRESS: `, data);
});
},
);
// DOUBLE UPSCAYL PROGRESS
window.electron.on(COMMAND.DOUBLE_UPSCAYL_PROGRESS, (_, data: string) => {
window.electron.on(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_PROGRESS,
(_, data: string) => {
if (data.length > 0 && data.length < 10) {
if (data === "0.00%") {
setDoubleUpscaylCounter(doubleUpscaylCounter + 1);
@ -261,29 +227,38 @@ const Home = () => {
}
handleErrors(data);
logit(`🚧 DOUBLE_UPSCAYL_PROGRESS: `, data);
});
},
);
// UPSCAYL DONE
window.electron.on(COMMAND.UPSCAYL_DONE, (_, data: string) => {
window.electron.on(ELECTRON_COMMANDS.UPSCAYL_DONE, (_, data: string) => {
setProgress("");
setUpscaledImagePath(data);
logit("upscaledImagePath: ", data);
logit(`💯 UPSCAYL_DONE: `, data);
});
// FOLDER UPSCAYL DONE
window.electron.on(COMMAND.FOLDER_UPSCAYL_DONE, (_, data: string) => {
window.electron.on(
ELECTRON_COMMANDS.FOLDER_UPSCAYL_DONE,
(_, data: string) => {
setProgress("");
setUpscaledBatchFolderPath(data);
logit(`💯 FOLDER_UPSCAYL_DONE: `, data);
});
},
);
// DOUBLE UPSCAYL DONE
window.electron.on(COMMAND.DOUBLE_UPSCAYL_DONE, (_, data: string) => {
window.electron.on(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_DONE,
(_, data: string) => {
setProgress("");
setTimeout(() => setUpscaledImagePath(data), 500);
setDoubleUpscaylCounter(0);
logit(`💯 DOUBLE_UPSCAYL_DONE: `, data);
});
},
);
// CUSTOM FOLDER LISTENER
window.electron.on(COMMAND.CUSTOM_MODEL_FILES_LIST, (_, data: string[]) => {
window.electron.on(
ELECTRON_COMMANDS.CUSTOM_MODEL_FILES_LIST,
(_, data: string[]) => {
logit(`📜 CUSTOM_MODEL_FILES_LIST: `, data);
const newModelOptions = data.map((model) => {
return {
@ -292,68 +267,16 @@ const Home = () => {
};
});
// Add newModelsList to modelOptions and remove duplicates
const combinedModelOptions = [...modelOptions, ...newModelOptions];
const uniqueModelOptions = combinedModelOptions.filter(
// Check if any model in the array appears more than once
(model, index, array) =>
array.findIndex((t) => t.value === model.value) === index,
);
const modelMap = new Map();
[...modelOptions, ...newModelOptions].forEach((model) => {
modelMap.set(model.value, model);
});
const uniqueModelOptions = Array.from(modelMap.values());
setModelOptions(uniqueModelOptions);
});
}, []);
// FETCH CUSTOM MODELS FROM CUSTOM MODELS PATH
useEffect(() => {
const customModelsPath = JSON.parse(
localStorage.getItem("customModelsPath"),
},
);
if (customModelsPath !== null) {
window.electron.send(COMMAND.GET_MODELS_LIST, customModelsPath);
logit("🎯 GET_MODELS_LIST: ", customModelsPath);
}
}, []);
// FETCH NEWS
useEffect(() => {
// TODO: ADD AN ABOUT TAB
if (window && window.navigator.onLine === false) return;
try {
fetch("https://raw.githubusercontent.com/upscayl/upscayl/main/news.md", {
cache: "no-cache",
})
.then((res) => {
return res.text();
})
.then((result) => {
const newsData = result;
if (!newsData) {
console.log("📰 Could not fetch news data");
return;
}
const markdownData = matter(newsData);
if (!markdownData) return;
if (markdownData && markdownData.data.dontShow) {
return;
}
if (
markdownData &&
news &&
markdownData?.data?.version === news?.data?.version
) {
console.log("📰 News is up to date");
if (showNewsModal === false) {
setShowNewsModal(false);
}
} else if (markdownData) {
setNews(matter(newsData));
setShowNewsModal(true);
}
});
} catch (error) {
console.log("Could not fetch Upscayl News");
}
}, [news]);
// LOADING STATE
useEffect(() => {
setIsLoading(false);
@ -373,555 +296,36 @@ const Home = () => {
setUpscaledBatchFolderPath("");
};
// UTILS
// CHECK IF IMAGE IS VALID
const validateImagePath = (path: string) => {
if (path.length > 0) {
logit("🖼 imagePath: ", path);
const extension = path.toLocaleLowerCase().split(".").pop();
logit("🔤 Extension: ", extension);
if (!allowedFileTypes.includes(extension.toLowerCase())) {
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.DESCRIPTION"),
});
resetImagePaths();
}
} else {
resetImagePaths();
}
};
// HANDLERS
const handleMouseMove = useCallback((e: any) => {
const { left, top, width, height } = e.target.getBoundingClientRect();
const x = ((e.pageX - left) / width) * 100;
const y = ((e.pageY - top) / height) * 100;
setBackgroundPosition(`${x}% ${y}%`);
}, []);
const selectImageHandler = async () => {
resetImagePaths();
var path = await window.electron.invoke(COMMAND.SELECT_FILE);
if (path === null) return;
logit("🖼 Selected Image Path: ", path);
setImagePath(path);
var dirname = getDirectoryFromPath(path);
logit("📁 Selected Image Directory: ", dirname);
if (!featureFlags.APP_STORE_BUILD) {
if (!rememberOutputFolder) {
setOutputPath(dirname);
}
}
validateImagePath(path);
};
const selectFolderHandler = async () => {
resetImagePaths();
var path = await window.electron.invoke(COMMAND.SELECT_FOLDER);
if (path !== null) {
logit("🖼 Selected Folder Path: ", path);
setBatchFolderPath(path);
if (!rememberOutputFolder) {
setOutputPath(path);
}
} else {
logit("🚫 Folder selection cancelled");
setBatchFolderPath("");
if (!rememberOutputFolder) {
setOutputPath("");
}
}
};
const handleModelChange = (e: any) => {
setModel(e.value);
logit("🔀 Model changed: ", e.value);
localStorage.setItem(
"model",
JSON.stringify({ label: e.label, value: e.value }),
);
};
// DRAG AND DROP HANDLERS
const handleDragEnter = (e) => {
e.preventDefault();
console.log("drag enter");
};
const handleDragLeave = (e) => {
e.preventDefault();
console.log("drag leave");
};
const handleDragOver = (e) => {
e.preventDefault();
console.log("drag over");
};
const openFolderHandler = (e) => {
logit("📂 OPEN_FOLDER: ", upscaledBatchFolderPath);
window.electron.send(COMMAND.OPEN_FOLDER, upscaledBatchFolderPath);
};
const handleDrop = (e) => {
e.preventDefault();
resetImagePaths();
if (
e.dataTransfer.items.length === 0 ||
e.dataTransfer.files.length === 0
) {
logit("👎 No valid files dropped");
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
return;
}
const type = e.dataTransfer.items[0].type;
const filePath = e.dataTransfer.files[0].path;
const extension = e.dataTransfer.files[0].name.split(".").at(-1);
logit("⤵️ Dropped file: ", JSON.stringify({ type, filePath, extension }));
if (
!type.includes("image") ||
!allowedFileTypes.includes(extension.toLowerCase())
) {
logit("🚫 Invalid file dropped");
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
} else {
logit("🖼 Setting image path: ", filePath);
setImagePath(filePath);
var dirname = getDirectoryFromPath(filePath);
logit("🗂 Setting output path: ", dirname);
if (!featureFlags.APP_STORE_BUILD) {
if (!rememberOutputFolder) {
setOutputPath(dirname);
}
}
validateImagePath(filePath);
}
};
const handlePaste = (e) => {
resetImagePaths();
e.preventDefault();
const type = e.clipboardData.items[0].type;
const filePath = e.clipboardData.files[0].path;
const extension = e.clipboardData.files[0].name.split(".").at(-1);
logit("📋 Pasted file: ", JSON.stringify({ type, filePath, extension }));
if (
!type.includes("image") &&
!allowedFileTypes.includes(extension.toLowerCase())
) {
toast({
title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"),
});
} else {
setImagePath(filePath);
var dirname = getDirectoryFromPath(filePath);
logit("🗂 Setting output path: ", dirname);
if (!rememberOutputFolder) {
setOutputPath(dirname);
}
}
};
const upscaylHandler = async () => {
logit("🔄 Resetting Upscaled Image Path");
setUpscaledImagePath("");
setUpscaledBatchFolderPath("");
if (imagePath !== "" || batchFolderPath !== "") {
setProgress(t("APP.PROGRESS.WAIT_TITLE"));
// Double Upscayl
if (doubleUpscayl) {
window.electron.send<DoubleUpscaylPayload>(COMMAND.DOUBLE_UPSCAYL, {
imagePath,
outputPath,
model,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
noImageProcessing,
compression: compression.toString(),
customWidth: customWidth > 0 ? customWidth.toString() : null,
useCustomWidth,
tileSize,
});
logit("🏁 DOUBLE_UPSCAYL");
} else if (batchMode) {
// Batch Upscayl
setDoubleUpscayl(false);
window.electron.send<BatchUpscaylPayload>(COMMAND.FOLDER_UPSCAYL, {
batchFolderPath,
outputPath,
model,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
noImageProcessing,
compression: compression.toString(),
customWidth: customWidth > 0 ? customWidth.toString() : null,
useCustomWidth,
tileSize,
});
logit("🏁 FOLDER_UPSCAYL");
} else {
// Single Image Upscayl
window.electron.send<ImageUpscaylPayload>(COMMAND.UPSCAYL, {
imagePath,
outputPath,
model,
gpuId: gpuId.length === 0 ? null : gpuId,
saveImageAs,
scale,
overwrite,
noImageProcessing,
compression: compression.toString(),
customWidth: customWidth > 0 ? customWidth.toString() : null,
useCustomWidth,
tileSize,
});
logit("🏁 UPSCAYL");
}
} else {
toast({
title: t("ERRORS.NO_IMAGE_ERROR.TITLE"),
description: t("ERRORS.NO_IMAGE_ERROR.DESCRIPTION"),
});
logit("🚫 No valid image selected");
}
};
const stopHandler = () => {
window.electron.send(COMMAND.STOP);
logit("🛑 Stopping Upscayl");
resetImagePaths();
};
if (isLoading) {
return (
<Logo className="absolute left-1/2 top-1/2 w-36 -translate-x-1/2 -translate-y-1/2 animate-pulse" />
<UpscaylSVGLogo className="absolute left-1/2 top-1/2 w-36 -translate-x-1/2 -translate-y-1/2 animate-pulse" />
);
}
return (
<div className="flex h-screen w-screen flex-row overflow-hidden bg-base-300">
{/* TOP LOGO WHEN SIDEBAR IS HIDDEN */}
{!showSidebar && (
<div className="fixed right-2 top-2 z-50 flex items-center justify-center gap-2 rounded-[7px] bg-base-300 px-2 py-1 font-medium text-base-content ">
<Logo className="w-5" />
{t("TITLE")}
</div>
)}
{/* SIDEBAR BUTTON */}
<button
className={cn(
"fixed left-0 top-1/2 z-[999] -translate-y-1/2 rounded-r-full bg-base-100 p-4 ",
showSidebar ? "hidden" : "",
)}
onClick={() => setShowSidebar((prev) => !prev)}
>
<ChevronRightIcon />
</button>
{/* LEFT PANE */}
<div
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"
onClick={() => setShowSidebar((prev) => !prev)}
>
<ChevronLeftIcon />
</button>
{/* UPSCAYL CLOUD MODAL */}
{featureFlags.SHOW_UPSCAYL_CLOUD_INFO && (
<UpscaylCloudModal
show={showCloudModal}
setShow={setShowCloudModal}
setDontShowCloudModal={setDontShowCloudModal}
/>
)}
{/* MACOS TITLEBAR */}
{window.electron.platform === "mac" && (
<div className="mac-titlebar pt-8"></div>
)}
{/* HEADER */}
<Header version={version} />
{!dontShowCloudModal && featureFlags.SHOW_UPSCAYL_CLOUD_INFO && (
<button
className="mx-5 mb-5 animate-pulse rounded-btn bg-success p-1 text-sm text-slate-50 shadow-lg shadow-success/40"
onClick={() => {
setShowCloudModal(true);
}}
>
{t("INTRO")}
</button>
)}
{/* NEWS DIALOG */}
<NewsModal
show={showNewsModal}
setShow={(val: boolean) => {
setShowNewsModal(val);
setNews((prev) => ({ ...prev, seen: true }));
}}
news={news}
/>
<Tabs selectedTab={selectedTab} setSelectedTab={setSelectedTab} />
{selectedTab === 0 && (
<LeftPaneImageSteps
<Sidebar
imagePath={imagePath}
dimensions={dimensions}
setUpscaledImagePath={setUpscaledImagePath}
batchFolderPath={batchFolderPath}
setUpscaledBatchFolderPath={setUpscaledBatchFolderPath}
selectImageHandler={selectImageHandler}
selectFolderHandler={selectFolderHandler}
handleModelChange={handleModelChange}
upscaylHandler={upscaylHandler}
batchMode={batchMode}
setBatchMode={setBatchMode}
/>
<MainContent
imagePath={imagePath}
doubleUpscayl={doubleUpscayl}
setDoubleUpscayl={setDoubleUpscayl}
dimensions={dimensions}
setGpuId={setGpuId}
model={model}
setModel={setModel}
setSaveImageAs={setSaveImageAs}
/>
)}
{selectedTab === 1 && (
<SettingsTab
batchMode={batchMode}
setModel={setModel}
compression={compression}
setCompression={setCompression}
gpuId={gpuId}
setGpuId={setGpuId}
saveImageAs={saveImageAs}
setSaveImageAs={setSaveImageAs}
logData={logData}
os={os}
show={showCloudModal}
setShow={setShowCloudModal}
setDontShowCloudModal={setDontShowCloudModal}
/>
)}
{/* )} */}
<Footer />
</div>
{/* RIGHT PANE */}
<div
className="relative flex h-screen w-full flex-col items-center justify-center"
onDrop={(e) => handleDrop(e)}
onDragOver={(e) => handleDragOver(e)}
onDragEnter={(e) => handleDragEnter(e)}
onDragLeave={(e) => handleDragLeave(e)}
onDoubleClick={() => {
if (batchMode) {
selectFolderHandler();
} else {
selectImageHandler();
}
}}
onPaste={(e) => handlePaste(e)}
>
{window.electron.platform === "mac" && (
<div className="mac-titlebar absolute top-0 h-8 w-full"></div>
)}
{progress.length > 0 &&
upscaledImagePath.length === 0 &&
upscaledBatchFolderPath.length === 0 ? (
<ProgressBar
batchMode={batchMode}
progress={progress}
resetImagePaths={resetImagePaths}
upscaledBatchFolderPath={upscaledBatchFolderPath}
setImagePath={setImagePath}
validateImagePath={validateImagePath}
selectFolderHandler={selectFolderHandler}
selectImageHandler={selectImageHandler}
batchFolderPath={batchFolderPath}
upscaledImagePath={upscaledImagePath}
doubleUpscaylCounter={doubleUpscaylCounter}
stopHandler={stopHandler}
setDimensions={setDimensions}
/>
) : null}
{/* DEFAULT PANE INFO */}
{((!batchMode &&
imagePath.length === 0 &&
upscaledImagePath.length === 0) ||
(batchMode &&
batchFolderPath.length === 0 &&
upscaledBatchFolderPath.length === 0)) && (
<RightPaneInfo version={version} batchMode={batchMode} />
)}
{/* SHOW SELECTED IMAGE */}
{!batchMode &&
upscaledImagePath.length === 0 &&
imagePath.length > 0 && (
<>
<ImageOptions
zoomAmount={zoomAmount}
setZoomAmount={setZoomAmount}
resetImagePaths={resetImagePaths}
hideZoomOptions={true}
/>
<img
src={"file:///" + sanitizePath(imagePath)}
onLoad={(e: any) => {
setDimensions({
width: e.target.naturalWidth,
height: e.target.naturalHeight,
});
}}
draggable="false"
alt=""
className="h-full w-full bg-gradient-to-br from-base-300 to-base-100 object-contain"
/>
</>
)}
{/* BATCH UPSCALE SHOW SELECTED FOLDER */}
{batchMode &&
upscaledBatchFolderPath.length === 0 &&
batchFolderPath.length > 0 && (
<p className="select-none text-base-content">
<span className="font-bold">
{t("APP.PROGRESS.BATCH.SELECTED_FOLDER_TITLE")}
</span>{" "}
{batchFolderPath}
</p>
)}
{/* BATCH UPSCALE DONE INFO */}
{batchMode && upscaledBatchFolderPath.length > 0 && (
<div className="z-50 flex flex-col items-center">
<p className="select-none py-4 font-bold text-base-content">
{t("APP.PROGRESS.BATCH.DONE_TITLE")}
</p>
<button
className="bg-gradient-blue btn btn-primary rounded-btn p-3 font-medium text-white/90 transition-colors"
onClick={openFolderHandler}
>
{t("APP.PROGRESS.BATCH.OPEN_UPSCAYLED_FOLDER_TITLE")}
</button>
</div>
)}
<ImageOptions
zoomAmount={zoomAmount}
setZoomAmount={setZoomAmount}
resetImagePaths={resetImagePaths}
/>
{!batchMode &&
viewType === "lens" &&
upscaledImagePath &&
imagePath && (
<div
className="group relative h-full w-full overflow-hidden"
onMouseMove={handleMouseMoveCompare}
>
{/* UPSCALED IMAGE */}
<img
className="h-full w-full object-contain"
src={"file:///" + sanitizedUpscaledImagePath}
alt="Upscaled"
ref={upscaledImageRef}
/>
{/* LENS */}
<div
className="pointer-events-none absolute opacity-0 transition-opacity before:absolute before:left-1/2 before:h-full before:w-[2px] before:bg-white group-hover:opacity-100"
style={{
left: `${lensPosition.x}px`,
top: `${lensPosition.y}px`,
width: lensSize * 2,
height: lensSize,
border: "2px solid white",
boxShadow: "0 0 0 9999px rgba(0, 0, 0, 0.5)",
}}
>
<div className="flex h-full w-full">
<div className="h-full w-full overflow-hidden">
<img
src={"file:///" + sanitizedImagePath}
alt="Original"
className="h-full w-full"
style={{
objectFit: "contain",
objectPosition: `${-lensPosition.x}px ${-lensPosition.y}px`,
transform: `scale(${parseInt(zoomAmount) / 100})`,
transformOrigin: "top left",
}}
/>
</div>
<div className="h-full w-full overflow-hidden">
<img
src={"file:///" + sanitizedUpscaledImagePath}
alt="Upscaled"
className="h-full w-full"
style={{
objectFit: "contain",
objectPosition: `${-lensPosition.x}px ${-lensPosition.y}px`,
transform: `scale(${parseInt(zoomAmount) / 100})`,
transformOrigin: "top left",
}}
/>
</div>
</div>
<div className="absolute bottom-0 left-0 flex w-full items-center justify-around bg-black bg-opacity-50 p-1 px-2 text-center text-xs text-white backdrop-blur-sm">
<span>Original</span>
<span>Upscayl</span>
</div>
</div>
</div>
)}
{/* COMPARISON SLIDER */}
{!batchMode &&
viewType === "slider" &&
imagePath.length > 0 &&
upscaledImagePath.length > 0 && (
<>
<ReactCompareSlider
itemOne={
<>
<p className="absolute bottom-1 left-1 rounded-md bg-black p-1 text-sm font-medium text-white opacity-30">
{t("APP.SLIDER.ORIGINAL_TITLE")}
</p>
<img
/* USE REGEX TO GET THE FILENAME AND ENCODE IT INTO PROPER FORM IN ORDER TO AVOID ERRORS DUE TO SPECIAL CHARACTERS */
src={"file:///" + sanitizedImagePath}
alt={t("APP.SLIDER.ORIGINAL_TITLE")}
onMouseMove={handleMouseMove}
style={{
objectFit: "contain",
backgroundPosition: "0% 0%",
transformOrigin: backgroundPosition,
}}
className={`h-full w-full bg-gradient-to-br from-base-300 to-base-100 transition-transform group-hover:scale-[${zoomAmount}%]`}
/>
</>
}
itemTwo={
<>
<p className="absolute bottom-1 right-1 rounded-md bg-black p-1 text-sm font-medium text-white opacity-30">
{t("APP.SLIDER.UPSCAYLED_TITLE")}
</p>
<img
/* USE REGEX TO GET THE FILENAME AND ENCODE IT INTO PROPER FORM IN ORDER TO AVOID ERRORS DUE TO SPECIAL CHARACTERS */
src={"file:///" + sanitizedUpscaledImagePath}
alt={t("APP.SLIDER.UPSCAYLED_TITLE")}
style={{
objectFit: "contain",
backgroundPosition: "0% 0%",
transformOrigin: backgroundPosition,
}}
onMouseMove={handleMouseMove}
className={`h-full w-full bg-gradient-to-br from-base-300 to-base-100 transition-transform group-hover:scale-[${
zoomAmount || "100%"
}%]`}
/>
</>
}
className="group h-screen"
/>
</>
)}
</div>
</div>
);
};

View File

@ -2,6 +2,7 @@ import { IpcRenderer } from "electron";
export interface IElectronAPI {
on: (command, func?) => IpcRenderer;
off: (command, func?) => IpcRenderer;
send: <T>(command, func?: T) => IpcRenderer;
invoke: (command, func?) => any;
platform: "mac" | "win" | "linux";

View File

@ -11,7 +11,8 @@
"skipLibCheck": true,
"paths": {
"@electron/*": ["./electron/*"],
"@/*": ["./renderer/*"]
"@/*": ["./renderer/*"],
"@common/*": ["./common/*"]
}
},
"include": ["./electron/**/*", "./common/**/*", "./renderer/**/*"],