Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"private": true,
"dependencies": {
"browserslist": "^4.28.1",
"client-zip": "^2.5.0",
"css-prop-parser": "^0.1.0",
"image-extensions": "^1.1.0",
"modern-tar": "^0.7.2",
Expand Down
17 changes: 11 additions & 6 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {createDialog} from "./lib/popup-dialog.js";
import {compileStringTemplate} from "./lib/string-template.js";
import {expandEnv, expandDate} from "./lib/expand-env.mjs";
import {getTarPacker} from "./lib/tar-packer.js";
import {getZipPacker} from "./lib/zip-packer.js";

const PACKERS = {
tar: getTarPacker,
zip: getZipPacker
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid automated semicolon insertion (94% of all statements in the enclosing script have an explicit semicolon).

Suggested change
}
};

Copilot uses AI. Check for mistakes.

const MENU_ACTIONS = {
PICK_FROM_CURRENT_TAB: {
Expand Down Expand Up @@ -532,12 +538,11 @@ function getRawPacker() {
}

function getPacker() {
if (pref.get("packer") === "tar") {
try {
return getTarPacker();
} catch (err) {
console.warn(err);
}
try {
const getPacker = PACKERS[pref.get("packer")];
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When pref.get("packer") returns "none" or any other value not in the PACKERS object, getPacker on line 542 will be undefined, causing line 543 to throw "undefined is not a function". The previous implementation handled this by only checking for "tar" explicitly and falling back to getRawPacker() for all other values including "none". Add a check like if (!getPacker) return getRawPacker(); after line 542 to preserve this behavior.

Suggested change
const getPacker = PACKERS[pref.get("packer")];
const getPacker = PACKERS[pref.get("packer")];
if (!getPacker) {
return getRawPacker();
}

Copilot uses AI. Check for mistakes.
return getPacker();
} catch (err) {
console.warn(err);
}
return getRawPacker();
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/pref.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const DEFAULT = {
isolateTabs: false,
filenameConflictAction: "uniquify",
lowResPreview: true,
packer: IS_ANDROID ? "tar" : "none",
packer: IS_ANDROID ? "zip" : "none",
saveAs: false,
selectByDefault: true,
singleClick: false,
Expand Down
6 changes: 3 additions & 3 deletions src/lib/tar-packer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function getTarPacker() {
if (typeof navigator.storage?.getDirectory !== 'function') {
throw new Error('File System Access API is not supported in this browser.');
}
const tarName = `temp-${Date.now()}-${Math.random().toString(16).slice(2)}.tar`;
const tempName = `temp-${Date.now()}-${Math.random().toString(16).slice(2)}.tar`;
const {readable, controller} = createTarPacker();
let pendingPipe = null;
return {
Expand All @@ -17,7 +17,7 @@ export function getTarPacker() {

async function prepare() {
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle(tarName, {create: true});
const handle = await root.getFileHandle(tempName, {create: true});
const writable = await handle.createWritable();
pendingPipe = readable.pipeTo(writable);
}
Expand All @@ -35,6 +35,6 @@ export function getTarPacker() {
async function save() {
controller.finalize();
await pendingPipe;
return {tarName, downloadName: `image-picka-${new Date().toISOString()}.tar`};
return {tempName, downloadName: `image-picka-${new Date().toISOString()}.tar`};
}
}
51 changes: 51 additions & 0 deletions src/lib/zip-packer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {makeZip} from "client-zip"
import {defer} from "./defer.js";

const EXT = "zip";

export function getZipPacker() {
if (typeof navigator.storage?.getDirectory !== 'function') {
throw new Error('File System Access API is not supported in this browser.');
}
const tempName = `temp-${Date.now()}-${Math.random().toString(16).slice(2)}.${EXT}`;
let pendingPipe = null;
let itemReady = defer();
let zipReady = defer();
return {
prepare,
pack,
save,
waitResponse: true,
singleThread: true,
}

async function prepare() {
const root = await navigator.storage.getDirectory();
const handle = await root.getFileHandle(tempName, {create: true});
const writable = await handle.createWritable();
const zipStream = makeZip(async function*() {
let item;
while ((item = await itemReady.promise)) {
yield item;
zipReady.resolve();
zipReady = defer();
}
}());
pendingPipe = zipStream.pipeTo(writable);
}

async function pack({blob, filename}) {
itemReady.resolve({
name: filename,
input: blob,
Comment on lines +38 to +40
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike tar-packer.js which explicitly awaits blob.stream().pipeTo(fileStream), this implementation doesn't explicitly wait for the blob to be consumed. The synchronization relies on client-zip's stream backpressure to prevent memory issues. While this should work correctly, it's a different pattern from tar-packer and may behave differently under high load or with very large files. Consider documenting this design choice or verifying the backpressure behavior if issues arise.

Suggested change
itemReady.resolve({
name: filename,
input: blob,
// Pass a ReadableStream to client-zip so that blob data is consumed
// via the browser's streaming backpressure rather than as a whole Blob.
itemReady.resolve({
name: filename,
input: blob.stream(),

Copilot uses AI. Check for mistakes.
});
itemReady = defer();
await zipReady.promise;
}

async function save() {
itemReady.resolve(null); // signal end of items
await pendingPipe;
return {tempName, downloadName: `image-picka-${new Date().toISOString()}.${EXT}`};
}
}
1 change: 1 addition & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ const root = createUI({
label: _("optionPackerLabel"),
options: {
none: _("optionPackerNone"),
zip: _("optionPackerZip"),
tar: _("optionPackerTar")
},
help: _("optionPackerHelp")
Expand Down
8 changes: 4 additions & 4 deletions src/picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,14 @@ function init({tabs: originalTabs, env}) {
env,
batchId: BATCH_ID
});
if (result?.tarName) {
if (result?.tempName) {
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle(result.tarName);
const fileHandle = await root.getFileHandle(result.tempName);
const file = await fileHandle.getFile();
const url = URL.createObjectURL(file);
const a = document.createElement("a");
a.href = url;
a.download = result.downloadName || result.tarName;
a.download = result.downloadName || result.tempName;
a.style.display = "none";
document.body.appendChild(a);
a.click();
Expand All @@ -195,7 +195,7 @@ function init({tabs: originalTabs, env}) {
await timeout(1000);
URL.revokeObjectURL(url);
// NOTE: can't remove file until download complete
// await root.removeEntry(result.tarName);
// await root.removeEntry(result.tempName);
}
if (!IS_ANDROID) {
// NOTE: closing the tab will close the download confirmation in Firefox Android
Expand Down
3 changes: 3 additions & 0 deletions src/static/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@
},
"optionPackerTar": {
"message": "TAR archive"
},
"optionPackerZip": {
"message": "ZIP archive"
},
"optionPreviewMaxHeightUpperBoundLabel": {
"message": "The maximum value of the thumbnail size slider",
Expand Down