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
202 lines
6.8 KiB
JavaScript
202 lines
6.8 KiB
JavaScript
/*
|
|
* Dictionary-group Settings controls.
|
|
*
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import "./dictionary-group-state.js";
|
|
|
|
const { normaliseGroupName, groupNameKey } = globalThis.HDDictionaryGroups;
|
|
export const normaliseDictionaryGroups = globalThis.HDDictionaryGroups.normaliseDictionaryGroups;
|
|
|
|
const ALL_GROUP_NAME_KEY = groupNameKey("All");
|
|
|
|
function element(id) {
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function groupNameError(groups, name, excludedId = null) {
|
|
if (name === "") return "Enter a group name.";
|
|
const key = groupNameKey(name);
|
|
if (key === ALL_GROUP_NAME_KEY) return "All is reserved and cannot be used as a group name.";
|
|
if (groups.some((group) =>
|
|
group.id !== excludedId && groupNameKey(group.name) === key)) {
|
|
return "A group with this name already exists.";
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function bindMoveButtons(row, prefix, index, length, label, move) {
|
|
const up = row.querySelector(`.${prefix}-up`);
|
|
const down = row.querySelector(`.${prefix}-down`);
|
|
up.dataset.pinnedDisabled = String(index === 0);
|
|
down.dataset.pinnedDisabled = String(index === length - 1);
|
|
up.setAttribute("aria-label", `Move ${label} up`);
|
|
down.setAttribute("aria-label", `Move ${label} down`);
|
|
up.addEventListener("click", () => move(-1));
|
|
down.addEventListener("click", () => move(1));
|
|
}
|
|
|
|
export function createDictionaryGroupController({
|
|
setError,
|
|
readState,
|
|
readDictionaries,
|
|
commitGroups,
|
|
dictionaryLabel,
|
|
moveListItem,
|
|
updateItemById,
|
|
renderDeferredAfterBlur,
|
|
bindNameDraft,
|
|
}) {
|
|
function changeNamedGroup(name, excludedId, update) {
|
|
void commitGroups((current) => {
|
|
const error = groupNameError(current, name, excludedId);
|
|
if (error) {
|
|
setError(error);
|
|
return null;
|
|
}
|
|
setError("");
|
|
return update(current);
|
|
});
|
|
}
|
|
|
|
function changeGroup(id, update) {
|
|
void commitGroups((current) => updateItemById(current, id, update));
|
|
}
|
|
|
|
function moveGroup(id, step) {
|
|
void commitGroups((current) => {
|
|
const index = current.findIndex((group) => group.id === id);
|
|
return moveListItem(current, index, index + step);
|
|
});
|
|
}
|
|
|
|
function moveMember(groupId, dictionaryId, step) {
|
|
changeGroup(groupId, (group) => {
|
|
const index = group.dictionaryIds.indexOf(dictionaryId);
|
|
const dictionaryIds = moveListItem(group.dictionaryIds, index, index + step);
|
|
return dictionaryIds === null ? group : { ...group, dictionaryIds };
|
|
});
|
|
}
|
|
|
|
function renderMember(group, dictionary, index) {
|
|
const row = element("dict-group-member-template").content.firstElementChild.cloneNode(true);
|
|
const label = dictionaryLabel(dictionary);
|
|
row.dataset.dictionaryId = dictionary.id;
|
|
row.querySelector(".dict-group-member-name").textContent = label;
|
|
const canonical = row.querySelector(".dict-group-member-canonical");
|
|
canonical.textContent = dictionary.displayName ? dictionary.title : "";
|
|
canonical.hidden = !dictionary.displayName;
|
|
|
|
bindMoveButtons(
|
|
row,
|
|
"dict-group-member",
|
|
index,
|
|
group.dictionaryIds.length,
|
|
`${label} in ${group.name}`,
|
|
(step) => moveMember(group.id, dictionary.id, step),
|
|
);
|
|
|
|
const remove = row.querySelector(".dict-group-member-remove");
|
|
remove.setAttribute("aria-label", `Remove ${label} from ${group.name}`);
|
|
remove.addEventListener("click", () => {
|
|
changeGroup(group.id, (current) => ({
|
|
...current,
|
|
dictionaryIds: current.dictionaryIds.filter((id) => id !== dictionary.id),
|
|
}));
|
|
});
|
|
return row;
|
|
}
|
|
|
|
function bindName(row, group) {
|
|
const input = row.querySelector(".dict-group-name");
|
|
input.setAttribute("aria-label", `Name for ${group.name}`);
|
|
bindNameDraft(input, "groups", group.id, "name", group.name, normaliseGroupName, groupNameError);
|
|
renderDeferredAfterBlur(input);
|
|
}
|
|
|
|
function bindAdd(row, group, dictionaries) {
|
|
const select = row.querySelector(".dict-group-add-select");
|
|
const add = row.querySelector(".dict-group-add");
|
|
const available = dictionaries.filter((dictionary) => !group.dictionaryIds.includes(dictionary.id));
|
|
const placeholder = document.createElement("option");
|
|
placeholder.value = "";
|
|
placeholder.textContent = available.length === 0 ? "All dictionaries added" : "Choose a dictionary";
|
|
select.appendChild(placeholder);
|
|
for (const dictionary of available) {
|
|
const option = document.createElement("option");
|
|
option.value = dictionary.id;
|
|
option.textContent = dictionaryLabel(dictionary);
|
|
select.appendChild(option);
|
|
}
|
|
select.dataset.pinnedDisabled = String(available.length === 0);
|
|
add.dataset.pinnedDisabled = String(available.length === 0);
|
|
add.addEventListener("click", () => {
|
|
if (select.value === "") return;
|
|
const dictionaryId = select.value;
|
|
changeGroup(group.id, (current) => ({
|
|
...current,
|
|
dictionaryIds: [...current.dictionaryIds, dictionaryId],
|
|
}));
|
|
});
|
|
}
|
|
|
|
function renderRow(group, index, state, dictionaries, installedById) {
|
|
const row = element("dict-group-template").content.firstElementChild.cloneNode(true);
|
|
row.dataset.groupId = group.id;
|
|
bindName(row, group);
|
|
|
|
const remove = row.querySelector(".dict-group-delete");
|
|
remove.setAttribute("aria-label", `Delete ${group.name}`);
|
|
bindMoveButtons(
|
|
row,
|
|
"dict-group",
|
|
index,
|
|
state.groups.length,
|
|
group.name,
|
|
(step) => moveGroup(group.id, step),
|
|
);
|
|
remove.addEventListener("click", () => {
|
|
void commitGroups((current) => current.filter((entry) => entry.id !== group.id));
|
|
});
|
|
|
|
bindAdd(row, group, dictionaries);
|
|
const members = row.querySelector(".dict-group-members");
|
|
group.dictionaryIds.forEach((dictionaryId, memberIndex) => {
|
|
members.appendChild(renderMember(group, installedById.get(dictionaryId), memberIndex));
|
|
});
|
|
row.querySelector(".dict-group-members-empty").hidden = group.dictionaryIds.length > 0;
|
|
return row;
|
|
}
|
|
|
|
function render() {
|
|
const state = readState();
|
|
const dictionaries = readDictionaries();
|
|
const installedById = new Map(dictionaries.map((dictionary) => [dictionary.id, dictionary]));
|
|
const list = element("dict-group-list");
|
|
list.textContent = "";
|
|
state.groups.forEach((group, index) => {
|
|
list.appendChild(renderRow(group, index, state, dictionaries, installedById));
|
|
});
|
|
element("dict-group-empty").hidden = state.groups.length > 0;
|
|
}
|
|
|
|
function create() {
|
|
const input = element("dict-group-name-new");
|
|
const name = normaliseGroupName(input.value);
|
|
const error = groupNameError(readState().groups, name);
|
|
if (error) {
|
|
setError(error);
|
|
return;
|
|
}
|
|
input.value = "";
|
|
setError("");
|
|
changeNamedGroup(name, null, (current) => [
|
|
...current,
|
|
{ id: crypto.randomUUID(), name, dictionaryIds: [] },
|
|
]);
|
|
}
|
|
|
|
return { create, render };
|
|
}
|