Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";3
const api = window.chjPlugin.hashing;4
const i18n = window.chjHashI18n.createI18n();5
const t = (key, values) => i18n.t(key, values);6
const state = { algorithms: [], calculateSelection: null, verifySelection: null, compareA: null, compareB: null, manifestSource: null, manifestFile: null, manifestRoot: null, activeJobId: null, activeJob: null };7
const $ = (id) => document.getElementById(id);9
function setText(id, value) { $(id).textContent = value; }10
function describeSelection(selection) { return selection?.files?.map((file) => `${file.name} (${formatBytes(file.size)})`).join(", ") || selection?.name || t("Nothing selected"); }11
function formatBytes(value) { const bytes = Number(value || 0); if (bytes < 1024) return `${bytes} B`; const units = ["KiB", "MiB", "GiB", "TiB"]; let amount = bytes; let unit = -1; do { amount /= 1024; unit += 1; } while (amount >= 1024 && unit < units.length - 1); return `${new Intl.NumberFormat(i18n.getLanguage(), { minimumFractionDigits: amount >= 100 ? 0 : amount >= 10 ? 1 : 2, maximumFractionDigits: amount >= 100 ? 0 : amount >= 10 ? 1 : 2 }).format(amount)} ${units[unit]}`; }12
function formatDuration(ms) { const seconds = Math.max(0, Number(ms || 0) / 1000); if (seconds < 60) return `${seconds.toFixed(1)} s`; return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`; }13
function errorMessage(error) { return i18n.errorMessage(error); }14
async function action(callback) { try { await callback(); } catch (error) { setText("summary", errorMessage(error)); $("summary").className = "summary failed"; } }16
function buildAlgorithmList() {17
const root = $("algorithmList"); root.replaceChildren();18
const groups = new Map();19
for (const algorithm of state.algorithms) {20
if (!groups.has(algorithm.category)) groups.set(algorithm.category, []);21
groups.get(algorithm.category).push(algorithm);22
}23
for (const [name, algorithms] of groups) {24
const group = document.createElement("section"); group.className = "algorithm-group";25
const heading = document.createElement("h4"); heading.dataset.category = name; heading.textContent = t(`category.${name}`); group.append(heading);26
for (const algorithm of algorithms) {27
const row = document.createElement("label"); row.className = "algorithm";28
const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.algorithm = algorithm.id; checkbox.checked = ["sha256", "sha512", "blake3"].includes(algorithm.id);29
const title = document.createElement("span"); title.textContent = algorithm.name; row.append(checkbox, title);30
if (algorithm.xof || algorithm.seeded || algorithm.keyed) {31
const advanced = document.createElement("span"); advanced.className = "advanced";32
const input = document.createElement("input"); input.dataset.optionFor = algorithm.id;33
if (algorithm.xof) { input.type = "number"; input.min = algorithm.xof.minBytes; input.max = algorithm.xof.maxBytes; input.value = algorithm.xof.defaultBytes; input.title = t("Output length in bytes"); input.placeholder = t("Output bytes"); }34
else if (algorithm.seeded) { input.value = "0"; input.title = t("{bits}-bit seed", { bits: algorithm.seedBits }); input.placeholder = t("Seed (default 0)"); }35
else { input.value = ""; input.title = t("{bytes}-byte hexadecimal key", { bytes: algorithm.keyBytes }); input.placeholder = t("HEX key ({bytes} bytes; zero key if blank)", { bytes: algorithm.keyBytes }); }36
advanced.append(input); row.append(advanced);37
}38
group.append(row);39
}40
root.append(group);41
}42
}44
function algorithmRequest(algorithm, optionRoot = document) {45
const request = { id: algorithm.id }; const input = optionRoot.querySelector(`[data-option-for="${algorithm.id}"]`);46
if (algorithm.xof) request.outputBytes = Number(input?.value || algorithm.xof.defaultBytes);47
if (algorithm.seeded) request.seed = input?.value || "0";48
if (algorithm.keyed && input?.value.trim()) request.keyHex = input.value.trim();49
return request;50
}51
function selectedAlgorithms() { return state.algorithms.filter((algorithm) => document.querySelector(`[data-algorithm="${algorithm.id}"]`)?.checked).map((algorithm) => algorithmRequest(algorithm)); }52
function simpleAlgorithm(id) { const algorithm = state.algorithms.find((item) => item.id === id); return algorithmRequest(algorithm); }54
function populateSelect(id, filter = () => true) {55
const select = $(id); const first = select.querySelector("option[value='']"); select.replaceChildren(); if (first) select.append(first);56
for (const algorithm of state.algorithms.filter(filter)) { const option = document.createElement("option"); option.value = algorithm.id; option.textContent = algorithm.name; select.append(option); }57
}59
async function chooseFiles(multiple) { const result = await api.selectFiles(multiple); return result.canceled ? null : result; }60
async function chooseDirectory() { const result = await api.selectDirectory(); return result.canceled ? null : result; }61
function acceptJob(job) { state.activeJobId = job.jobId; state.activeJob = job; renderJob(job); }63
function renderJob(job) {64
if (!job || (state.activeJobId && job.jobId !== state.activeJobId)) return;65
state.activeJob = job; state.activeJobId = job.jobId;66
setText("jobState", `${t(job.state).toLocaleUpperCase(i18n.getLanguage())} · ${job.filesCompleted}/${job.filesTotal}${job.currentFile ? ` · ${job.currentFile.name}` : ""}`);67
$("progressBar").style.width = `${Math.max(0, Math.min(100, job.percent || 0))}%`;68
$("progressStats").replaceChildren(...[69
`${formatBytes(job.bytes)} / ${formatBytes(job.totalBytes)}`,70
`${Number(job.percent || 0).toFixed(1)}%`,71
`${formatBytes(job.throughputBytesPerSecond)}/s`,72
t("Elapsed {duration}", { duration: formatDuration(job.elapsedMs) }),73
t("ETA {duration}", { duration: job.etaMs == null ? "—" : formatDuration(job.etaMs) })74
].map((text) => { const span = document.createElement("span"); span.textContent = text; return span; }));75
$("cancel").classList.toggle("hidden", !["queued", "running"].includes(job.state));76
$("copyAll").classList.toggle("hidden", !job.results.some((result) => result.hashes?.length));77
$("exportResults").classList.toggle("hidden", job.state !== "completed" || !job.results.some((result) => result.hashes?.length));78
const summary = $("summary"); summary.className = "summary";79
if (job.error) { summary.textContent = `${job.error.code}: ${errorMessage(job.error)}`; summary.classList.add("failed"); }80
else if (job.mode === "compare" && job.state === "completed") { const failed = job.results.some((result) => result.status !== "COMPLETED"); summary.textContent = t(failed ? "Comparison failed" : job.comparison?.identical ? "IDENTICAL digests" : "DIFFERENT digests"); summary.classList.add(failed ? "failed" : job.comparison?.identical ? "match" : "mismatch"); }81
else if (job.mode === "verify" && job.results[0]) { summary.textContent = t(job.results[0].status); summary.classList.add(job.results[0].status === "MATCH" ? "match" : "mismatch"); }82
else if (job.state === "completed") { const matches = job.results.filter((result) => result.status === "MATCH").length; const failures = job.results.filter((result) => !["COMPLETED", "MATCH"].includes(result.status)).length; summary.textContent = job.mode === "verify-manifest" ? t("{matches} matched · {failures} failed", { matches, failures }) : failures ? t("{completed} completed · {failures} failed", { completed: job.results.length - failures, failures }) : t("{count} file(s) completed", { count: job.results.length }); if (failures) summary.classList.add("failed"); }83
else summary.textContent = "";84
renderResults(job);85
}87
function renderResults(job) {88
const body = $("results"); body.replaceChildren();89
for (const result of job.results) {90
const hashes = result.hashes?.length ? result.hashes : [null];91
for (const hash of hashes) {92
const row = document.createElement("tr");93
for (const value of [result.file.relativePath, formatBytes(result.file.size), hash?.id || "—", hash?.value || (result.error ? errorMessage(result.error) : "—"), t(result.status)]) { const cell = document.createElement("td"); cell.textContent = value; if (value === hash?.value) cell.className = "digest"; row.append(cell); }94
row.children[4].className = `status-${String(result.status).toLowerCase().replace(/\s+/g, "-")}`;95
const controls = document.createElement("td");96
if (hash) { const copy = document.createElement("button"); copy.className = "small ghost"; copy.textContent = t("Copy"); copy.addEventListener("click", () => action(async () => { await api.copyResult({ jobId: job.jobId, fileId: result.file.fileId, algorithmId: hash.id, compact: true }); copy.textContent = t("Copied"); setTimeout(() => { copy.textContent = t("Copy"); }, 1200); })); controls.append(copy); }97
row.append(controls); body.append(row);98
}99
}100
}102
document.querySelectorAll(".tab").forEach((button) => button.addEventListener("click", () => {103
document.querySelectorAll(".tab").forEach((item) => item.classList.toggle("active", item === button));104
document.querySelectorAll(".tab-panel").forEach((panel) => panel.classList.toggle("active", panel.id === button.dataset.tab));105
}));107
$("closeButton").addEventListener("click", () => window.chjPlugin.close());108
$("calculateMode").addEventListener("change", () => $("recursiveLabel").classList.toggle("hidden", $("calculateMode").value !== "directory"));109
$("calculatePick").addEventListener("click", () => action(async () => { const mode = $("calculateMode").value; state.calculateSelection = mode === "directory" ? await chooseDirectory() : await chooseFiles(mode === "multiple"); setText("calculateSelection", describeSelection(state.calculateSelection)); }));110
$("selectAll").addEventListener("click", () => document.querySelectorAll("[data-algorithm]").forEach((input) => { input.checked = true; }));111
$("selectNone").addEventListener("click", () => document.querySelectorAll("[data-algorithm]").forEach((input) => { input.checked = false; }));112
$("recommended").addEventListener("click", () => document.querySelectorAll("[data-algorithm]").forEach((input) => { input.checked = ["sha256", "sha512", "blake3"].includes(input.dataset.algorithm); }));113
$("calculateStart").addEventListener("click", () => action(async () => { if (!state.calculateSelection) throw new Error(t("Choose local input first.")); const algorithms = selectedAlgorithms(); if (!algorithms.length) throw new Error(t("Select at least one algorithm.")); acceptJob(await api.start({ selectionId: state.calculateSelection.selectionId, algorithms, recursive: $("recursive").checked, output: $("output").value })); }));115
$("verifyPick").addEventListener("click", () => action(async () => { state.verifySelection = await chooseFiles(false); setText("verifySelection", describeSelection(state.verifySelection)); }));116
$("expected").addEventListener("input", () => { const value = $("expected").value.trim(); if ($("expectedEncoding").value !== "hex" || !/^[0-9a-f]+$/i.test(value)) return setText("detection", ""); const candidates = state.algorithms.filter((algorithm) => algorithm.digestBits && algorithm.digestBits / 4 === value.length); setText("detection", candidates.length ? t("Length candidates: {names}. Length alone is not definitive.", { names: candidates.map((item) => item.name).join(", ") }) : t("No fixed-length algorithm matches this length.")); });117
$("expectedEncoding").addEventListener("change", () => $("expected").dispatchEvent(new Event("input")));118
$("verifyStart").addEventListener("click", () => action(async () => { if (!state.verifySelection) throw new Error(t("Choose a local file first.")); acceptJob(await api.verify({ selectionId: state.verifySelection.selectionId, algorithm: simpleAlgorithm($("verifyAlgorithm").value), expected: $("expected").value, expectedEncoding: $("expectedEncoding").value })); }));120
$("comparePickA").addEventListener("click", () => action(async () => { state.compareA = await chooseFiles(false); setText("compareA", describeSelection(state.compareA)); }));121
$("comparePickB").addEventListener("click", () => action(async () => { state.compareB = await chooseFiles(false); setText("compareB", describeSelection(state.compareB)); }));122
$("compareStart").addEventListener("click", () => action(async () => { if (!state.compareA || !state.compareB) throw new Error(t("Choose both local files first.")); const algorithms = [...$("compareAlgorithm").selectedOptions].map((option) => simpleAlgorithm(option.value)); if (!algorithms.length) throw new Error(t("Select at least one comparison algorithm.")); acceptJob(await api.compare({ leftSelectionId: state.compareA.selectionId, rightSelectionId: state.compareB.selectionId, algorithms })); }));124
$("manifestSourcePick").addEventListener("click", () => action(async () => { state.manifestSource = $("manifestSourceMode").value === "directory" ? await chooseDirectory() : await chooseFiles(true); setText("manifestSource", describeSelection(state.manifestSource)); }));125
$("manifestFormat").addEventListener("change", () => { if ($("manifestFormat").value === "sfv") $("manifestAlgorithm").value = "crc32"; });126
$("manifestGenerate").addEventListener("click", () => action(async () => { if (!state.manifestSource) throw new Error(t("Choose local source files first.")); const id = $("manifestAlgorithm").value; const extension = $("manifestFormat").value === "sfv" ? "sfv" : (id === "crc32" ? "checksums" : id.replace(/-/g, "")); const destination = await api.selectManifestDestination(`checksums.${extension}`); if (destination.canceled) return; acceptJob(await api.generateManifest({ selectionId: state.manifestSource.selectionId, destinationSelectionId: destination.selectionId, algorithm: simpleAlgorithm(id), format: $("manifestFormat").value, recursive: $("manifestRecursive").checked })); }));127
$("manifestPick").addEventListener("click", () => action(async () => { const result = await api.selectManifest(); state.manifestFile = result.canceled ? null : result; setText("manifestSelection", describeSelection(result.canceled ? null : { files: [result.file] })); }));128
$("manifestRootPick").addEventListener("click", () => action(async () => { state.manifestRoot = await chooseDirectory(); setText("manifestRoot", describeSelection(state.manifestRoot)); }));129
$("manifestVerify").addEventListener("click", () => action(async () => { if (!state.manifestFile || !state.manifestRoot) throw new Error(t("Choose a manifest and its local root directory.")); acceptJob(await api.verifyManifest({ manifestSelectionId: state.manifestFile.selectionId, rootSelectionId: state.manifestRoot.selectionId, algorithmId: $("manifestVerifyAlgorithm").value || undefined })); }));131
$("cancel").addEventListener("click", () => action(async () => { if (state.activeJobId) renderJob(await api.cancel(state.activeJobId)); }));132
$("copyAll").addEventListener("click", () => action(async () => { await api.copyResult({ jobId: state.activeJobId }); $("copyAll").textContent = t("Copied"); setTimeout(() => { $("copyAll").textContent = t("Copy all"); }, 1200); }));133
$("exportResults").addEventListener("click", () => action(async () => { const destination = await api.selectManifestDestination("hash-results.txt"); if (!destination.canceled) await api.exportResults({ jobId: state.activeJobId, destinationSelectionId: destination.selectionId, format: "text" }); }));135
api.onProgress((job) => renderJob(job));137
function applyLanguage(language) {138
i18n.setLanguage(language);139
i18n.apply(document);140
for (const [id, selection] of Object.entries({ calculateSelection: state.calculateSelection, verifySelection: state.verifySelection, compareA: state.compareA, compareB: state.compareB, manifestSource: state.manifestSource, manifestSelection: state.manifestFile ? { files: [state.manifestFile.file] } : null, manifestRoot: state.manifestRoot })) setText(id, describeSelection(selection));141
document.querySelectorAll("[data-category]").forEach((heading) => { heading.textContent = t(`category.${heading.dataset.category}`); });142
for (const algorithm of state.algorithms) {143
const input = document.querySelector(`[data-option-for="${algorithm.id}"]`);144
if (!input) continue;145
if (algorithm.xof) { input.title = t("Output length in bytes"); input.placeholder = t("Output bytes"); }146
else if (algorithm.seeded) { input.title = t("{bits}-bit seed", { bits: algorithm.seedBits }); input.placeholder = t("Seed (default 0)"); }147
else { input.title = t("{bytes}-byte hexadecimal key", { bytes: algorithm.keyBytes }); input.placeholder = t("HEX key ({bytes} bytes; zero key if blank)", { bytes: algorithm.keyBytes }); }148
}149
$("expected").dispatchEvent(new Event("input"));150
if (state.activeJob) renderJob(state.activeJob);151
}153
window.chjPlugin.ui.onLanguageChanged(applyLanguage);154
action(async () => {155
const [language, algorithms] = await Promise.all([window.chjPlugin.ui.getLanguage(), api.getAlgorithms()]);156
state.algorithms = algorithms;157
applyLanguage(language);158
buildAlgorithmList();159
const fixed = (algorithm) => !algorithm.xof;160
populateSelect("verifyAlgorithm", fixed); populateSelect("compareAlgorithm", fixed); populateSelect("manifestAlgorithm", (algorithm) => fixed(algorithm) && !algorithm.keyed && !algorithm.seeded); populateSelect("manifestVerifyAlgorithm", fixed);161
$("verifyAlgorithm").value = "sha256"; $("compareAlgorithm").value = "sha256"; $("manifestAlgorithm").value = "sha256";162
});SHA-256: 28045a7c2c55ccaaf5ad0b8d941780f71b88927a9d67469f262b183e4cb90c8a
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0