1
0
mirror of https://github.com/upscayl/upscayl.git synced 2024-11-27 17:00:52 +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_FILE: "Select a File",
SELECT_FOLDER: "Select a Folder", SELECT_FOLDER: "Select a Folder",
UPSCAYL: "Upscale the Image", UPSCAYL: "Upscale the Image",
@ -25,6 +25,6 @@ const COMMAND = {
OS: "Get OS", OS: "Get OS",
SCALING_AND_CONVERTING: "Adding some finishing touches", SCALING_AND_CONVERTING: "Adding some finishing touches",
UPSCAYL_ERROR: "Upscaling Error", 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; SHOW_UPSCAYL_CLOUD_INFO: boolean;
}; };
export const featureFlags: FeatureFlags = { export const FEATURE_FLAGS: FeatureFlags = {
APP_STORE_BUILD: false, APP_STORE_BUILD: false,
SHOW_UPSCAYL_CLOUD_INFO: 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 { getBatchArguments } from "../utils/get-arguments";
import slash from "../utils/slash"; import slash from "../utils/slash";
import { modelsPath } from "../utils/get-resource-paths"; 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 { BatchUpscaylPayload } from "../../common/types/types";
import showNotification from "../utils/show-notification"; import showNotification from "../utils/show-notification";
import { DEFAULT_MODELS } from "../../common/models-list"; import { DEFAULT_MODELS } from "../../common/models-list";
@ -72,28 +72,28 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => {
if (!mainWindow) return; if (!mainWindow) return;
data = data.toString(); data = data.toString();
mainWindow.webContents.send( mainWindow.webContents.send(
COMMAND.FOLDER_UPSCAYL_PROGRESS, ELECTRON_COMMANDS.FOLDER_UPSCAYL_PROGRESS,
data.toString(), data.toString(),
); );
if ((data as string).includes("Error")) { if ((data as string).includes("Error")) {
logit("❌ ", data); logit("❌ ", data);
encounteredError = true; encounteredError = true;
} else if (data.includes("Resizing")) { } else if (data.includes("Resizing")) {
mainWindow.webContents.send(COMMAND.SCALING_AND_CONVERTING); mainWindow.webContents.send(ELECTRON_COMMANDS.SCALING_AND_CONVERTING);
} }
}; };
const onError = (data: any) => { const onError = (data: any) => {
if (!mainWindow) return; if (!mainWindow) return;
mainWindow.setProgressBar(-1); mainWindow.setProgressBar(-1);
mainWindow.webContents.send( mainWindow.webContents.send(
COMMAND.FOLDER_UPSCAYL_PROGRESS, ELECTRON_COMMANDS.FOLDER_UPSCAYL_PROGRESS,
data.toString(), data.toString(),
); );
failed = true; failed = true;
upscayl.kill(); upscayl.kill();
mainWindow && mainWindow &&
mainWindow.webContents.send( mainWindow.webContents.send(
COMMAND.UPSCAYL_ERROR, ELECTRON_COMMANDS.UPSCAYL_ERROR,
`Error upscaling images! ${data}`, `Error upscaling images! ${data}`,
); );
return; return;
@ -104,7 +104,7 @@ const batchUpscayl = async (event, payload: BatchUpscaylPayload) => {
logit("💯 Done upscaling"); logit("💯 Done upscaling");
upscayl.kill(); upscayl.kill();
mainWindow.webContents.send( mainWindow.webContents.send(
COMMAND.FOLDER_UPSCAYL_DONE, ELECTRON_COMMANDS.FOLDER_UPSCAYL_DONE,
outputFolderPath, outputFolderPath,
); );
if (!encounteredError) { if (!encounteredError) {

View File

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

View File

@ -14,7 +14,7 @@ import {
} from "../utils/get-arguments"; } from "../utils/get-arguments";
import { modelsPath } from "../utils/get-resource-paths"; import { modelsPath } from "../utils/get-resource-paths";
import logit from "../utils/logit"; import logit from "../utils/logit";
import COMMAND from "../../common/commands"; import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { DoubleUpscaylPayload } from "../../common/types/types"; import { DoubleUpscaylPayload } from "../../common/types/types";
import { ImageFormat } from "../types/types"; import { ImageFormat } from "../types/types";
import showNotification from "../utils/show-notification"; import showNotification from "../utils/show-notification";
@ -87,12 +87,15 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
if (!mainWindow) return; if (!mainWindow) return;
data.toString(); data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER // 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 // SET FAILED TO TRUE
failed2 = true; failed2 = true;
mainWindow && mainWindow &&
mainWindow.webContents.send( mainWindow.webContents.send(
COMMAND.UPSCAYL_ERROR, ELECTRON_COMMANDS.UPSCAYL_ERROR,
"Error upscaling image. Error: " + data, "Error upscaling image. Error: " + data,
); );
showNotification("Upscayl Failure", "Failed to upscale image!"); showNotification("Upscayl Failure", "Failed to upscale image!");
@ -105,13 +108,16 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
// CONVERT DATA TO STRING // CONVERT DATA TO STRING
data = data.toString(); data = data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER // 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 PROGRESS HAS ERROR, UPSCAYL FAILED
if (data.includes("Error")) { if (data.includes("Error")) {
upscayl2.kill(); upscayl2.kill();
failed2 = true; failed2 = true;
} else if (data.includes("Resizing")) { } 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"); logit("💯 Done upscaling");
mainWindow.setProgressBar(-1); 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!"); showNotification("Upscayled", "Image upscayled successfully!");
} }
}; };
@ -132,12 +141,15 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
mainWindow.setProgressBar(-1); mainWindow.setProgressBar(-1);
data.toString(); data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER // 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 // SET FAILED TO TRUE
failed = true; failed = true;
mainWindow && mainWindow &&
mainWindow.webContents.send( mainWindow.webContents.send(
COMMAND.UPSCAYL_ERROR, ELECTRON_COMMANDS.UPSCAYL_ERROR,
"Error upscaling image. Error: " + data, "Error upscaling image. Error: " + data,
); );
showNotification("Upscayl Failure", "Failed to upscale image!"); showNotification("Upscayl Failure", "Failed to upscale image!");
@ -150,13 +162,16 @@ const doubleUpscayl = async (event, payload: DoubleUpscaylPayload) => {
// CONVERT DATA TO STRING // CONVERT DATA TO STRING
data = data.toString(); data = data.toString();
// SEND UPSCAYL PROGRESS TO RENDERER // 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 PROGRESS HAS ERROR, UPSCAYL FAILED
if (data.includes("Error") || data.includes("failed")) { if (data.includes("Error") || data.includes("failed")) {
upscayl.kill(); upscayl.kill();
failed = true; failed = true;
} else if (data.includes("Resizing")) { } 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 { getMainWindow } from "../main-window";
import { import {
savedCustomModelsPath, savedCustomModelsPath,
@ -17,7 +17,10 @@ const getModelsList = async (event, payload) => {
logit("📁 Custom Models Folder Path: ", savedCustomModelsPath); logit("📁 Custom Models Folder Path: ", savedCustomModelsPath);
const models = await getModels(payload); 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 fs from "fs";
import { modelsPath } from "../utils/get-resource-paths"; import { modelsPath } from "../utils/get-resource-paths";
import COMMAND from "../../common/commands"; import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { import {
savedCustomModelsPath, savedCustomModelsPath,
setChildProcesses, setChildProcesses,
@ -60,14 +60,17 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
// Check if windows can write the new filename to the file system // Check if windows can write the new filename to the file system
if (outFile.length >= 255) { if (outFile.length >= 255) {
logit("Filename too long for Windows."); 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 // UPSCALE
if (fs.existsSync(outFile) && !overwrite) { if (fs.existsSync(outFile) && !overwrite) {
// If already upscayled, just output that file // If already upscayled, just output that file
logit("✅ Already upscayled at: ", outFile); logit("✅ Already upscayled at: ", outFile);
mainWindow.webContents.send(COMMAND.UPSCAYL_DONE, outFile); mainWindow.webContents.send(ELECTRON_COMMANDS.UPSCAYL_DONE, outFile);
} else { } else {
logit( logit(
"✅ Upscayl Variables: ", "✅ Upscayl Variables: ",
@ -115,18 +118,24 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
logit(data.toString()); logit(data.toString());
mainWindow.setProgressBar(parseFloat(data.slice(0, data.length)) / 100); mainWindow.setProgressBar(parseFloat(data.slice(0, data.length)) / 100);
data = data.toString(); data = data.toString();
mainWindow.webContents.send(COMMAND.UPSCAYL_PROGRESS, data.toString()); mainWindow.webContents.send(
ELECTRON_COMMANDS.UPSCAYL_PROGRESS,
data.toString(),
);
if (data.includes("Error")) { if (data.includes("Error")) {
upscayl.kill(); upscayl.kill();
failed = true; failed = true;
} else if (data.includes("Resizing")) { } else if (data.includes("Resizing")) {
mainWindow.webContents.send(COMMAND.SCALING_AND_CONVERTING); mainWindow.webContents.send(ELECTRON_COMMANDS.SCALING_AND_CONVERTING);
} }
}; };
const onError = (data) => { const onError = (data) => {
if (!mainWindow) return; if (!mainWindow) return;
mainWindow.setProgressBar(-1); mainWindow.setProgressBar(-1);
mainWindow.webContents.send(COMMAND.UPSCAYL_ERROR, data.toString()); mainWindow.webContents.send(
ELECTRON_COMMANDS.UPSCAYL_ERROR,
data.toString(),
);
failed = true; failed = true;
upscayl.kill(); upscayl.kill();
return; return;
@ -137,7 +146,7 @@ const imageUpscayl = async (event, payload: ImageUpscaylPayload) => {
// Free up memory // Free up memory
upscayl.kill(); upscayl.kill();
mainWindow.setProgressBar(-1); mainWindow.setProgressBar(-1);
mainWindow.webContents.send(COMMAND.UPSCAYL_DONE, outFile); mainWindow.webContents.send(ELECTRON_COMMANDS.UPSCAYL_DONE, outFile);
showNotification("Upscayl", "Image upscayled successfully!"); showNotification("Upscayl", "Image upscayled successfully!");
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,19 +11,6 @@ import { atomWithStorage } from "jotai/utils";
type Translations = typeof en; type Translations = typeof en;
type Locales = "en" | "ru" | "ja" | "zh" | "es" | "fr"; 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> = { const translations: Record<Locales, Translations> = {
en, en,
ru, ru,
@ -33,7 +20,44 @@ export const translationAtom = atom((get) => {
fr, 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); const template = getNestedTranslation(translations[locale], key);
// Replace placeholders with parameters, e.g., {name} => John // 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 { translationAtom } from "@/atoms/translations-atom";
import { useAtomValue, useSetAtom } from "jotai"; import { useAtomValue, useSetAtom } from "jotai";
import React from "react"; 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 React from "react";
import Logo from "./icons/Logo"; import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { translationAtom } from "@/atoms/translations-atom"; 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")} data-tooltip-content={t("HEADER.GITHUB_BUTTON_TITLE")}
> >
<div className="flex items-center gap-3 px-5 py-5"> <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"> <div className="flex flex-col justify-center">
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
{t("TITLE")}{" "} {t("TITLE")}{" "}
<span className="text-xs"> <span className="text-xs">
{version} {featureFlags.APP_STORE_BUILD && "Mac"} {version} {FEATURE_FLAGS.APP_STORE_BUILD && "Mac"}
</span> </span>
</h1> </h1>
<p className="">{t("HEADER.DESCRIPTION")}</p> <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 log from "electron-log/renderer";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import React from "react"; import React from "react";
const useLog = () => { const useLogger = () => {
const setLogData = useSetAtom(logAtom); const setLogData = useSetAtom(logAtom);
const logit = (...args: any) => { const logit = (...args: any) => {
@ -13,9 +13,7 @@ const useLog = () => {
setLogData((prevLogData) => [...prevLogData, data]); setLogData((prevLogData) => [...prevLogData, data]);
}; };
return { return logit;
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() { function Spinner() {
return ( return (
// By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL // By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL
@ -10,7 +6,8 @@ function Spinner() {
fill="currentColor" fill="currentColor"
stroke="currentColor" stroke="currentColor"
xmlns="http://www.w3.org/2000/svg" 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 fill="none" fill-rule="evenodd">
<g transform="translate(2 1)" stroke="currentColor" stroke-width="1.5"> <g transform="translate(2 1)" stroke="currentColor" stroke-width="1.5">
<circle <circle
@ -18,7 +15,8 @@ function Spinner() {
cy="11.462" cy="11.462"
r="5" r="5"
fill-opacity="1" fill-opacity="1"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"
@ -33,7 +31,8 @@ function Spinner() {
cy="27.063" cy="27.063"
r="5" r="5"
fill-opacity="0" fill-opacity="0"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"
@ -48,7 +47,8 @@ function Spinner() {
cy="42.663" cy="42.663"
r="5" r="5"
fill-opacity="0" fill-opacity="0"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"
@ -63,7 +63,8 @@ function Spinner() {
cy="49.125" cy="49.125"
r="5" r="5"
fill-opacity="0" fill-opacity="0"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"
@ -78,7 +79,8 @@ function Spinner() {
cy="42.663" cy="42.663"
r="5" r="5"
fill-opacity="0" fill-opacity="0"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"
@ -93,7 +95,8 @@ function Spinner() {
cy="27.063" cy="27.063"
r="5" r="5"
fill-opacity="0" fill-opacity="0"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"
@ -108,7 +111,8 @@ function Spinner() {
cy="11.462" cy="11.462"
r="5" r="5"
fill-opacity="0" fill-opacity="0"
fill="currentColor"> fill="currentColor"
>
<animate <animate
attributeName="fill-opacity" attributeName="fill-opacity"
begin="0s" begin="0s"

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
const Logo = ({ ...rest }) => { const UpscaylSVGLogo = ({ ...rest }) => {
return ( return (
<svg <svg
viewBox="0 0 256 256" 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 { 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 { cn } from "@/lib/utils";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { WrenchIcon } from "lucide-react"; import { WrenchIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
const ImageOptions = ({ const ImageViewSettings = ({
zoomAmount, zoomAmount,
setZoomAmount, setZoomAmount,
resetImagePaths, resetImagePaths,
hideZoomOptions,
}: { }: {
zoomAmount: string; zoomAmount: string;
setZoomAmount: (arg: any) => void; setZoomAmount: (arg: any) => void;
resetImagePaths: () => void; resetImagePaths: () => void;
hideZoomOptions?: boolean;
}) => { }) => {
const [openSidebar, setOpenSidebar] = useState(false); const [openSidebar, setOpenSidebar] = useState(false);
const [viewType, setViewType] = useAtom(viewTypeAtom); 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 { useAtomValue } from "jotai";
import React from "react"; import React from "react";
function RightPaneInfo({ version, batchMode }) { function InstructionsCard({ version, batchMode }) {
const t = useAtomValue(translationAtom); const t = useAtomValue(translationAtom);
return ( 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 React, { useEffect } from "react";
import Spinner from "../../icons/Spinner"; import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
import Logo from "@/components/icons/Logo";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { translationAtom } from "@/atoms/translations-atom"; import { translationAtom } from "@/atoms/translations-atom";
import { ELECTRON_COMMANDS } from "@common/electron-commands";
import useLogger from "../hooks/use-logger";
function ProgressBar({ function ProgressBar({
progress, progress,
doubleUpscaylCounter, doubleUpscaylCounter,
stopHandler,
batchMode, batchMode,
resetImagePaths,
}: { }: {
progress: string; progress: string;
doubleUpscaylCounter: number; doubleUpscaylCounter: number;
stopHandler: () => void;
batchMode: boolean; batchMode: boolean;
resetImagePaths: () => void;
}) { }) {
const [batchProgress, setBatchProgress] = React.useState(0); const [batchProgress, setBatchProgress] = React.useState(0);
const t = useAtomValue(translationAtom); const t = useAtomValue(translationAtom);
const logit = useLogger();
useEffect(() => { useEffect(() => {
const progressString = progress.trim().replace(/\n/g, ""); const progressString = progress.trim().replace(/\n/g, "");
@ -26,6 +28,12 @@ function ProgressBar({
} }
}, [progress]); }, [progress]);
const stopHandler = () => {
window.electron.send(ELECTRON_COMMANDS.STOP);
logit("🛑 Stopping Upscayl");
resetImagePaths();
};
// const progressStyle = useMemo(() => { // const progressStyle = useMemo(() => {
// if (progress.includes("%")) { // if (progress.includes("%")) {
// return { // return {
@ -44,11 +52,13 @@ function ProgressBar({
return ( 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="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"> <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"> <p className="rounded-full px-2 pb-2 font-bold">
{batchMode && {batchMode &&
`${t("APP.PROGRESS_BAR.BATCH_UPSCAYL_IN_PROGRESS_TITLE")} ${batchProgress}`} `${t("APP.PROGRESS_BAR.BATCH_UPSCAYL_IN_PROGRESS_TITLE")} ${batchProgress}`}
</p> </p>
<div className="flex flex-col items-center gap-1"> <div className="flex flex-col items-center gap-1">
{progress !== "Hold on..." ? ( {progress !== "Hold on..." ? (
<p className="text-sm font-bold"> <p className="text-sm font-bold">
@ -60,10 +70,12 @@ function ProgressBar({
) : ( ) : (
<p className="text-sm font-bold">{progress}</p> <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"> <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")} {t("APP.PROGRESS_BAR.IN_PROGRESS_TITLE")}
</p> </p>
</div> </div>
<button onClick={stopHandler} className="btn btn-outline"> <button onClick={stopHandler} className="btn btn-outline">
{t("APP.PROGRESS_BAR.STOP_BUTTON_TITLE")} {t("APP.PROGRESS_BAR.STOP_BUTTON_TITLE")}
</button> </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 { SelectTheme } from "./select-theme";
import { SaveOutputFolderToggle } from "./SaveOutputFolderToggle"; import { SaveOutputFolderToggle } from "./save-output-folder-toggle";
import { GpuIdInput } from "./GpuIdInput"; import { InputGpuId } from "./input-gpu-id";
import { CustomModelsFolderSelect } from "./CustomModelsFolderSelect"; import { CustomModelsFolderSelect } from "./select-custom-models-folder";
import { LogArea } from "./LogArea"; import { LogArea } from "./log-area";
import { ImageScaleSelect } from "./ImageScaleSelect"; import { SelectImageScale } from "./select-image-scale";
import { ImageFormatSelect } from "./ImageFormatSelect"; import { SelectImageFormat } from "./select-image-format";
import { DonateButton } from "./DonateButton"; import { DonateButton } from "./donate-button";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { themeChange } from "theme-change"; import { themeChange } from "theme-change";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { customModelsPathAtom, scaleAtom } from "../../atoms/userSettingsAtom"; import { customModelsPathAtom, scaleAtom } from "@/atoms/user-settings-atom";
import { modelsListAtom } from "../../atoms/modelsListAtom"; import { modelsListAtom } from "@/atoms/models-list-atom";
import useLog from "../hooks/useLog"; import useLogger from "@/components/hooks/use-logger";
import { CompressionInput } from "./CompressionInput"; import { InputCompression } from "./input-compression";
import OverwriteToggle from "./OverwriteToggle"; import OverwriteToggle from "./overwrite-toggle";
import { UpscaylCloudModal } from "../UpscaylCloudModal"; import { UpscaylCloudModal } from "@/components/upscayl-cloud-modal";
import { ResetSettings } from "./ResetSettings"; import { ResetSettingsButton } from "./reset-settings-button";
import { featureFlags } from "@common/feature-flags"; import { FEATURE_FLAGS } from "@common/feature-flags";
import TurnOffNotificationsToggle from "./TurnOffNotificationsToggle"; import TurnOffNotificationsToggle from "./turn-off-notifications-toggle";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CustomResolutionInput } from "./CustomResolutionInput"; import { InputCustomResolution } from "./input-custom-resolution";
import { TileSizeInput } from "./TileSizeInput"; import { InputTileSize } from "./input-tile-size";
import LanguageSwitcher from "./language-switcher"; import LanguageSwitcher from "./language-switcher";
import { translationAtom } from "@/atoms/translations-atom"; import { translationAtom } from "@/atoms/translations-atom";
@ -34,7 +34,6 @@ interface IProps {
gpuId: string; gpuId: string;
setGpuId: React.Dispatch<React.SetStateAction<string>>; setGpuId: React.Dispatch<React.SetStateAction<string>>;
logData: string[]; logData: string[];
os: "linux" | "mac" | "win" | undefined;
show: boolean; show: boolean;
setShow: React.Dispatch<React.SetStateAction<boolean>>; setShow: React.Dispatch<React.SetStateAction<boolean>>;
setDontShowCloudModal: React.Dispatch<React.SetStateAction<boolean>>; setDontShowCloudModal: React.Dispatch<React.SetStateAction<boolean>>;
@ -50,19 +49,11 @@ function SettingsTab({
saveImageAs, saveImageAs,
setSaveImageAs, setSaveImageAs,
logData, logData,
os,
show, show,
setShow, setShow,
setDontShowCloudModal, setDontShowCloudModal,
}: IProps) { }: IProps) {
// STATES
const [currentModel, setCurrentModel] = useState<{
label: string;
value: string;
}>({
label: null,
value: null,
});
const [isCopied, setIsCopied] = useState(false); const [isCopied, setIsCopied] = useState(false);
const [customModelsPath, setCustomModelsPath] = useAtom(customModelsPathAtom); const [customModelsPath, setCustomModelsPath] = useAtom(customModelsPathAtom);
@ -72,7 +63,7 @@ function SettingsTab({
const [timeoutId, setTimeoutId] = useState(null); const [timeoutId, setTimeoutId] = useState(null);
const t = useAtomValue(translationAtom); const t = useAtomValue(translationAtom);
const { logit } = useLog(); const logit = useLogger();
useEffect(() => { useEffect(() => {
themeChange(false); themeChange(false);
@ -90,7 +81,6 @@ function SettingsTab({
} }
if (!localStorage.getItem("model")) { if (!localStorage.getItem("model")) {
setCurrentModel(modelOptions[0]);
setModel(modelOptions[0].value); setModel(modelOptions[0].value);
localStorage.setItem("model", JSON.stringify(modelOptions[0])); localStorage.setItem("model", JSON.stringify(modelOptions[0]));
logit("🔀 Setting model to", modelOptions[0].value); logit("🔀 Setting model to", modelOptions[0].value);
@ -107,7 +97,6 @@ function SettingsTab({
logit("🔀 Setting model to", modelOptions[0].value); logit("🔀 Setting model to", modelOptions[0].value);
currentlySavedModel = modelOptions[0]; currentlySavedModel = modelOptions[0];
} }
setCurrentModel(currentlySavedModel);
setModel(currentlySavedModel.value); setModel(currentlySavedModel.value);
logit( logit(
"⚙️ Getting model from localStorage: ", "⚙️ Getting model from localStorage: ",
@ -194,7 +183,7 @@ function SettingsTab({
> >
{t("SETTINGS.SUPPORT.DOCS_BUTTON_TITLE")} {t("SETTINGS.SUPPORT.DOCS_BUTTON_TITLE")}
</a> </a>
{featureFlags.APP_STORE_BUILD && ( {FEATURE_FLAGS.APP_STORE_BUILD && (
<a <a
className="btn btn-primary" 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`} 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")} {t("SETTINGS.SUPPORT.EMAIL_BUTTON_TITLE")}
</a> </a>
)} )}
{!featureFlags.APP_STORE_BUILD && <DonateButton />} {!FEATURE_FLAGS.APP_STORE_BUILD && <DonateButton />}
</div> </div>
<LogArea <LogArea
@ -213,23 +202,23 @@ function SettingsTab({
/> />
{/* THEME SELECTOR */} {/* THEME SELECTOR */}
<ThemeSelect /> <SelectTheme />
<LanguageSwitcher /> <LanguageSwitcher />
{/* IMAGE FORMAT BUTTONS */} {/* IMAGE FORMAT BUTTONS */}
<ImageFormatSelect <SelectImageFormat
batchMode={batchMode} batchMode={batchMode}
saveImageAs={saveImageAs} saveImageAs={saveImageAs}
setExportType={setExportType} setExportType={setExportType}
/> />
{/* IMAGE SCALE */} {/* IMAGE SCALE */}
<ImageScaleSelect scale={scale} setScale={setScale} /> <SelectImageScale scale={scale} setScale={setScale} />
<CustomResolutionInput /> <InputCustomResolution />
<CompressionInput <InputCompression
compression={compression} compression={compression}
handleCompressionChange={handleCompressionChange} handleCompressionChange={handleCompressionChange}
/> />
@ -240,9 +229,9 @@ function SettingsTab({
<TurnOffNotificationsToggle /> <TurnOffNotificationsToggle />
{/* GPU ID INPUT */} {/* GPU ID INPUT */}
<GpuIdInput gpuId={gpuId} handleGpuIdChange={handleGpuIdChange} /> <InputGpuId gpuId={gpuId} handleGpuIdChange={handleGpuIdChange} />
<TileSizeInput /> <InputTileSize />
{/* CUSTOM MODEL */} {/* CUSTOM MODEL */}
<CustomModelsFolderSelect <CustomModelsFolderSelect
@ -251,9 +240,9 @@ function SettingsTab({
/> />
{/* RESET SETTINGS */} {/* RESET SETTINGS */}
<ResetSettings /> <ResetSettingsButton />
{featureFlags.SHOW_UPSCAYL_CLOUD_INFO && ( {FEATURE_FLAGS.SHOW_UPSCAYL_CLOUD_INFO && (
<> <>
<button <button
className="mx-5 mb-5 animate-pulse rounded-btn bg-success p-1 text-sm text-slate-50 shadow-lg shadow-success/40" 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; handleCompressionChange: (arg: any) => void;
}; };
export function CompressionInput({ export function InputCompression({
compression, compression,
handleCompressionChange, handleCompressionChange,
}: CompressionInputProps) { }: 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 { useAtom, useAtomValue } from "jotai";
import React from "react"; import React from "react";
import { translationAtom } from "@/atoms/translations-atom"; import { translationAtom } from "@/atoms/translations-atom";
export function CustomResolutionInput() { export function InputCustomResolution() {
const [useCustomWidth, setUseCustomWidth] = useAtom(useCustomWidthAtom); const [useCustomWidth, setUseCustomWidth] = useAtom(useCustomWidthAtom);
const [customWidth, setCustomWidth] = useAtom(customWidthAtom); const [customWidth, setCustomWidth] = useAtom(customWidthAtom);
const t = useAtomValue(translationAtom); const t = useAtomValue(translationAtom);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -127,7 +127,7 @@
"FROM_TITLE": "Upscayl元 ", "FROM_TITLE": "Upscayl元 ",
"TO_TITLE": " から ", "TO_TITLE": " から ",
"NO_OUTPUT_FOLDER_ALERT": "まず出力フォルダを選択してください", "NO_OUTPUT_FOLDER_ALERT": "まず出力フォルダを選択してください",
"START_BUTTON_TITLE": "Upscayl", "START_BUTTON_TITLE": "Upscayl 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Upscayl中 ⏳" "IN_PROGRESS_BUTTON_TITLE": "Upscayl中 ⏳"
}, },
"IMAGE_OPTIONS": { "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" "LINK_TITLE": "Команда Upscayl"
}, },
"SETTINGS": { "SETTINGS": {
"TITLE": "НАСТРОЙКИ", "TITLE": "Настройки",
"CHANGE_LANGUAGE": { "TITLE": "Сменить язык" }, "CHANGE_LANGUAGE": { "TITLE": "Сменить язык" },
"IMAGE_COMPRESSION": { "IMAGE_COMPRESSION": {
"TITLE": "Сжатие изображения", "TITLE": "Сжатие изображения",
@ -127,7 +127,7 @@
"FROM_TITLE": "Увеличить с ", "FROM_TITLE": "Увеличить с ",
"TO_TITLE": " до ", "TO_TITLE": " до ",
"NO_OUTPUT_FOLDER_ALERT": "Пожалуйста, сначала выберите папку вывода", "NO_OUTPUT_FOLDER_ALERT": "Пожалуйста, сначала выберите папку вывода",
"START_BUTTON_TITLE": "Увеличить", "START_BUTTON_TITLE": "Увеличить 🚀",
"IN_PROGRESS_BUTTON_TITLE": "Увеличение ⏳" "IN_PROGRESS_BUTTON_TITLE": "Увеличение ⏳"
}, },
"IMAGE_OPTIONS": { "IMAGE_OPTIONS": {

View File

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

View File

@ -1,143 +1,107 @@
"use client"; "use client";
import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useState, useEffect } from "react";
import COMMAND from "../../common/commands"; import { ELECTRON_COMMANDS } from "@common/electron-commands";
import { ReactCompareSlider } from "react-compare-slider"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import Header from "../components/Header"; import { modelsListAtom } from "../atoms/models-list-atom";
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 { import {
batchModeAtom, batchModeAtom,
lensSizeAtom,
compressionAtom,
dontShowCloudModalAtom,
noImageProcessingAtom,
savedOutputPathAtom, savedOutputPathAtom,
overwriteAtom,
progressAtom, progressAtom,
scaleAtom,
viewTypeAtom,
rememberOutputFolderAtom, rememberOutputFolderAtom,
showSidebarAtom, } from "../atoms/user-settings-atom";
customWidthAtom, import useLogger from "../components/hooks/use-logger";
useCustomWidthAtom, import { newsAtom, showNewsModalAtom } from "@/atoms/news-atom";
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";
import { useToast } from "@/components/ui/use-toast"; import { useToast } from "@/components/ui/use-toast";
import { ToastAction } from "@/components/ui/toast"; import { ToastAction } from "@/components/ui/toast";
import Logo from "@/components/icons/Logo"; import UpscaylSVGLogo from "@/components/icons/upscayl-logo-svg";
import { sanitizePath } from "@common/sanitize-path";
import getDirectoryFromPath from "@common/get-directory-from-path";
import { translationAtom } from "@/atoms/translations-atom"; 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 Home = () => {
const allowedFileTypes = ["png", "jpg", "jpeg", "jfif", "webp"];
const t = useAtomValue(translationAtom); 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 [imagePath, setImagePath] = useState("");
const [upscaledImagePath, setUpscaledImagePath] = 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({ const [dimensions, setDimensions] = useState({
width: null, width: null,
height: null, height: null,
}); });
const [selectedTab, setSelectedTab] = useState(0); const setOutputPath = useSetAtom(savedOutputPathAtom);
const [isLoading, setIsLoading] = useState(true); const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const [showCloudModal, setShowCloudModal] = useState(false);
const [minSize, setMinSize] = useState(22); const batchMode = useAtomValue(batchModeAtom);
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 }); const [batchFolderPath, setBatchFolderPath] = useState("");
const upscaledImageRef = useRef<HTMLImageElement>(null); const [upscaledBatchFolderPath, setUpscaledBatchFolderPath] = useState("");
const [lensPosition, setLensPosition] = useState({ x: 0, y: 0 });
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 [modelOptions, setModelOptions] = useAtom(modelsListAtom);
const [scale] = useAtom(scaleAtom);
const [dontShowCloudModal, setDontShowCloudModal] = useAtom(
dontShowCloudModalAtom,
);
const noImageProcessing = useAtomValue(noImageProcessingAtom);
const [news, setNews] = useAtom(newsAtom); const [news, setNews] = useAtom(newsAtom);
const [showNewsModal, setShowNewsModal] = useAtom(showNewsModalAtom); 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 selectImageHandler = async () => {
const { toast } = useToast(); 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( const selectFolderHandler = async () => {
() => sanitizePath(imagePath), resetImagePaths();
[imagePath], const path = await window.electron.invoke(ELECTRON_COMMANDS.SELECT_FOLDER);
); if (path !== null) {
logit("🖼 Selected Folder Path: ", path);
const sanitizedUpscaledImagePath = useMemo( setBatchFolderPath(path);
() => sanitizePath(upscaledImagePath), if (!rememberOutputFolder) {
[upscaledImagePath], setOutputPath(path);
); }
} else {
const handleMouseMoveCompare = (e: React.MouseEvent) => { logit("🚫 Folder selection cancelled");
if (upscaledImageRef.current) { setBatchFolderPath("");
const { left, top, width, height } = if (!rememberOutputFolder) {
upscaledImageRef.current.getBoundingClientRect(); setOutputPath("");
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)),
});
} }
}; };
// SET CONFIG VARIABLES ON FIRST RUN const validateImagePath = (path: string) => {
useEffect(() => { if (path.length > 0) {
// UPSCAYL VERSION logit("🖼 imagePath: ", path);
const upscaylVersion = navigator?.userAgent?.match( const extension = path.split(".").pop().toLowerCase();
/Upscayl\/([\d\.]+\d+)/, logit("🔤 Extension: ", extension);
)[1]; if (!VALID_IMAGE_FORMATS.includes(extension)) {
setVersion(upscaylVersion); toast({
}, []); title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("ERRORS.INVALID_IMAGE_ERROR.DESCRIPTION"),
});
resetImagePaths();
}
} else {
resetImagePaths();
}
};
// ELECTRON EVENT LISTENERS // ELECTRON EVENT LISTENERS
useEffect(() => { useEffect(() => {
@ -203,25 +167,19 @@ const Home = () => {
resetImagePaths(); resetImagePaths();
} }
}; };
// OS
window.electron.on(
COMMAND.OS,
(_, data: "linux" | "mac" | "win" | undefined) => {
if (data) {
setOs(data);
}
},
);
// LOG // LOG
window.electron.on(COMMAND.LOG, (_, data: string) => { window.electron.on(ELECTRON_COMMANDS.LOG, (_, data: string) => {
logit(`🎒 BACKEND REPORTED: `, data); logit(`🎒 BACKEND REPORTED: `, data);
}); });
// SCALING AND CONVERTING // 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")); setProgress(t("APP.PROGRESS.PROCESSING_TITLE"));
}); },
);
// UPSCAYL ERROR // UPSCAYL ERROR
window.electron.on(COMMAND.UPSCAYL_ERROR, (_, data: string) => { window.electron.on(ELECTRON_COMMANDS.UPSCAYL_ERROR, (_, data: string) => {
toast({ toast({
title: t("ERRORS.GENERIC_ERROR.TITLE"), title: t("ERRORS.GENERIC_ERROR.TITLE"),
description: data, description: data,
@ -229,7 +187,9 @@ const Home = () => {
resetImagePaths(); resetImagePaths();
}); });
// UPSCAYL PROGRESS // 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) { if (data.length > 0 && data.length < 10) {
setProgress(data); setProgress(data);
} else if (data.includes("converting")) { } else if (data.includes("converting")) {
@ -239,9 +199,12 @@ const Home = () => {
} }
handleErrors(data); handleErrors(data);
logit(`🚧 UPSCAYL_PROGRESS: `, data); logit(`🚧 UPSCAYL_PROGRESS: `, data);
}); },
);
// FOLDER UPSCAYL PROGRESS // 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")) { if (data.includes("Successful")) {
setProgress(t("APP.PROGRESS.SUCCESS_TITLE")); setProgress(t("APP.PROGRESS.SUCCESS_TITLE"));
} }
@ -250,9 +213,12 @@ const Home = () => {
} }
handleErrors(data); handleErrors(data);
logit(`🚧 FOLDER_UPSCAYL_PROGRESS: `, data); logit(`🚧 FOLDER_UPSCAYL_PROGRESS: `, data);
}); },
);
// DOUBLE UPSCAYL PROGRESS // 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.length > 0 && data.length < 10) {
if (data === "0.00%") { if (data === "0.00%") {
setDoubleUpscaylCounter(doubleUpscaylCounter + 1); setDoubleUpscaylCounter(doubleUpscaylCounter + 1);
@ -261,29 +227,38 @@ const Home = () => {
} }
handleErrors(data); handleErrors(data);
logit(`🚧 DOUBLE_UPSCAYL_PROGRESS: `, data); logit(`🚧 DOUBLE_UPSCAYL_PROGRESS: `, data);
}); },
);
// UPSCAYL DONE // UPSCAYL DONE
window.electron.on(COMMAND.UPSCAYL_DONE, (_, data: string) => { window.electron.on(ELECTRON_COMMANDS.UPSCAYL_DONE, (_, data: string) => {
setProgress(""); setProgress("");
setUpscaledImagePath(data); setUpscaledImagePath(data);
logit("upscaledImagePath: ", data); logit("upscaledImagePath: ", data);
logit(`💯 UPSCAYL_DONE: `, data); logit(`💯 UPSCAYL_DONE: `, data);
}); });
// FOLDER UPSCAYL DONE // FOLDER UPSCAYL DONE
window.electron.on(COMMAND.FOLDER_UPSCAYL_DONE, (_, data: string) => { window.electron.on(
ELECTRON_COMMANDS.FOLDER_UPSCAYL_DONE,
(_, data: string) => {
setProgress(""); setProgress("");
setUpscaledBatchFolderPath(data); setUpscaledBatchFolderPath(data);
logit(`💯 FOLDER_UPSCAYL_DONE: `, data); logit(`💯 FOLDER_UPSCAYL_DONE: `, data);
}); },
);
// DOUBLE UPSCAYL DONE // DOUBLE UPSCAYL DONE
window.electron.on(COMMAND.DOUBLE_UPSCAYL_DONE, (_, data: string) => { window.electron.on(
ELECTRON_COMMANDS.DOUBLE_UPSCAYL_DONE,
(_, data: string) => {
setProgress(""); setProgress("");
setTimeout(() => setUpscaledImagePath(data), 500); setTimeout(() => setUpscaledImagePath(data), 500);
setDoubleUpscaylCounter(0); setDoubleUpscaylCounter(0);
logit(`💯 DOUBLE_UPSCAYL_DONE: `, data); logit(`💯 DOUBLE_UPSCAYL_DONE: `, data);
}); },
);
// CUSTOM FOLDER LISTENER // 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); logit(`📜 CUSTOM_MODEL_FILES_LIST: `, data);
const newModelOptions = data.map((model) => { const newModelOptions = data.map((model) => {
return { return {
@ -292,68 +267,16 @@ const Home = () => {
}; };
}); });
// Add newModelsList to modelOptions and remove duplicates // Add newModelsList to modelOptions and remove duplicates
const combinedModelOptions = [...modelOptions, ...newModelOptions]; const modelMap = new Map();
const uniqueModelOptions = combinedModelOptions.filter( [...modelOptions, ...newModelOptions].forEach((model) => {
// Check if any model in the array appears more than once modelMap.set(model.value, model);
(model, index, array) => });
array.findIndex((t) => t.value === model.value) === index, const uniqueModelOptions = Array.from(modelMap.values());
);
setModelOptions(uniqueModelOptions); 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 // LOADING STATE
useEffect(() => { useEffect(() => {
setIsLoading(false); setIsLoading(false);
@ -373,555 +296,36 @@ const Home = () => {
setUpscaledBatchFolderPath(""); 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) { if (isLoading) {
return ( 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 ( return (
<div className="flex h-screen w-screen flex-row overflow-hidden bg-base-300"> <div className="flex h-screen w-screen flex-row overflow-hidden bg-base-300">
{/* TOP LOGO WHEN SIDEBAR IS HIDDEN */} <Sidebar
{!showSidebar && ( imagePath={imagePath}
<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 "> dimensions={dimensions}
<Logo className="w-5" /> setUpscaledImagePath={setUpscaledImagePath}
{t("TITLE")} batchFolderPath={batchFolderPath}
</div> setUpscaledBatchFolderPath={setUpscaledBatchFolderPath}
)}
{/* 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
selectImageHandler={selectImageHandler} selectImageHandler={selectImageHandler}
selectFolderHandler={selectFolderHandler} selectFolderHandler={selectFolderHandler}
handleModelChange={handleModelChange} />
upscaylHandler={upscaylHandler} <MainContent
batchMode={batchMode}
setBatchMode={setBatchMode}
imagePath={imagePath} imagePath={imagePath}
doubleUpscayl={doubleUpscayl} resetImagePaths={resetImagePaths}
setDoubleUpscayl={setDoubleUpscayl} upscaledBatchFolderPath={upscaledBatchFolderPath}
dimensions={dimensions} setImagePath={setImagePath}
setGpuId={setGpuId} validateImagePath={validateImagePath}
model={model} selectFolderHandler={selectFolderHandler}
setModel={setModel} selectImageHandler={selectImageHandler}
setSaveImageAs={setSaveImageAs} batchFolderPath={batchFolderPath}
/> upscaledImagePath={upscaledImagePath}
)}
{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}
doubleUpscaylCounter={doubleUpscaylCounter} 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> </div>
); );
}; };

View File

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

View File

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