diff --git a/electron/file-operations.test.ts b/electron/file-operations.test.ts index d75dca3..88abf65 100644 --- a/electron/file-operations.test.ts +++ b/electron/file-operations.test.ts @@ -2,7 +2,13 @@ import { mkdir, mkdtemp, rm, utimes, writeFile } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { fileExists, getFileStats, readFileAsBuffer, scanFolder } from "./file-operations"; +import { + fileExists, + getFileStats, + getQuickLookThumbnail, + readFileAsBuffer, + scanFolder, +} from "./file-operations"; let dir: string; @@ -91,6 +97,13 @@ describe("readFileAsBuffer", () => { expect(Buffer.from(buffer).toString("utf8")).toBe("hello"); }); + describe("getQuickLookThumbnail", () => { + it("returns null when Quick Look is unavailable", async () => { + if (process.platform === "darwin") return; + expect(await getQuickLookThumbnail(join(dir, "file.pptx"))).toBeNull(); + }); + }); + it("rejects for a file that does not exist", async () => { await expect(readFileAsBuffer(join(dir, "missing.txt"))).rejects.toThrow(); }); diff --git a/electron/file-operations.ts b/electron/file-operations.ts index bc3d3ba..a37f5b4 100644 --- a/electron/file-operations.ts +++ b/electron/file-operations.ts @@ -1,10 +1,15 @@ +import { execFile } from "child_process"; import { shell } from "electron"; import { existsSync, readFileSync } from "fs"; -import { readdir, stat } from "fs/promises"; -import { join } from "path"; +import { mkdtemp, readdir, readFile, rm, stat } from "fs/promises"; +import { tmpdir } from "os"; +import { basename, join } from "path"; +import { promisify } from "util"; import { getFileType, isSupportedExtension } from "./file-types"; import type { FileItem, FileStats } from "./ipc-types"; +const execFileAsync = promisify(execFile); + // Error-handling policy: operations with no safe fallback value (scanFolder, // moveToTrash, readFileAsBuffer) log and rethrow, via logAndRethrow below, so // a failure never gets silently swallowed. getFileStats has a safe fallback @@ -91,3 +96,28 @@ export async function readFileAsBuffer(filePath: string): Promise { return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); }); } + +// Ask macOS Quick Look for a rendered thumbnail. A null result is intentional: +// unsupported platforms and files use the renderer's format-specific fallback. +export async function getQuickLookThumbnail(filePath: string): Promise { + if (process.platform !== "darwin") return null; + + const outputDirectory = await mkdtemp(join(tmpdir(), "quicktoss-quicklook-")); + try { + await execFileAsync("/usr/bin/qlmanage", ["-t", "-s", "1600", "-o", outputDirectory, filePath]); + const outputFiles = await readdir(outputDirectory); + const thumbnailName = outputFiles.find((name) => name.toLowerCase().endsWith(".png")); + if (!thumbnailName) { + console.warn(`Quick Look did not generate a thumbnail for ${basename(filePath)}`); + return null; + } + + const buffer = await readFile(join(outputDirectory, thumbnailName)); + return new Uint8Array(buffer).slice().buffer; + } catch (error) { + console.warn(`Quick Look preview unavailable for ${basename(filePath)}:`, error); + return null; + } finally { + await rm(outputDirectory, { recursive: true, force: true }); + } +} diff --git a/electron/file-types.test.ts b/electron/file-types.test.ts index 80f2aa6..29ca7fa 100644 --- a/electron/file-types.test.ts +++ b/electron/file-types.test.ts @@ -17,6 +17,8 @@ describe("getFileType", () => { it("classifies documents", () => { expect(getFileType(".pdf")).toBe("document"); expect(getFileType(".docx")).toBe("document"); + expect(getFileType(".pptx")).toBe("document"); + expect(getFileType(".xlsx")).toBe("document"); }); it("classifies video", () => { @@ -40,6 +42,8 @@ describe("getFileType", () => { describe("isSupportedExtension", () => { it("accepts a supported extension", () => { expect(isSupportedExtension(".jpg")).toBe(true); + expect(isSupportedExtension(".pptx")).toBe(true); + expect(isSupportedExtension(".xlsx")).toBe(true); }); it("rejects an unsupported one", () => { diff --git a/electron/file-types.ts b/electron/file-types.ts index 393b27e..943953e 100644 --- a/electron/file-types.ts +++ b/electron/file-types.ts @@ -25,6 +25,8 @@ export const DOCUMENT_EXTENSIONS = [ ".yml", ".doc", ".docx", + ".pptx", + ".xlsx", ] as const; export const VIDEO_EXTENSIONS = [".mp4", ".mov", ".avi"] as const; diff --git a/electron/ipc-types.ts b/electron/ipc-types.ts index 91ee87d..5df06d4 100644 --- a/electron/ipc-types.ts +++ b/electron/ipc-types.ts @@ -40,6 +40,7 @@ export interface ElectronAPI { getFileStats: (filePath: string) => Promise; fileExists: (filePath: string) => Promise; readFileAsBuffer: (filePath: string) => Promise; + getQuickLookThumbnail: (filePath: string) => Promise; getSettings: () => Promise; saveSettings: (settings: AppSettings) => Promise; onUpdateStatus: (callback: (status: UpdateStatus) => void) => () => void; @@ -56,6 +57,7 @@ export const CHANNELS = { getFileStats: "get-file-stats", fileExists: "file-exists", readFileAsBuffer: "read-file-as-buffer", + getQuickLookThumbnail: "get-quick-look-thumbnail", getSettings: "get-settings", saveSettings: "save-settings", onUpdateStatus: "update-status", diff --git a/electron/main.ts b/electron/main.ts index 7683835..f44465a 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -3,6 +3,7 @@ import { join } from "path"; import { fileExists, getFileStats, + getQuickLookThumbnail, moveToTrash, readFileAsBuffer, scanFolder, @@ -113,6 +114,7 @@ class QuickTossApp { getFileStats, fileExists, readFileAsBuffer, + getQuickLookThumbnail, getSettings: this.settingsStore.getSettings, saveSettings: this.settingsStore.saveSettings, openReleasePage: this.updater.openReleasePage, diff --git a/electron/preload.ts b/electron/preload.ts index 601c34f..16b793f 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -12,6 +12,8 @@ contextBridge.exposeInMainWorld("electronAPI", { getFileStats: (filePath: string) => ipcRenderer.invoke(CHANNELS.getFileStats, filePath), fileExists: (filePath: string) => ipcRenderer.invoke(CHANNELS.fileExists, filePath), readFileAsBuffer: (filePath: string) => ipcRenderer.invoke(CHANNELS.readFileAsBuffer, filePath), + getQuickLookThumbnail: (filePath: string) => + ipcRenderer.invoke(CHANNELS.getQuickLookThumbnail, filePath), getSettings: () => ipcRenderer.invoke(CHANNELS.getSettings), saveSettings: (settings: AppSettings) => ipcRenderer.invoke(CHANNELS.saveSettings, settings), onUpdateStatus: (callback: (status: UpdateStatus) => void) => { diff --git a/package-lock.json b/package-lock.json index 370404a..a769ce4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,15 @@ "clsx": "^1.2.1", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.8.9", + "heic2any": "^0.0.4", + "jszip": "^3.10.2", "lucide-react": "^0.263.0", + "mammoth": "^1.12.2", "pdfjs-dist": "^3.11.174", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-pdf": "^6.2.2" + "react-pdf": "^6.2.2", + "xlsx": "^0.18.5" }, "devDependencies": { "@biomejs/biome": "^2.2.4", @@ -3311,7 +3315,6 @@ "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3331,6 +3334,15 @@ "license": "ISC", "optional": true }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -3789,7 +3801,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4177,6 +4188,19 @@ "optional": true, "peer": true }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -4360,6 +4384,15 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4551,7 +4584,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/crc": { @@ -4569,9 +4601,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -4856,6 +4886,12 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dir-compare": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", @@ -5013,6 +5049,15 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5749,6 +5794,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -6178,6 +6232,12 @@ "node": ">= 0.4" } }, + "node_modules/heic2any": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/heic2any/-/heic2any-0.0.4.tgz", + "integrity": "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==", + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -6333,6 +6393,12 @@ ], "license": "BSD-3-Clause" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -6359,7 +6425,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "devOptional": true, "license": "ISC" }, "node_modules/ini": { @@ -6467,9 +6532,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isbinaryfile": { "version": "5.0.6", @@ -6756,6 +6819,48 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6822,6 +6927,15 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -7188,6 +7302,17 @@ "loose-envify": "cli.js" } }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/loupe": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", @@ -7291,6 +7416,60 @@ "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" } }, + "node_modules/mammoth": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.2.tgz", + "integrity": "sha512-MH2vkgafD/2MYUaEOtoXLKrHQZ7yLYTHGQgyLNtYZHiUxU1K1QQ+8qMFquDAfhBm06wSGSws3J+Q9QTsKtR44g==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mammoth/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/mammoth/node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, + "node_modules/mammoth/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/mammoth/node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -7766,6 +7945,12 @@ "wrappy": "1" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, "node_modules/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -7783,6 +7968,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -7800,7 +7991,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8225,9 +8415,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/progress": { "version": "2.0.3", @@ -8845,6 +9033,12 @@ "license": "ISC", "optional": true }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/sharp": { "version": "0.35.3", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", @@ -9099,6 +9293,18 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -9844,6 +10050,12 @@ "node": ">=14.17" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", @@ -9923,7 +10135,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "devOptional": true, "license": "MIT" }, "node_modules/verror": { @@ -10231,6 +10442,24 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -10275,6 +10504,27 @@ "devOptional": true, "license": "ISC" }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/package.json b/package.json index b0c7eae..862d636 100644 --- a/package.json +++ b/package.json @@ -79,11 +79,15 @@ "clsx": "^1.2.1", "electron-squirrel-startup": "^1.0.1", "electron-updater": "^6.8.9", + "heic2any": "^0.0.4", + "jszip": "^3.10.2", "lucide-react": "^0.263.0", + "mammoth": "^1.12.2", "pdfjs-dist": "^3.11.174", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-pdf": "^6.2.2" + "react-pdf": "^6.2.2", + "xlsx": "^0.18.5" }, "build": { "appId": "com.quicktoss.app", diff --git a/src/components/DocxPreview.tsx b/src/components/DocxPreview.tsx new file mode 100644 index 0000000..c51cc6f --- /dev/null +++ b/src/components/DocxPreview.tsx @@ -0,0 +1,52 @@ +import mammoth from "mammoth"; +import { useEffect, useState } from "react"; + +interface DocxPreviewProps { + buffer: ArrayBuffer; + onError: () => void; +} + +const DocxPreview = ({ buffer, onError }: DocxPreviewProps) => { + const [html, setHtml] = useState(""); + const [error, setError] = useState(false); + + useEffect(() => { + let cancelled = false; + mammoth + .convertToHtml({ arrayBuffer: buffer }) + .then(({ value }) => { + if (!cancelled) setHtml(value); + }) + .catch((conversionError) => { + console.error("Error converting DOCX file:", conversionError); + if (!cancelled) { + setError(true); + onError(); + } + }); + return () => { + cancelled = true; + }; + }, [buffer, onError]); + + if (error) return null; + if (!html) { + return
; + } + + return ( +