mirror of
https://github.com/andwati/kget-capture.git
synced 2026-10-18 11:12:57 +00:00
Route context-menu captures through chrome.downloads so they resolve Chrome's real filename (Content-Disposition, extension, etc.) via the same onDeterminingFilename path auto-capture already uses, and harden the native host against the bare-directory destDir KGet was misparsing as a filename.
134 lines
4.9 KiB
JavaScript
134 lines
4.9 KiB
JavaScript
const NATIVE_HOST = "com.kget_capture.host";
|
|
const MENU_ID = "kget-capture-download";
|
|
|
|
function updateBadge(enabled) {
|
|
chrome.action.setBadgeText({ text: enabled ? "ON" : "OFF" });
|
|
chrome.action.setBadgeBackgroundColor({ color: enabled ? "#2e7d32" : "#9e9e9e" });
|
|
}
|
|
|
|
async function refreshBadge() {
|
|
const { autoCaptureEnabled } = await chrome.storage.local.get({ autoCaptureEnabled: true });
|
|
updateBadge(autoCaptureEnabled);
|
|
}
|
|
|
|
// KGet is a network download manager -- it can't "download" a URL that's
|
|
// already local (or embedded in the page), so don't hand it these schemes.
|
|
// file: shows up when saving a page you navigated to directly (Ctrl+S on a
|
|
// file:// URL is still a "download" as far as chrome.downloads is
|
|
// concerned), and KGet's addTransfer rejects it with a confusing
|
|
// "protocol is not supported" dialog if we try.
|
|
const UNSUPPORTED_SCHEMES = ["blob:", "data:", "file:", "filesystem:"];
|
|
|
|
function isCapturableUrl(url) {
|
|
return !UNSUPPORTED_SCHEMES.some((scheme) => url.startsWith(scheme));
|
|
}
|
|
|
|
function sendToKGet(url, filename) {
|
|
chrome.runtime.sendNativeMessage(
|
|
NATIVE_HOST,
|
|
{ action: "addTransfer", url, filename: filename || null },
|
|
(response) => {
|
|
if (chrome.runtime.lastError) {
|
|
console.error("[kget-capture] native host error:", chrome.runtime.lastError.message);
|
|
return;
|
|
}
|
|
if (!response || !response.ok) {
|
|
console.error("[kget-capture] KGet reported failure:", response && response.error);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
chrome.runtime.onInstalled.addListener(async (details) => {
|
|
chrome.contextMenus.create({
|
|
id: MENU_ID,
|
|
title: "Download with KGet",
|
|
contexts: ["link", "image", "video", "audio"],
|
|
});
|
|
|
|
if (details.reason === "install") {
|
|
await chrome.storage.local.set({ autoCaptureEnabled: true });
|
|
}
|
|
refreshBadge();
|
|
});
|
|
|
|
// Downloads started from the context menu, tracked so captureDownload()
|
|
// still resolves/forwards their real filename even when auto-capture is
|
|
// toggled off -- a right-click "Download with KGet" should always work.
|
|
const pendingContextMenuDownloads = new Set();
|
|
|
|
chrome.contextMenus.onClicked.addListener((info) => {
|
|
if (info.menuItemId !== MENU_ID) return;
|
|
const url = info.linkUrl || info.srcUrl;
|
|
if (!url) return;
|
|
if (!isCapturableUrl(url)) {
|
|
console.warn("[kget-capture] cannot hand a local/embedded URL to KGet:", url);
|
|
return;
|
|
}
|
|
// Route through chrome.downloads instead of sending the raw URL straight
|
|
// to KGet -- this reuses onDeterminingFilename/captureDownload below to
|
|
// get Chrome's real resolved filename (Content-Disposition, etc.) instead
|
|
// of leaving KGet to guess one from the bare URL, which drops query
|
|
// strings and gets opaque/signed URLs wrong.
|
|
chrome.downloads.download({ url }, (downloadId) => {
|
|
if (chrome.runtime.lastError || downloadId === undefined) {
|
|
console.error(
|
|
"[kget-capture] couldn't start download to resolve filename, falling back to raw URL:",
|
|
chrome.runtime.lastError && chrome.runtime.lastError.message
|
|
);
|
|
sendToKGet(url, null);
|
|
return;
|
|
}
|
|
pendingContextMenuDownloads.add(downloadId);
|
|
});
|
|
});
|
|
|
|
// onDeterminingFilename (rather than onCreated) fires once Chrome has
|
|
// already resolved the real filename (from Content-Disposition or the URL),
|
|
// which is exactly what we need to hand KGet the correct name instead of an
|
|
// opaque URL path segment.
|
|
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
|
// We're about to cancel this download regardless, so Chrome's own choice
|
|
// doesn't matter -- just let it proceed so the event resolves promptly.
|
|
suggest();
|
|
captureDownload(item);
|
|
});
|
|
|
|
async function captureDownload(item) {
|
|
const isContextMenuCapture = pendingContextMenuDownloads.delete(item.id);
|
|
const { autoCaptureEnabled } = await chrome.storage.local.get({ autoCaptureEnabled: true });
|
|
if (!autoCaptureEnabled && !isContextMenuCapture) return;
|
|
|
|
const url = item.finalUrl || item.url;
|
|
if (!isCapturableUrl(url)) {
|
|
console.warn("[kget-capture] cannot hand a local/embedded URL to KGet, letting Chrome handle it:", url);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await chrome.downloads.cancel(item.id);
|
|
// erase() only clears the history entry -- it does NOT delete the
|
|
// partial file Chrome already wrote to disk. removeFile() does that.
|
|
await chrome.downloads.removeFile(item.id);
|
|
} catch (e) {
|
|
// Expected if Chrome finished the download before cancel() landed, or
|
|
// hadn't written any bytes yet -- not worth surfacing to the user.
|
|
}
|
|
try {
|
|
await chrome.downloads.erase({ id: item.id });
|
|
} catch (e) {
|
|
console.error("[kget-capture] failed to erase Chrome download entry:", e);
|
|
}
|
|
|
|
const filename = item.filename ? item.filename.split(/[\\/]/).pop() : null;
|
|
sendToKGet(url, filename);
|
|
}
|
|
|
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
if (area === "local" && "autoCaptureEnabled" in changes) {
|
|
updateBadge(changes.autoCaptureEnabled.newValue);
|
|
}
|
|
});
|
|
|
|
refreshBadge();
|