Files
SubMiner/vendor/hachidori/extension/local-file-access.js
T
sudacode d9fdc7ef6d feat(dictionary): add Hachidori backend support
- 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
2026-09-22 00:21:19 -07:00

156 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 Firefoxs 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 Hachidoris 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);
},
};
}