mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-09-22 17:16:19 -07:00
- Add backend selection, setup gating, Anki integration, and external host support - Add launcher flags, documentation, packaging, and focused tests - Open on-demand overlay modals on the first attempt
156 lines
5.7 KiB
JavaScript
156 lines
5.7 KiB
JavaScript
// File-URL access is controlled by the user in browser-owned extension details.
|
||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
||
export function createLocalFileAccessController({
|
||
document,
|
||
container,
|
||
chromeApi = globalThis.browser ?? globalThis.chrome,
|
||
onDismiss = null,
|
||
}) {
|
||
const window = document.defaultView;
|
||
const firefox = (() => {
|
||
try {
|
||
return new URL(chromeApi.runtime.getURL("")).protocol === "moz-extension:";
|
||
} catch {
|
||
return false;
|
||
}
|
||
})();
|
||
// Setup shows this section with a dismiss action; Settings shows it without.
|
||
const surface = onDismiss ? "setup" : "Settings";
|
||
// Firefox refuses tabs.create for about:addons and has no API that opens the
|
||
// Add-ons Manager for a full-tab options page, so it gets the exact manual
|
||
// path instead of an Open button.
|
||
const COPY = {
|
||
firefox: {
|
||
browserName: "Firefox",
|
||
description: "allow Hachidori to access local files in Firefox’s Add-ons Manager.",
|
||
instruction: "Open about:addons, choose Hachidori, open the Permissions and data tab,"
|
||
+ " turn on “Access local files on your computer”, then return to this tab.",
|
||
reopen: null,
|
||
settingsUrl: null,
|
||
},
|
||
chrome: {
|
||
browserName: "Chrome",
|
||
description: "enable “Allow access to file URLs” in Hachidori’s extension settings.",
|
||
instruction: "Turn on “Allow access to file URLs”, then return to this tab.",
|
||
reopen: "On the extension details page, open Extension options",
|
||
settingsUrl: () => `chrome://extensions/?id=${chromeApi.runtime.id}`,
|
||
},
|
||
}[firefox ? "firefox" : "chrome"];
|
||
const { browserName } = COPY;
|
||
const canOpenSettings = COPY.settingsUrl !== null;
|
||
const make = (tag, id, text, className = "") => {
|
||
const node = document.createElement(tag);
|
||
node.id = id;
|
||
node.textContent = text;
|
||
node.className = className;
|
||
return node;
|
||
};
|
||
const heading = make("h2", "local-file-heading", "Read saved pages too");
|
||
const description = make("p", "local-file-description",
|
||
`To look up Japanese in HTML files opened from your computer, ${COPY.description}`);
|
||
const instruction = make("p", "local-file-instruction", COPY.instruction);
|
||
let recovery = null;
|
||
if (canOpenSettings) {
|
||
const returnPath = onDismiss ? ", then choose Resume setup." : " to return.";
|
||
recovery = make("p", "local-file-recovery",
|
||
`${browserName} may close ${surface} when it reloads Hachidori. ${COPY.reopen}${returnPath}`);
|
||
}
|
||
const status = make("output", "local-file-status", "");
|
||
status.setAttribute("role", "status");
|
||
status.tabIndex = -1;
|
||
const actions = make("div", "local-file-actions", "", "local-file-actions");
|
||
let open;
|
||
if (canOpenSettings) {
|
||
open = make("button", "local-file-open", "Open extension settings", "ghost");
|
||
open.type = "button";
|
||
actions.append(open);
|
||
}
|
||
let skip;
|
||
if (onDismiss) {
|
||
skip = make("button", "local-file-skip", "Not now", "ghost");
|
||
skip.type = "button";
|
||
actions.append(skip);
|
||
}
|
||
container.classList.add("local-file-access");
|
||
container.setAttribute("aria-labelledby", heading.id);
|
||
container.replaceChildren(...[heading, description, actions, instruction, recovery, status].filter(Boolean));
|
||
container.hidden = true;
|
||
|
||
let allowed = null, opened = false, error = "", dismissed = false, disposed = false;
|
||
let sequence = 0, opening = false;
|
||
|
||
function render() {
|
||
const enabled = allowed === true;
|
||
const hideActions = (enabled && Boolean(onDismiss)) || actions.childElementCount === 0;
|
||
const focusedAction = actions.contains(document.activeElement);
|
||
container.hidden = dismissed;
|
||
heading.hidden = enabled;
|
||
description.hidden = enabled;
|
||
// Without an Open button the manual path is the only path, so it shows at once.
|
||
instruction.hidden = (canOpenSettings && !opened) || enabled;
|
||
if (recovery) recovery.hidden = instruction.hidden;
|
||
actions.hidden = hideActions;
|
||
container.classList.toggle("is-enabled", enabled);
|
||
const message = enabled ? "Local-file lookups enabled" : error;
|
||
if (status.textContent !== message) status.textContent = message;
|
||
if (hideActions && focusedAction) status.focus();
|
||
}
|
||
|
||
async function refresh() {
|
||
if (disposed || dismissed) return;
|
||
const request = ++sequence;
|
||
let nextAllowed = null, nextError = "";
|
||
try { nextAllowed = await chromeApi.extension.isAllowedFileSchemeAccess(); }
|
||
catch { nextError = `Could not check local-file access. You can check it in ${browserName}’s extension settings.`; }
|
||
if (disposed || dismissed || request !== sequence) return;
|
||
allowed = nextAllowed;
|
||
error = nextError;
|
||
render();
|
||
}
|
||
|
||
async function openSettings() {
|
||
if (opening) return;
|
||
opening = true;
|
||
opened = true;
|
||
error = "";
|
||
render();
|
||
try {
|
||
await chromeApi.tabs.create({
|
||
url: COPY.settingsUrl(),
|
||
});
|
||
}
|
||
catch {
|
||
if (!disposed && !dismissed) {
|
||
error = "Could not open extension settings. Try again.";
|
||
render();
|
||
}
|
||
} finally { opening = false; }
|
||
}
|
||
|
||
function dismiss() {
|
||
dismissed = true;
|
||
container.hidden = true;
|
||
onDismiss();
|
||
}
|
||
function visible() {
|
||
if (document.visibilityState === "visible") void refresh();
|
||
}
|
||
open?.addEventListener("click", openSettings);
|
||
skip?.addEventListener("click", dismiss);
|
||
document.addEventListener("visibilitychange", visible);
|
||
window.addEventListener("pageshow", visible);
|
||
void refresh();
|
||
|
||
return {
|
||
refresh,
|
||
destroy() {
|
||
disposed = true;
|
||
open?.removeEventListener("click", openSettings);
|
||
skip?.removeEventListener("click", dismiss);
|
||
document.removeEventListener("visibilitychange", visible);
|
||
window.removeEventListener("pageshow", visible);
|
||
},
|
||
};
|
||
}
|