diff --git a/common/electron-commands.ts b/common/electron-commands.ts index f1ebcb7..7eaba07 100644 --- a/common/electron-commands.ts +++ b/common/electron-commands.ts @@ -25,6 +25,9 @@ const ELECTRON_COMMANDS = { OS: "Get OS", SCALING_AND_CONVERTING: "Adding some finishing touches", UPSCAYL_ERROR: "Upscaling Error", + PASTE_IMAGE: "Paste Image from clipboard", + PASTE_IMAGE_SAVE_SUCCESS: "Clipboard Image saved successfully", + PASTE_IMAGE_SAVE_ERROR: "Clipboard Image save failed", } as const; export { ELECTRON_COMMANDS }; diff --git a/common/get-directory-from-path.ts b/common/get-directory-from-path.ts index bc27318..2cb4481 100644 --- a/common/get-directory-from-path.ts +++ b/common/get-directory-from-path.ts @@ -1,12 +1,15 @@ -export default function getDirectoryFromPath(filePath: string): string { +export default function getDirectoryFromPath( + filePath: string, + popFileName: boolean = true, +): string { // Define the path separator based on the operating system const separator = filePath.includes("/") ? "/" : "\\"; // Split the file path by the path separator const pathParts = filePath.split(separator); - // Remove the last element to get the directory - pathParts.pop(); + // Remove the last element to get the directory if popFileName is true + if (popFileName) pathParts.pop(); // Join the remaining parts back together to form the directory path const directoryPath = pathParts.join(separator); diff --git a/electron/commands/batch-upscayl.ts b/electron/commands/batch-upscayl.ts index 584552f..30646f2 100644 --- a/electron/commands/batch-upscayl.ts +++ b/electron/commands/batch-upscayl.ts @@ -11,7 +11,7 @@ import { spawnUpscayl } from "../utils/spawn-upscayl"; import { getBatchArguments } from "../utils/get-arguments"; import slash from "../utils/slash"; import { modelsPath } from "../utils/get-resource-paths"; -import { ELECTRON_COMMANDS } from "@common/electron-commands"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; import { BatchUpscaylPayload } from "../../common/types/types"; import showNotification from "../utils/show-notification"; import { DEFAULT_MODELS } from "../../common/models-list"; diff --git a/electron/commands/custom-models-select.ts b/electron/commands/custom-models-select.ts index a0ab7d0..ae0ef0e 100644 --- a/electron/commands/custom-models-select.ts +++ b/electron/commands/custom-models-select.ts @@ -5,7 +5,7 @@ import { } from "../utils/config-variables"; import logit from "../utils/logit"; import slash from "../utils/slash"; -import { ELECTRON_COMMANDS } from "@common/electron-commands"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; import getModels from "../utils/get-models"; import { getMainWindow } from "../main-window"; import settings from "electron-settings"; diff --git a/electron/commands/double-upscayl.ts b/electron/commands/double-upscayl.ts index 34a81e3..b38377c 100644 --- a/electron/commands/double-upscayl.ts +++ b/electron/commands/double-upscayl.ts @@ -14,7 +14,7 @@ import { } from "../utils/get-arguments"; import { modelsPath } from "../utils/get-resource-paths"; import logit from "../utils/logit"; -import { ELECTRON_COMMANDS } from "@common/electron-commands"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; import { DoubleUpscaylPayload } from "../../common/types/types"; import { ImageFormat } from "../types/types"; import showNotification from "../utils/show-notification"; diff --git a/electron/commands/get-models-list.ts b/electron/commands/get-models-list.ts index cce3e37..29499e9 100644 --- a/electron/commands/get-models-list.ts +++ b/electron/commands/get-models-list.ts @@ -1,4 +1,4 @@ -import { ELECTRON_COMMANDS } from "@common/electron-commands"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; import { getMainWindow } from "../main-window"; import { savedCustomModelsPath, diff --git a/electron/commands/image-upscayl.ts b/electron/commands/image-upscayl.ts index 9a7ce50..983b0e8 100644 --- a/electron/commands/image-upscayl.ts +++ b/electron/commands/image-upscayl.ts @@ -1,6 +1,6 @@ import fs from "fs"; import { modelsPath } from "../utils/get-resource-paths"; -import { ELECTRON_COMMANDS } from "@common/electron-commands"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; import { savedCustomModelsPath, setChildProcesses, diff --git a/electron/commands/paste-image.ts b/electron/commands/paste-image.ts new file mode 100644 index 0000000..27aa9e5 --- /dev/null +++ b/electron/commands/paste-image.ts @@ -0,0 +1,63 @@ +import { getMainWindow } from "../main-window"; +import logit from "../utils/logit"; +import fs from "fs"; +import { tmpdir, homedir } from "os"; +import path from "path"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; + +interface IClipboardFileParameters { + name: string; + extension: string; + size: number; + type: string; + encodedBuffer: string; +} + +const createTempFileFromClipboard = ( + appFolderPrefix: string, + inputFileParams: IClipboardFileParameters, + onSuccessCallback: (inputFilePath: string, outputFilePath: string) => void, + onErrorCallback: (error: Error) => void, +) => { + let tempDirectory = fs.mkdtempSync(path.join(tmpdir(), appFolderPrefix)); + let tempFilePath = path.join(tempDirectory, inputFileParams.name); + + fs.writeFile( + tempFilePath, + Buffer.from(inputFileParams.encodedBuffer, "base64"), + (err) => { + if (err) { + onErrorCallback(new Error("No permission to temp folder")); + return; + } + let homeDirectoryAsOutputFolder = homedir(); + onSuccessCallback(tempFilePath, homeDirectoryAsOutputFolder); + }, + ); +}; + +const pasteImage = (event, file: IClipboardFileParameters) => { + const mainWindow = getMainWindow(); + if (!mainWindow) return; + if (!file || !file.name || !file.encodedBuffer) return; + const appFolderPrefix = "upscayl"; + createTempFileFromClipboard( + appFolderPrefix, + file, + (imageFilePath, homeDirectory) => { + mainWindow.webContents.send(ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_SUCCESS, [ + imageFilePath, + homeDirectory, + ]); + }, + (error) => { + logit(error.message); + mainWindow.webContents.send( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_ERROR, + error.message, + ); + }, + ); +}; + +export default pasteImage; diff --git a/electron/index.ts b/electron/index.ts index 0bc3ebf..6f2ffd9 100644 --- a/electron/index.ts +++ b/electron/index.ts @@ -19,6 +19,7 @@ import doubleUpscayl from "./commands/double-upscayl"; import autoUpdate from "./commands/auto-update"; import { FEATURE_FLAGS } from "../common/feature-flags"; import settings from "electron-settings"; +import pasteImage from "./commands/paste-image"; // INITIALIZATION log.initialize({ preload: true }); @@ -94,6 +95,8 @@ ipcMain.on(ELECTRON_COMMANDS.FOLDER_UPSCAYL, batchUpscayl); ipcMain.on(ELECTRON_COMMANDS.DOUBLE_UPSCAYL, doubleUpscayl); +ipcMain.on(ELECTRON_COMMANDS.PASTE_IMAGE, pasteImage); + if (!FEATURE_FLAGS.APP_STORE_BUILD) { autoUpdater.on("update-downloaded", autoUpdate); } diff --git a/electron/utils/logit.ts b/electron/utils/logit.ts index b80a8f7..9ad1396 100644 --- a/electron/utils/logit.ts +++ b/electron/utils/logit.ts @@ -1,5 +1,5 @@ import log from "electron-log"; -import { ELECTRON_COMMANDS } from "@common/electron-commands"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; import { getMainWindow } from "../main-window"; const logit = (...args: any) => { diff --git a/renderer/components/main-content/index.tsx b/renderer/components/main-content/index.tsx index 44298b9..e3fcdfa 100644 --- a/renderer/components/main-content/index.tsx +++ b/renderer/components/main-content/index.tsx @@ -1,6 +1,6 @@ "use client"; import useLogger from "../hooks/use-logger"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { ELECTRON_COMMANDS } from "@common/electron-commands"; import { useAtomValue, useSetAtom } from "jotai"; import { @@ -165,40 +165,103 @@ const MainContent = ({ const handlePaste = (e: React.ClipboardEvent) => { 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); - } + if (e.clipboardData.files.length) { + const fileObject = e.clipboardData.files[0]; + const currentDate = new Date(Date.now()); + const currentTime = `${currentDate.getHours()}-${currentDate.getMinutes()}-${currentDate.getSeconds()}`; + const fileName = `${currentTime}-${fileObject.name}`; + const file = { + name: fileName, + extension: fileName.split(".").pop(), + size: fileObject.size, + type: fileObject.type.split("/")[0], + encodedBuffer: "", + }; + + logit( + "📋 Pasted file: ", + JSON.stringify({ + type: file.type, + name: file.name, + extension: file.extension, + }), + ); + + if ( + file.type === "image" && + VALID_IMAGE_FORMATS.includes(file.extension) + ) { + const reader = new FileReader(); + reader.onload = async (event) => { + const result = event.target?.result; + if (typeof result === "string") { + file.encodedBuffer = Buffer.from(result, "utf-8").toString( + "base64", + ); + } else if (result instanceof ArrayBuffer) { + file.encodedBuffer = Buffer.from(new Uint8Array(result)).toString( + "base64", + ); + } else { + logit("🚫 Invalid file pasted"); + toast({ + title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"), + description: t( + "ERRORS.INVALID_IMAGE_ERROR.CLIPBOARD_DESCRIPTION", + ), + }); + } + window.electron.send(ELECTRON_COMMANDS.PASTE_IMAGE, file); + }; + reader.readAsArrayBuffer(fileObject); + } else { + logit("🚫 Invalid file pasted"); + toast({ + title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"), + description: t("ERRORS.INVALID_IMAGE_ERROR.CLIPBOARD_DESCRIPTION"), + }); } - validateImagePath(filePath); + } else { + logit("🚫 Invalid file pasted"); + toast({ + title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"), + description: t("ERRORS.INVALID_IMAGE_ERROR.CLIPBOARD_DESCRIPTION"), + }); } }; + useEffect(() => { + const handlePasteEvent = (e) => handlePaste(e); + window.addEventListener("paste", handlePasteEvent); + window.electron.on( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_SUCCESS, + (_: any, output: string[]) => { + let [imageFilePath, homeDirectory] = output; + setImagePath(imageFilePath); + var dirname = getDirectoryFromPath(homeDirectory, false); + logit("🗂 Setting output path: ", dirname); + if (!FEATURE_FLAGS.APP_STORE_BUILD) { + if (!rememberOutputFolder) { + setOutputPath(dirname); + } + } + validateImagePath(imageFilePath); + }, + ); + window.electron.on( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_ERROR, + (_: any, error: string) => { + toast({ + title: t("ERRORS.NO_IMAGE_ERROR.TITLE"), + description: error, + }); + }, + ); + return () => { + window.removeEventListener("paste", handlePasteEvent); + }; + }, []); + return (
diff --git a/renderer/components/sidebar/index.tsx b/renderer/components/sidebar/index.tsx index ecd62a3..fa61194 100644 --- a/renderer/components/sidebar/index.tsx +++ b/renderer/components/sidebar/index.tsx @@ -24,10 +24,10 @@ import { import { useToast } from "@/components/ui/use-toast"; import UpscaylSteps from "./upscayl-tab/upscayl-steps"; import SettingsTab from "./settings-tab"; -import Footer from "../footer"; +import Footer from "../Footer"; import { NewsModal } from "../news-modal"; -import Tabs from "../tabs"; -import Header from "../header"; +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"; diff --git a/renderer/locales/en.json b/renderer/locales/en.json index 5041fa4..0694083 100644 --- a/renderer/locales/en.json +++ b/renderer/locales/en.json @@ -194,8 +194,9 @@ }, "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" + "DESCRIPTION": "Please select/paste an image with a valid extension like PNG, JPG, JPEG, JFIF or WEBP.", + "ADDITIONAL_DESCRIPTION": "Please drag and drop an image", + "CLIPBOARD_DESCRIPTION": "No Image file found in Clipboard to paste!" }, "NO_IMAGE_ERROR": { "TITLE": "No image selected", diff --git a/renderer/locales/es.json b/renderer/locales/es.json index 911d6ba..080701a 100644 --- a/renderer/locales/es.json +++ b/renderer/locales/es.json @@ -194,8 +194,9 @@ }, "INVALID_IMAGE_ERROR": { "TITLE": "Imagen inválida", - "DESCRIPTION": "Por favor, selecciona una imagen con una extensión válida como PNG, JPG, JPEG, JFIF o WEBP.", - "ADDITIONAL_DESCRIPTION": "Por favor, arrastra y suelta una imagen" + "DESCRIPTION": "Por favor, selecciona/pega una imagen con una extensión válida como PNG, JPG, JPEG, JFIF o WEBP.", + "ADDITIONAL_DESCRIPTION": "Por favor, arrastra y suelta una imagen", + "CLIPBOARD_DESCRIPTION": "¡No se encontró ningún archivo de imagen en el portapapeles para pegar!" }, "NO_IMAGE_ERROR": { "TITLE": "No se ha seleccionado imagen", diff --git a/renderer/locales/fr.json b/renderer/locales/fr.json index eb50172..37d8d0f 100644 --- a/renderer/locales/fr.json +++ b/renderer/locales/fr.json @@ -194,8 +194,9 @@ }, "INVALID_IMAGE_ERROR": { "TITLE": "Image invalide", - "DESCRIPTION": "Veuillez sélectionner une image avec une extension valide comme PNG, JPG, JPEG, JFIF ou WEBP.", - "ADDITIONAL_DESCRIPTION": "Veuillez glisser-déposer une image" + "DESCRIPTION": "Veuillez sélectionner/coller une image avec une extension valide comme PNG, JPG, JPEG, JFIF ou WEBP.", + "ADDITIONAL_DESCRIPTION": "Veuillez faire glisser et déposer une image", + "CLIPBOARD_DESCRIPTION": "Aucun fichier image trouvé dans le presse-papiers à coller !" }, "NO_IMAGE_ERROR": { "TITLE": "Aucune image sélectionnée", diff --git a/renderer/locales/ja.json b/renderer/locales/ja.json index b05d8fc..0fdea19 100644 --- a/renderer/locales/ja.json +++ b/renderer/locales/ja.json @@ -194,8 +194,9 @@ }, "INVALID_IMAGE_ERROR": { "TITLE": "無効な画像", - "DESCRIPTION": "PNG、JPG、JPEG、JFIF、またはWEBPなどの有効な拡張子を持つ画像を選択してください。", - "ADDITIONAL_DESCRIPTION": "画像をドラッグアンドドロップしてください" + "DESCRIPTION": "PNG、JPG、JPEG、JFIF、または WEBP のような有効な拡張子の画像を選択/貼り付けてください。", + "ADDITIONAL_DESCRIPTION": "画像をドラッグアンドドロップしてください", + "CLIPBOARD_DESCRIPTION": "クリップボードに貼り付ける画像ファイルが見つかりません!" }, "NO_IMAGE_ERROR": { "TITLE": "画像が選択されていません", diff --git a/renderer/locales/ru.json b/renderer/locales/ru.json index 00e58ce..90ecbdb 100644 --- a/renderer/locales/ru.json +++ b/renderer/locales/ru.json @@ -194,8 +194,9 @@ }, "INVALID_IMAGE_ERROR": { "TITLE": "Неверное изображение", - "DESCRIPTION": "Пожалуйста, выберите изображение с правильным расширением, таким как PNG, JPG, JPEG, JFIF или WEBP.", - "ADDITIONAL_DESCRIPTION": "Пожалуйста, перетащите изображение" + "DESCRIPTION": "Пожалуйста, выберите/вставьте изображение с допустимым расширением, таким как PNG, JPG, JPEG, JFIF или WEBP.", + "ADDITIONAL_DESCRIPTION": "Пожалуйста, перетащите и отпустите изображение", + "CLIPBOARD_DESCRIPTION": "Файл изображения не найден в буфере обмена для вставки!" }, "NO_IMAGE_ERROR": { "TITLE": "Изображение не выбрано", diff --git a/renderer/locales/zh.json b/renderer/locales/zh.json index 4cb4d7d..bc99ec9 100644 --- a/renderer/locales/zh.json +++ b/renderer/locales/zh.json @@ -194,8 +194,9 @@ }, "INVALID_IMAGE_ERROR": { "TITLE": "图片无效", - "DESCRIPTION": "请选择一个扩展名为 PNG、JPG、JPEG、JFIF 或 WEBP 的有效图片", - "ADDITIONAL_DESCRIPTION": "请拖放图片" + "DESCRIPTION": "请选择/粘貼一个扩展名为 PNG、JPG、JPEG、JFIF 或 WEBP 的有效图片", + "ADDITIONAL_DESCRIPTION": "請拖放一個圖像", + "CLIPBOARD_DESCRIPTION": "剪貼板中未找到可粘貼的圖像文件!" }, "NO_IMAGE_ERROR": { "TITLE": "未选择图片", diff --git a/renderer/next-env.d.ts b/renderer/next-env.d.ts index 4f11a03..a4a7b3f 100644 --- a/renderer/next-env.d.ts +++ b/renderer/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.