168 lines
3.9 KiB
JavaScript
168 lines
3.9 KiB
JavaScript
// state.js — minimal pub/sub store + hash router. Vanilla, no framework.
|
|
|
|
const listeners = new Set();
|
|
const state = {
|
|
remotes: [], // [{ name, type }]
|
|
jobs: new Map(), // jobid -> { jobid, status, progress, action, src, dst }
|
|
providers: null, // cached /config/providers response
|
|
};
|
|
|
|
export function getState() {
|
|
return state;
|
|
}
|
|
|
|
export function setState(patch) {
|
|
Object.assign(state, patch);
|
|
for (const l of listeners) {
|
|
try {
|
|
l(state);
|
|
} catch (e) {
|
|
console.error("state listener error", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function subscribe(fn) {
|
|
listeners.add(fn);
|
|
return () => listeners.delete(fn);
|
|
}
|
|
|
|
// --- Router ---
|
|
// Hash-based routes:
|
|
// #/remotes
|
|
// #/browse/<remote>/<path...>
|
|
// #/jobs
|
|
// #/jobs/new
|
|
// #/configure/new
|
|
// #/configure/new/<providerName>
|
|
// #/configure/edit/<remoteName>
|
|
|
|
const routeListeners = new Set();
|
|
|
|
export function onRoute(fn) {
|
|
routeListeners.add(fn);
|
|
return () => routeListeners.delete(fn);
|
|
}
|
|
|
|
function parseHash(hash) {
|
|
const raw = hash.replace(/^#\/?/, "");
|
|
const parts = raw.split("/").filter(Boolean);
|
|
if (parts.length === 0) {
|
|
return { name: "remotes", params: {} };
|
|
}
|
|
switch (parts[0]) {
|
|
case "remotes":
|
|
return { name: "remotes", params: {} };
|
|
case "browse":
|
|
return {
|
|
name: "browse",
|
|
params: {
|
|
remote: decodeURIComponent(parts[1] || ""),
|
|
path: parts.slice(2).map(decodeURIComponent).join("/"),
|
|
},
|
|
};
|
|
case "jobs":
|
|
if (parts[1] === "new") {
|
|
return { name: "jobs-new", params: {} };
|
|
}
|
|
return { name: "jobs", params: {} };
|
|
case "configure":
|
|
if (parts[1] === "new") {
|
|
return {
|
|
name: "configure-new",
|
|
params: { provider: parts[2] ? decodeURIComponent(parts[2]) : "" },
|
|
};
|
|
}
|
|
if (parts[1] === "edit") {
|
|
return {
|
|
name: "configure-edit",
|
|
params: { remote: decodeURIComponent(parts[2] || "") },
|
|
};
|
|
}
|
|
return { name: "configure-new", params: { provider: "" } };
|
|
default:
|
|
return { name: "remotes", params: {} };
|
|
}
|
|
}
|
|
|
|
export function navigate(hash) {
|
|
if (location.hash !== hash) {
|
|
location.hash = hash;
|
|
} else {
|
|
dispatch();
|
|
}
|
|
}
|
|
|
|
function dispatch() {
|
|
const route = parseHash(location.hash);
|
|
for (const l of routeListeners) {
|
|
try {
|
|
l(route);
|
|
} catch (e) {
|
|
console.error("route listener error", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
window.addEventListener("hashchange", dispatch);
|
|
window.addEventListener("load", dispatch);
|
|
|
|
// --- Toast ---
|
|
export function toast(message, kind = "default", ttl = 4000) {
|
|
const stack = document.getElementById("toast-stack");
|
|
if (!stack) return;
|
|
const el = document.createElement("div");
|
|
el.className = `toast toast-${kind}`;
|
|
el.textContent = message;
|
|
stack.appendChild(el);
|
|
setTimeout(() => el.remove(), ttl);
|
|
}
|
|
|
|
// --- Format helpers ---
|
|
export function formatBytes(n) {
|
|
if (n == null || isNaN(n)) return "—";
|
|
if (n < 1024) return `${n} B`;
|
|
const units = ["KB", "MB", "GB", "TB", "PB"];
|
|
let v = n / 1024;
|
|
let i = 0;
|
|
while (v >= 1024 && i < units.length - 1) {
|
|
v /= 1024;
|
|
i++;
|
|
}
|
|
return `${v.toFixed(1)} ${units[i]}`;
|
|
}
|
|
|
|
export function formatSpeed(bytesPerSec) {
|
|
return formatBytes(bytesPerSec) + "/s";
|
|
}
|
|
|
|
export function formatDuration(seconds) {
|
|
if (seconds == null || !isFinite(seconds)) return "—";
|
|
const s = Math.round(seconds);
|
|
const h = Math.floor(s / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
const sec = s % 60;
|
|
if (h > 0) return `${h}h ${m}m`;
|
|
if (m > 0) return `${m}m ${sec}s`;
|
|
return `${sec}s`;
|
|
}
|
|
|
|
export function formatTime(iso) {
|
|
if (!iso) return "—";
|
|
try {
|
|
const d = new Date(iso);
|
|
return d.toLocaleString();
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
export function escapeHtml(s) {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|