feat: surface detailed download progress
Changelog: - forward download progress from background/offscreen workers to content frames - add progress UI and button state management in Safari Books page script - emit structured progress callbacks and faster image fetching in SafariBooksDownloader - sync Firefox bundle with progress plumbing and concurrency improvements - ignore packaged extension archives Description: Expose progress events through the downloader so the content script can present real-time status, including chapter and asset fetching milestones. Added concurrency controls, page detection, and packaging telemetry, while keeping Firefox build behavior in sync and ignoring generated .xpi bundles.
This commit is contained in:
parent
edd0f9f809
commit
7a44902433
12 changed files with 895 additions and 106 deletions
|
|
@ -4,8 +4,6 @@
|
|||
let observerInstance = null;
|
||||
let ensureIntervalId = null;
|
||||
let initialized = false;
|
||||
const runtime = typeof browser !== "undefined" ? browser : chrome;
|
||||
const rawChrome = typeof chrome !== "undefined" ? chrome : null;
|
||||
|
||||
const TITLE_SELECTORS = [
|
||||
'[data-test="book-header-title"]',
|
||||
|
|
@ -105,56 +103,96 @@
|
|||
return button;
|
||||
}
|
||||
|
||||
function sendRuntimeMessage(message) {
|
||||
try {
|
||||
const result = runtime.runtime.sendMessage(message);
|
||||
if (result && typeof result.then === "function") {
|
||||
return result;
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
function setButtonState(button, state, message) {
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
switch (state) {
|
||||
case "busy":
|
||||
button.disabled = true;
|
||||
button.style.opacity = "0.7";
|
||||
break;
|
||||
case "idle":
|
||||
button.disabled = false;
|
||||
button.style.opacity = "";
|
||||
break;
|
||||
case "error":
|
||||
button.disabled = false;
|
||||
button.style.opacity = "";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (message) {
|
||||
button.textContent = message;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtime.runtime.sendMessage(message, (response) => {
|
||||
const err = rawChrome?.runtime?.lastError;
|
||||
if (err) {
|
||||
reject(new Error(err.message));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownload(button, bookId) {
|
||||
function setProgressMessage(wrapper, message) {
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
let status = wrapper.querySelector(".reilly-progress-status");
|
||||
if (!status) {
|
||||
return;
|
||||
}
|
||||
status.textContent = message;
|
||||
}
|
||||
|
||||
function handleDownload(button, bookId, wrapper, frameId) {
|
||||
if (!bookId) {
|
||||
return;
|
||||
}
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.style.opacity = "0.7";
|
||||
button.textContent = "Downloading…";
|
||||
setButtonState(button, "busy", "Downloading…");
|
||||
setProgressMessage(wrapper, "Starting download...");
|
||||
|
||||
sendRuntimeMessage({ type: "downloadBook", bookId })
|
||||
.then((response) => {
|
||||
if (response?.ok) {
|
||||
button.textContent = "Download started";
|
||||
} else {
|
||||
button.textContent = response?.error ? `Failed: ${response.error}` : "Download failed";
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const runtime = typeof browser !== "undefined" ? browser : chrome;
|
||||
const message = { type: "downloadBook", bookId, frameId };
|
||||
const sendPromise = runtime.runtime.sendMessage(message);
|
||||
|
||||
const handleResponse = (response) => {
|
||||
if (response?.ok) {
|
||||
setButtonState(button, "busy", "Download started");
|
||||
setProgressMessage(wrapper, "Packaging and saving EPUB...");
|
||||
} else {
|
||||
const errorMessage = response?.error ? `Failed: ${response.error}` : "Download failed";
|
||||
setButtonState(button, "error", errorMessage);
|
||||
setProgressMessage(wrapper, errorMessage);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
setButtonState(button, "idle");
|
||||
}, response?.ok ? 2000 : 4000);
|
||||
};
|
||||
|
||||
if (sendPromise && typeof sendPromise.then === "function") {
|
||||
sendPromise.then(handleResponse).catch((error) => {
|
||||
console.error("SafariBooks Downloader:", error);
|
||||
button.textContent = "Failed (extension error)";
|
||||
})
|
||||
.finally(() => {
|
||||
setButtonState(button, "error", "Failed (extension error)");
|
||||
setProgressMessage(wrapper, "Failed: extension error.");
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.disabled = false;
|
||||
button.style.opacity = "";
|
||||
}, 2000);
|
||||
setButtonState(button, "idle");
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
chrome.runtime.sendMessage(message, (response) => {
|
||||
const error = chrome.runtime.lastError;
|
||||
if (error) {
|
||||
console.error("SafariBooks Downloader:", error);
|
||||
setButtonState(button, "error", "Failed (extension error)");
|
||||
setProgressMessage(wrapper, "Failed: extension error.");
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
setButtonState(button, "idle");
|
||||
}, 4000);
|
||||
return;
|
||||
}
|
||||
handleResponse(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function injectButton() {
|
||||
|
|
@ -180,10 +218,6 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const button = createButton();
|
||||
button.dataset.bookId = bookId;
|
||||
button.addEventListener("click", () => handleDownload(button, bookId));
|
||||
|
||||
const wrapperId = `${BUTTON_ID}-wrapper`;
|
||||
let wrapper = document.getElementById(wrapperId);
|
||||
if (!wrapper) {
|
||||
|
|
@ -193,18 +227,133 @@
|
|||
wrapper.style.alignItems = "center";
|
||||
wrapper.style.gap = "12px";
|
||||
wrapper.style.marginLeft = "12px";
|
||||
|
||||
const button = createButton();
|
||||
button.dataset.bookId = bookId;
|
||||
button.addEventListener("click", () => handleDownload(button, bookId, wrapper));
|
||||
|
||||
wrapper.appendChild(button);
|
||||
const progressContainer = document.createElement("span");
|
||||
progressContainer.className = "reilly-progress";
|
||||
progressContainer.style.display = "inline-flex";
|
||||
progressContainer.style.flexDirection = "column";
|
||||
progressContainer.style.background = "#e2e8f0";
|
||||
progressContainer.style.borderRadius = "4px";
|
||||
progressContainer.style.padding = "6px 8px";
|
||||
progressContainer.style.minWidth = "160px";
|
||||
progressContainer.style.border = "1px solid #cbd5e1";
|
||||
progressContainer.style.fontSize = "0.75rem";
|
||||
progressContainer.style.color = "#1e293b";
|
||||
progressContainer.style.lineHeight = "1.4";
|
||||
|
||||
const progressLabel = document.createElement("span");
|
||||
progressLabel.textContent = "Progress";
|
||||
progressLabel.style.fontSize = "0.7rem";
|
||||
progressLabel.style.textTransform = "uppercase";
|
||||
progressLabel.style.fontWeight = "700";
|
||||
progressLabel.style.color = "#475569";
|
||||
progressLabel.style.letterSpacing = "0.05em";
|
||||
|
||||
const progressStatus = document.createElement("span");
|
||||
progressStatus.className = "reilly-progress-status";
|
||||
progressStatus.textContent = "Waiting";
|
||||
|
||||
progressContainer.appendChild(progressLabel);
|
||||
progressContainer.appendChild(progressStatus);
|
||||
wrapper.appendChild(progressContainer);
|
||||
|
||||
if (titleElement.parentElement) {
|
||||
titleElement.parentElement.insertBefore(wrapper, titleElement.nextSibling);
|
||||
} else {
|
||||
titleElement.insertAdjacentElement("afterend", wrapper);
|
||||
}
|
||||
} else if (!wrapper.contains(button)) {
|
||||
wrapper.appendChild(button);
|
||||
} else {
|
||||
const button = wrapper.querySelector(`#${BUTTON_ID}`) || createButton();
|
||||
button.dataset.bookId = bookId;
|
||||
if (!wrapper.contains(button)) {
|
||||
wrapper.appendChild(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
if (!message || message.type !== "download-progress") {
|
||||
return;
|
||||
}
|
||||
const wrapper = document.getElementById(`${BUTTON_ID}-wrapper`);
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
const button = wrapper.querySelector(`#${BUTTON_ID}`);
|
||||
const { payload = {} } = message;
|
||||
if (!payload.stage) {
|
||||
return;
|
||||
}
|
||||
switch (payload.stage) {
|
||||
case "starting":
|
||||
setButtonState(button, "busy", "Preparing...");
|
||||
setProgressMessage(wrapper, "Preparing download...");
|
||||
break;
|
||||
case "session-check":
|
||||
setProgressMessage(wrapper, "Verifying session...");
|
||||
break;
|
||||
case "session-ok":
|
||||
setProgressMessage(wrapper, "Session verified.");
|
||||
break;
|
||||
case "metadata":
|
||||
setProgressMessage(wrapper, payload.title ? `Metadata: ${payload.title}` : "Metadata loaded.");
|
||||
break;
|
||||
case "chapters-discovered":
|
||||
setProgressMessage(
|
||||
wrapper,
|
||||
`Chapters: ${payload.total ?? "?"}${payload.total === 1 ? "" : " chapters"}`
|
||||
);
|
||||
break;
|
||||
case "chapter-start":
|
||||
setProgressMessage(
|
||||
wrapper,
|
||||
`Chapter ${payload.index}/${payload.total}: ${payload.title || ""}`
|
||||
);
|
||||
break;
|
||||
case "images-start":
|
||||
setProgressMessage(
|
||||
wrapper,
|
||||
`Images: 0/${payload.total} (max ${payload.concurrency})`
|
||||
);
|
||||
break;
|
||||
case "images-progress":
|
||||
setProgressMessage(
|
||||
wrapper,
|
||||
`Images: ${payload.completed}/${payload.total} (${payload.elapsedSeconds ?? 0}s)`
|
||||
);
|
||||
break;
|
||||
case "images-complete":
|
||||
setProgressMessage(wrapper, `Images complete (${payload.completed})`);
|
||||
break;
|
||||
case "css-images-start":
|
||||
setProgressMessage(wrapper, `CSS assets: ${payload.total}`);
|
||||
break;
|
||||
case "packaging-complete":
|
||||
setProgressMessage(wrapper, `Packaging done (${payload.durationMs}ms)`);
|
||||
break;
|
||||
case "download-start":
|
||||
setProgressMessage(wrapper, "Saving EPUB...");
|
||||
break;
|
||||
case "complete":
|
||||
setButtonState(button, "idle", "Download Complete");
|
||||
setProgressMessage(wrapper, "Download complete." );
|
||||
setTimeout(() => {
|
||||
if (button) {
|
||||
button.textContent = "Download EPUB";
|
||||
}
|
||||
setProgressMessage(wrapper, "Waiting");
|
||||
}, 4000);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
function observeForTitle() {
|
||||
if (observerInstance) {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue