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/common/image-formats.ts b/common/image-formats.ts new file mode 100644 index 0000000..4d489d3 --- /dev/null +++ b/common/image-formats.ts @@ -0,0 +1 @@ +export const imageFormats = ["png", "jpg", "jpeg", "webp"] as const; diff --git a/electron/commands/paste-image.ts b/electron/commands/paste-image.ts new file mode 100644 index 0000000..fe0ea1d --- /dev/null +++ b/electron/commands/paste-image.ts @@ -0,0 +1,61 @@ +import { getMainWindow } from "../main-window"; +import logit from "../utils/logit"; +import fs from "fs"; +import path from "path"; +import { ELECTRON_COMMANDS } from "../../common/electron-commands"; +import { ImageFormat } from "../types/types"; +import { imageFormats } from "../../common/image-formats"; + +interface IClipboardFileParameters { + name: string; + path: string; + extension: ImageFormat; + size: number; + type: string; + encodedBuffer: string; +} + +const isImageFormatValid = (format: string): format is ImageFormat => { + return (imageFormats as readonly string[]).includes(format); +}; + +const createTempFileFromClipboard = async ( + inputFileParams: IClipboardFileParameters, +): Promise => { + const tempFilePath = path.join(inputFileParams.path, inputFileParams.name); + const buffer = Buffer.from(inputFileParams.encodedBuffer, "base64"); + + await fs.promises.writeFile(tempFilePath, buffer); + return tempFilePath; +}; + +const pasteImage = async ( + event: Electron.IpcMainEvent, + file: IClipboardFileParameters, +) => { + const mainWindow = getMainWindow(); + if (!mainWindow) return; + if (!file || !file.name || !file.encodedBuffer) return; + if (isImageFormatValid(file.extension)) { + try { + const imageFilePath = await createTempFileFromClipboard(file); + mainWindow.webContents.send( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_SUCCESS, + imageFilePath, + ); + } catch (error: any) { + logit(error.message); + mainWindow.webContents.send( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_ERROR, + error.message, + ); + } + } else { + mainWindow.webContents.send( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_ERROR, + "Unsupported Image Format", + ); + } +}; + +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/types/types.d.ts b/electron/types/types.d.ts index 8a7f4c0..3883004 100644 --- a/electron/types/types.d.ts +++ b/electron/types/types.d.ts @@ -1 +1,3 @@ -export type ImageFormat = "png" | "jpg" | "webp"; +import { imageFormats } from "../../common/image-formats"; + +export type ImageFormat = (typeof imageFormats)[number]; diff --git a/renderer/components/main-content/index.tsx b/renderer/components/main-content/index.tsx index a6defdc..e158abf 100644 --- a/renderer/components/main-content/index.tsx +++ b/renderer/components/main-content/index.tsx @@ -1,8 +1,8 @@ "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 { useAtom, useAtomValue } from "jotai"; import { batchModeAtom, lensSizeAtom, @@ -63,7 +63,7 @@ const MainContent = ({ const { toast } = useToast(); const version = useUpscaylVersion(); - const setOutputPath = useSetAtom(savedOutputPathAtom); + const [outputPath, setOutputPath] = useAtom(savedOutputPathAtom); const progress = useAtomValue(progressAtom); const batchMode = useAtomValue(batchModeAtom); @@ -163,45 +163,108 @@ const MainContent = ({ }; const handlePaste = (e: React.ClipboardEvent) => { - console.log("📋 Pasted: ", e); - resetImagePaths(); e.preventDefault(); - const items = e.clipboardData.items; - const files = e.clipboardData.files; - console.log("🚀 => files:", files); + if (outputPath) { + resetImagePaths(); + 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 = `.temp-${currentTime}-${fileObject.name || "image"}`; + const file = { + name: fileName, + path: outputPath, + extension: fileName.split(".").pop() as ImageFormat, + size: fileObject.size, + type: fileObject.type.split("/")[0], + encodedBuffer: "", + }; - 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) - .toLowerCase() as ImageFormat; - logit("📋 Pasted file: ", JSON.stringify({ type, filePath, extension })); - if (!type.includes("image") && !VALID_IMAGE_FORMATS.includes(extension)) { - toast({ - title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"), - description: t("ERRORS.INVALID_IMAGE_ERROR.ADDITIONAL_DESCRIPTION"), - }); - } else { - setImagePath(filePath); - const dirname = getDirectoryFromPath(filePath); - logit("🗂 Setting output path: ", dirname); - if (!FEATURE_FLAGS.APP_STORE_BUILD) { - if (!rememberOutputFolder) { - setOutputPath(dirname); + logit( + "📋 Pasted file: ", + JSON.stringify({ + name: file.name, + path: file.path, + 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"), + }); } + } else { + logit("🚫 Invalid file pasted"); + toast({ + title: t("ERRORS.INVALID_IMAGE_ERROR.TITLE"), + description: t("ERRORS.INVALID_IMAGE_ERROR.CLIPBOARD_DESCRIPTION"), + }); } - validateImagePath(filePath); + } else { + toast({ + title: t("ERRORS.NO_OUTPUT_FOLDER_ERROR.TITLE"), + description: t("ERRORS.NO_OUTPUT_FOLDER_ERROR.DESCRIPTION"), + }); } }; + useEffect(() => { + // Events + const handlePasteEvent = (e) => handlePaste(e); + const handlePasteImageSaveSuccess = (_: any, imageFilePath: string) => { + setImagePath(imageFilePath); + validateImagePath(imageFilePath); + }; + const handlePasteImageSaveError = (_: any, error: string) => { + toast({ + title: t("ERRORS.NO_IMAGE_ERROR.TITLE"), + description: error, + }); + }; + window.addEventListener("paste", handlePasteEvent); + window.electron.on( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_SUCCESS, + handlePasteImageSaveSuccess, + ); + window.electron.on( + ELECTRON_COMMANDS.PASTE_IMAGE_SAVE_ERROR, + handlePasteImageSaveError, + ); + return () => { + window.removeEventListener("paste", handlePasteEvent); + }; + }, [t, outputPath]); + return (
diff --git a/renderer/components/main-content/instructions-card.tsx b/renderer/components/main-content/instructions-card.tsx index 0283035..f2b0db2 100644 --- a/renderer/components/main-content/instructions-card.tsx +++ b/renderer/components/main-content/instructions-card.tsx @@ -19,6 +19,8 @@ function InstructionsCard({ version, batchMode }) { ) : (

{t("APP.RIGHT_PANE_INFO.SELECT_IMAGES_DESCRIPTION")} +
+ {t("APP.RIGHT_PANE_INFO.PASTE_IMAGE_DESCRIPTION")}

)}

Upscayl v{version}

diff --git a/renderer/locales/en.json b/renderer/locales/en.json index 5041fa4..cad561d 100644 --- a/renderer/locales/en.json +++ b/renderer/locales/en.json @@ -147,7 +147,8 @@ "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." + "SELECT_IMAGES_DESCRIPTION": "Select or drag and drop a PNG, JPG, JPEG or WEBP image.", + "PASTE_IMAGE_DESCRIPTION": "Hit Ctrl + V or Cmd + V to Paste image from Clipboard" }, "PROGRESS": { "PROCESSING_TITLE": "Processing the image...", @@ -192,10 +193,15 @@ "GENERIC_ERROR": { "TITLE": "Error" }, + "NO_OUTPUT_FOLDER_ERROR": { + "TITLE": "Set Output Folder", + "DESCRIPTION": "Please select an output folder first" + }, "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..bbd9c03 100644 --- a/renderer/locales/es.json +++ b/renderer/locales/es.json @@ -147,7 +147,8 @@ "SELECT_FOLDER": "Selecciona una carpeta para aumentar", "SELECT_IMAGE": "Selecciona una imagen para aumentar", "SELECT_FOLDER_DESCRIPTION": "Asegúrate de que la carpeta no contenga nada excepto imágenes PNG, JPG, JPEG y WEBP.", - "SELECT_IMAGES_DESCRIPTION": "Selecciona o arrastra y suelta una imagen PNG, JPG, JPEG o WEBP." + "SELECT_IMAGES_DESCRIPTION": "Selecciona o arrastra y suelta una imagen PNG, JPG, JPEG o WEBP.", + "PASTE_IMAGE_DESCRIPTION": "Presiona Ctrl + V o Cmd + V para pegar la imagen desde el portapapeles" }, "PROGRESS": { "PROCESSING_TITLE": "Procesando la imagen...", @@ -192,10 +193,15 @@ "GENERIC_ERROR": { "TITLE": "Error" }, + "NO_OUTPUT_FOLDER_ERROR": { + "TITLE": "Establecer carpeta de salida", + "DESCRIPTION": "Por favor, selecciona primero una carpeta de salida" + }, "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..6aea5ce 100644 --- a/renderer/locales/fr.json +++ b/renderer/locales/fr.json @@ -147,7 +147,8 @@ "SELECT_FOLDER": "Sélectionnez un dossier à suréchantillonner", "SELECT_IMAGE": "Sélectionnez une image à suréchantillonner", "SELECT_FOLDER_DESCRIPTION": "Assurez-vous que le dossier ne contient rien d'autre que des images PNG, JPG, JPEG et WEBP.", - "SELECT_IMAGES_DESCRIPTION": "Sélectionnez ou glissez-déposez une image PNG, JPG, JPEG ou WEBP." + "SELECT_IMAGES_DESCRIPTION": "Sélectionnez ou glissez-déposez une image PNG, JPG, JPEG ou WEBP.", + "PASTE_IMAGE_DESCRIPTION": "Appuyez sur Ctrl + V ou Cmd + V pour coller l'image depuis le presse-papiers" }, "PROGRESS": { "PROCESSING_TITLE": "Traitement de l'image...", @@ -192,10 +193,15 @@ "GENERIC_ERROR": { "TITLE": "Erreur" }, + "NO_OUTPUT_FOLDER_ERROR": { + "TITLE": "Définir le dossier de sortie", + "DESCRIPTION": "Veuillez d'abord sélectionner un dossier de sortie" + }, "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..b385f78 100644 --- a/renderer/locales/ja.json +++ b/renderer/locales/ja.json @@ -147,7 +147,8 @@ "SELECT_FOLDER": "Upscaylするフォルダを選択", "SELECT_IMAGE": "Upscaylする画像を選択", "SELECT_FOLDER_DESCRIPTION": "フォルダにはPNG、JPG、JPEG、WEBPの画像以外のものが含まれていないことを確認してください。", - "SELECT_IMAGES_DESCRIPTION": "PNG、JPG、JPEG、またはWEBP画像を選択するか、ドラッグアンドドロップしてください。" + "SELECT_IMAGES_DESCRIPTION": "PNG、JPG、JPEG、またはWEBP画像を選択するか、ドラッグアンドドロップしてください。", + "PASTE_IMAGE_DESCRIPTION": "Ctrl + V または Cmd + V を押してクリップボードから画像を貼り付けます" }, "PROGRESS": { "PROCESSING_TITLE": "画像を処理中...", @@ -192,10 +193,15 @@ "GENERIC_ERROR": { "TITLE": "エラー" }, + "NO_OUTPUT_FOLDER_ERROR": { + "TITLE": "出力フォルダを設定", + "DESCRIPTION": "まず出力フォルダを選択してください" + }, "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..ad03a22 100644 --- a/renderer/locales/ru.json +++ b/renderer/locales/ru.json @@ -147,7 +147,8 @@ "SELECT_FOLDER": "Выберите папку для увеличения", "SELECT_IMAGE": "Выберите изображение для увеличения", "SELECT_FOLDER_DESCRIPTION": "Убедитесь, что в папке нет ничего, кроме изображений PNG, JPG, JPEG и WEBP.", - "SELECT_IMAGES_DESCRIPTION": "Выберите или перетащите изображение в формате PNG, JPG, JPEG или WEBP." + "SELECT_IMAGES_DESCRIPTION": "Выберите или перетащите изображение в формате PNG, JPG, JPEG или WEBP.", + "PASTE_IMAGE_DESCRIPTION": "Нажмите Ctrl + V или Cmd + V, чтобы вставить изображение из буфера обмена" }, "PROGRESS": { "PROCESSING_TITLE": "Обработка изображения...", @@ -192,10 +193,15 @@ "GENERIC_ERROR": { "TITLE": "Ошибка" }, + "NO_OUTPUT_FOLDER_ERROR": { + "TITLE": "Установить папку вывода", + "DESCRIPTION": "Пожалуйста, сначала выберите папку вывода" + }, "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..9bbe3f6 100644 --- a/renderer/locales/zh.json +++ b/renderer/locales/zh.json @@ -147,7 +147,8 @@ "SELECT_FOLDER": "选择要增强的文件夹", "SELECT_IMAGE": "选择要增强的图片", "SELECT_FOLDER_DESCRIPTION": "文件夹中只支持 PNG、JPG、JPEG 和 WEBP 图片。", - "SELECT_IMAGES_DESCRIPTION": "选择或拖放 PNG、JPG、JPEG 或 WEBP 图片。" + "SELECT_IMAGES_DESCRIPTION": "选择或拖放 PNG、JPG、JPEG 或 WEBP 图片。", + "PASTE_IMAGE_DESCRIPTION": "按 Ctrl + V 或 Cmd + V 来粘贴图像" }, "PROGRESS": { "PROCESSING_TITLE": "正在处理图片...", @@ -192,10 +193,15 @@ "GENERIC_ERROR": { "TITLE": "错误" }, + "NO_OUTPUT_FOLDER_ERROR": { + "TITLE": "选择输出文件夹", + "DESCRIPTION": "请先选择一个输出文件夹" + }, "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": "未选择图片",