"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import COMMAND from "../../common/commands";
import { ReactCompareSlider } from "react-compare-slider";
import Header from "../components/Header";
import Footer from "../components/Footer";
import ProgressBar from "../components/upscayl-tab/view/ProgressBar";
import RightPaneInfo from "../components/upscayl-tab/view/RightPaneInfo";
import ImageOptions from "../components/upscayl-tab/view/ImageOptions";
import LeftPaneImageSteps from "../components/upscayl-tab/config/LeftPaneImageSteps";
import Tabs from "../components/Tabs";
import SettingsTab from "../components/settings-tab";
import { useAtom, useAtomValue } from "jotai";
import { logAtom } from "../atoms/logAtom";
import { modelsListAtom } from "../atoms/modelsListAtom";
import {
batchModeAtom,
lensSizeAtom,
compressionAtom,
dontShowCloudModalAtom,
noImageProcessingAtom,
savedOutputPathAtom,
overwriteAtom,
progressAtom,
scaleAtom,
viewTypeAtom,
rememberOutputFolderAtom,
showSidebarAtom,
customWidthAtom,
useCustomWidthAtom,
tileSizeAtom,
} from "../atoms/userSettingsAtom";
import useLog from "../components/hooks/useLog";
import { UpscaylCloudModal } from "../components/UpscaylCloudModal";
import { featureFlags } from "@common/feature-flags";
import {
BatchUpscaylPayload,
DoubleUpscaylPayload,
ImageUpscaylPayload,
} from "@common/types/types";
import { NewsModal } from "@/components/NewsModal";
import { newsAtom, showNewsModalAtom } from "@/atoms/newsAtom";
import matter from "gray-matter";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { useToast } from "@/components/ui/use-toast";
import { ToastAction } from "@/components/ui/toast";
import Logo from "@/components/icons/Logo";
import { sanitizePath } from "@common/sanitize-path";
import getDirectoryFromPath from "@common/get-directory-from-path";
import { translationAtom } from "@/atoms/translations-atom";
const Home = () => {
const allowedFileTypes = ["png", "jpg", "jpeg", "webp"];
const t = useAtomValue(translationAtom);
// LOCAL STATES
const [os, setOs] = useState<"linux" | "mac" | "win" | undefined>(undefined);
const [imagePath, setImagePath] = useState("");
const [upscaledImagePath, setUpscaledImagePath] = useState("");
const [model, setModel] = useState("realesrgan-x4plus");
const [version, setVersion] = useState("");
const [batchFolderPath, setBatchFolderPath] = useState("");
const [doubleUpscayl, setDoubleUpscayl] = useState(false);
const overwrite = useAtomValue(overwriteAtom);
const [upscaledBatchFolderPath, setUpscaledBatchFolderPath] = useState("");
const [doubleUpscaylCounter, setDoubleUpscaylCounter] = useState(0);
const [gpuId, setGpuId] = useState("");
const [saveImageAs, setSaveImageAs] = useState("png");
const [zoomAmount, setZoomAmount] = useState("100");
const [backgroundPosition, setBackgroundPosition] = useState("0% 0%");
const [dimensions, setDimensions] = useState({
width: null,
height: null,
});
const [selectedTab, setSelectedTab] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [showCloudModal, setShowCloudModal] = useState(false);
const [minSize, setMinSize] = useState(22);
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
// ATOMIC STATES
const [outputPath, setOutputPath] = useAtom(savedOutputPathAtom);
const [compression, setCompression] = useAtom(compressionAtom);
const [progress, setProgress] = useAtom(progressAtom);
const [batchMode, setBatchMode] = useAtom(batchModeAtom);
const [logData, setLogData] = useAtom(logAtom);
const [modelOptions, setModelOptions] = useAtom(modelsListAtom);
const [scale] = useAtom(scaleAtom);
const [dontShowCloudModal, setDontShowCloudModal] = useAtom(
dontShowCloudModalAtom,
);
const noImageProcessing = useAtomValue(noImageProcessingAtom);
const [news, setNews] = useAtom(newsAtom);
const [showNewsModal, setShowNewsModal] = useAtom(showNewsModalAtom);
const viewType = useAtomValue(viewTypeAtom);
const lensSize = useAtomValue(lensSizeAtom);
const rememberOutputFolder = useAtomValue(rememberOutputFolderAtom);
const [showSidebar, setShowSidebar] = useAtom(showSidebarAtom);
const customWidth = useAtomValue(customWidthAtom);
const useCustomWidth = useAtomValue(useCustomWidthAtom);
const tileSize = useAtomValue(tileSizeAtom);
const { logit } = useLog();
const { toast } = useToast();
const sanitizedImagePath = useMemo(
() => sanitizePath(imagePath),
[imagePath],
);
const sanitizedUpscaledImagePath = useMemo(
() => sanitizePath(upscaledImagePath),
[upscaledImagePath],
);
const handleMouseMoveCompare = (e: React.MouseEvent) => {
const { left, top, height, width } =
e.currentTarget.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
setCursorPosition({ x, y });
const xZoom = ((e.pageX - left) / width) * 100;
const yZoom = ((e.pageY - top) / height) * 100;
setBackgroundPosition(`${xZoom}% ${yZoom}%`);
};
// SET CONFIG VARIABLES ON FIRST RUN
useEffect(() => {
// UPSCAYL VERSION
const upscaylVersion = navigator?.userAgent?.match(
/Upscayl\/([\d\.]+\d+)/,
)[1];
setVersion(upscaylVersion);
}, []);
// ELECTRON EVENT LISTENERS
useEffect(() => {
const handleErrors = (data: string) => {
if (data.includes("Invalid GPU")) {
toast({
title: t("APP.ERRORS.GPU_ERROR.TITLE"),
description: t("APP.ERRORS.GPU_ERROR.DESC", { data }),
action: (
),
});
resetImagePaths();
} else if (data.includes("write") || data.includes("read")) {
if (batchMode) return;
toast({
title: t("APP.ERRORS.READ_WRITE_ERROR.TITLE"),
description: t("APP.ERRORS.READ_WRITE_ERROR.DESC", { data }),
action: (
),
});
resetImagePaths();
} else if (data.includes("tile size")) {
toast({
title: t("APP.ERRORS.TILE_SIZE_ERROR.TITLE"),
description: t("APP.ERRORS.TILE_SIZE_ERROR.DESC", { data }),
});
resetImagePaths();
} else if (data.includes("uncaughtException")) {
toast({
title: t("APP.ERRORS.EXCEPTION_ERROR.TITLE"),
description: t("APP.ERRORS.EXCEPTION_ERROR.DESC"),
});
resetImagePaths();
}
};
// OS
window.electron.on(
COMMAND.OS,
(_, data: "linux" | "mac" | "win" | undefined) => {
if (data) {
setOs(data);
}
},
);
// LOG
window.electron.on(COMMAND.LOG, (_, data: string) => {
logit(`๐ BACKEND REPORTED: `, data);
});
// SCALING AND CONVERTING
window.electron.on(COMMAND.SCALING_AND_CONVERTING, (_, data: string) => {
setProgress(t("APP.INFOS.IMAGE_PROCESSING.START"));
});
// UPSCAYL ERROR
window.electron.on(COMMAND.UPSCAYL_ERROR, (_, data: string) => {
toast({
title: t("APP.ERRORS.GENERIC_ERROR.TITLE"),
description: data,
});
resetImagePaths();
});
// UPSCAYL PROGRESS
window.electron.on(COMMAND.UPSCAYL_PROGRESS, (_, data: string) => {
if (data.length > 0 && data.length < 10) {
setProgress(data);
} else if (data.includes("converting")) {
setProgress(t("APP.INFOS.IMAGE_PROCESSING.SCALE_CONVERT"));
} else if (data.includes("Successful")) {
setProgress(t("APP.INFOS.IMAGE_PROCESSING.SUCCESS"));
}
handleErrors(data);
logit(`๐ง UPSCAYL_PROGRESS: `, data);
});
// FOLDER UPSCAYL PROGRESS
window.electron.on(COMMAND.FOLDER_UPSCAYL_PROGRESS, (_, data: string) => {
if (data.includes("Successful")) {
setProgress(t("APP.INFOS.IMAGE_PROCESSING.SUCCESS"));
}
if (data.length > 0 && data.length < 10) {
setProgress(data);
}
handleErrors(data);
logit(`๐ง FOLDER_UPSCAYL_PROGRESS: `, data);
});
// DOUBLE UPSCAYL PROGRESS
window.electron.on(COMMAND.DOUBLE_UPSCAYL_PROGRESS, (_, data: string) => {
if (data.length > 0 && data.length < 10) {
if (data === "0.00%") {
setDoubleUpscaylCounter(doubleUpscaylCounter + 1);
}
setProgress(data);
}
handleErrors(data);
logit(`๐ง DOUBLE_UPSCAYL_PROGRESS: `, data);
});
// UPSCAYL DONE
window.electron.on(COMMAND.UPSCAYL_DONE, (_, data: string) => {
setProgress("");
setUpscaledImagePath(data);
logit("upscaledImagePath: ", data);
logit(`๐ฏ UPSCAYL_DONE: `, data);
});
// FOLDER UPSCAYL DONE
window.electron.on(COMMAND.FOLDER_UPSCAYL_DONE, (_, data: string) => {
setProgress("");
setUpscaledBatchFolderPath(data);
logit(`๐ฏ FOLDER_UPSCAYL_DONE: `, data);
});
// DOUBLE UPSCAYL DONE
window.electron.on(COMMAND.DOUBLE_UPSCAYL_DONE, (_, data: string) => {
setProgress("");
setTimeout(() => setUpscaledImagePath(data), 500);
setDoubleUpscaylCounter(0);
logit(`๐ฏ DOUBLE_UPSCAYL_DONE: `, data);
});
// CUSTOM FOLDER LISTENER
window.electron.on(COMMAND.CUSTOM_MODEL_FILES_LIST, (_, data: string[]) => {
logit(`๐ CUSTOM_MODEL_FILES_LIST: `, data);
const newModelOptions = data.map((model) => {
return {
value: model,
label: model,
};
});
// Add newModelsList to modelOptions and remove duplicates
const combinedModelOptions = [...modelOptions, ...newModelOptions];
const uniqueModelOptions = combinedModelOptions.filter(
// Check if any model in the array appears more than once
(model, index, array) =>
array.findIndex((t) => t.value === model.value) === index,
);
setModelOptions(uniqueModelOptions);
});
}, []);
// FETCH CUSTOM MODELS FROM CUSTOM MODELS PATH
useEffect(() => {
const customModelsPath = JSON.parse(
localStorage.getItem("customModelsPath"),
);
if (customModelsPath !== null) {
window.electron.send(COMMAND.GET_MODELS_LIST, customModelsPath);
logit("๐ฏ GET_MODELS_LIST: ", customModelsPath);
}
}, []);
// FETCH NEWS
useEffect(() => {
// TODO: ADD AN ABOUT TAB
if (window && window.navigator.onLine === false) return;
try {
fetch("https://raw.githubusercontent.com/upscayl/upscayl/main/news.md", {
cache: "no-cache",
})
.then((res) => {
return res.text();
})
.then((result) => {
const newsData = result;
if (!newsData) {
console.log("๐ฐ Could not fetch news data");
return;
}
const markdownData = matter(newsData);
if (!markdownData) return;
if (markdownData && markdownData.data.dontShow) {
return;
}
if (
markdownData &&
news &&
markdownData?.data?.version === news?.data?.version
) {
console.log("๐ฐ News is up to date");
if (showNewsModal === false) {
setShowNewsModal(false);
}
} else if (markdownData) {
setNews(matter(newsData));
setShowNewsModal(true);
}
});
} catch (error) {
console.log("Could not fetch Upscayl News");
}
}, [news]);
// LOADING STATE
useEffect(() => {
setIsLoading(false);
}, []);
// HANDLERS
const resetImagePaths = () => {
logit("๐ Resetting image paths");
setDimensions({
width: null,
height: null,
});
setProgress("");
setImagePath("");
setUpscaledImagePath("");
setBatchFolderPath("");
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("APP.ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("APP.ERRORS.INVALID_IMAGE_ERROR.DESC"),
});
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("APP.ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("APP.ERRORS.INVALID_IMAGE_ERROR.DRAG_DESC"),
});
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("APP.ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("APP.ERRORS.INVALID_IMAGE_ERROR.DRAG_DESC"),
});
} 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("APP.ERRORS.INVALID_IMAGE_ERROR.TITLE"),
description: t("APP.ERRORS.INVALID_IMAGE_ERROR.DRAG_DESC"),
});
} 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.INFOS.IMAGE_PROCESSING.WAIT"));
// Double Upscayl
if (doubleUpscayl) {
window.electron.send(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(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(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("APP.ERRORS.NO_IMAGE_ERROR.TITLE"),
description: t("APP.ERRORS.NO_IMAGE_ERROR.DESC"),
});
logit("๐ซ No valid image selected");
}
};
const stopHandler = () => {
window.electron.send(COMMAND.STOP);
logit("๐ Stopping Upscayl");
resetImagePaths();
};
if (isLoading) {
return (
);
}
return (
{/* TOP LOGO WHEN SIDEBAR IS HIDDEN */}
{!showSidebar && (
{t("APP.TITLE")}
)}
{/* SIDEBAR BUTTON */}
{/* LEFT PANE */}
{/* UPSCAYL CLOUD MODAL */}
{featureFlags.SHOW_UPSCAYL_CLOUD_INFO && (
)}
{/* MACOS TITLEBAR */}
{window.electron.platform === "mac" && (
)}
{/* HEADER */}
{!dontShowCloudModal && featureFlags.SHOW_UPSCAYL_CLOUD_INFO && (
)}
{/* NEWS DIALOG */}
{
setShowNewsModal(val);
setNews((prev) => ({ ...prev, seen: true }));
}}
news={news}
/>
{selectedTab === 0 && (
)}
{selectedTab === 1 && (
)}
{/* )} */}
{/* RIGHT PANE */}
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" && (
)}
{progress.length > 0 &&
upscaledImagePath.length === 0 &&
upscaledBatchFolderPath.length === 0 ? (
) : null}
{/* DEFAULT PANE INFO */}
{((!batchMode &&
imagePath.length === 0 &&
upscaledImagePath.length === 0) ||
(batchMode &&
batchFolderPath.length === 0 &&
upscaledBatchFolderPath.length === 0)) && (
)}
{/* SHOW SELECTED IMAGE */}
{!batchMode &&
upscaledImagePath.length === 0 &&
imagePath.length > 0 && (
<>
{
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 && (
{t("APP.INFOS.IMAGE_PROCESSING.BATCH.SELECT")}
{" "}
{batchFolderPath}
)}
{/* BATCH UPSCALE DONE INFO */}
{batchMode && upscaledBatchFolderPath.length > 0 && (
{t("APP.INFOS.IMAGE_PROCESSING.BATCH.DONE")}
)}
{!batchMode &&
viewType === "lens" &&
upscaledImagePath &&
imagePath && (
)}
{/* COMPARISON SLIDER */}
{!batchMode &&
viewType === "slider" &&
imagePath.length > 0 &&
upscaledImagePath.length > 0 && (
<>
{t("APP.INFOS.COMPARISION.SLIDER_ORIGINAL")}
>
}
itemTwo={
<>
{t("APP.INFOS.COMPARISION.SLIDER_PROCESSED")}
>
}
className="group h-screen"
/>
>
)}
);
};
export default Home;