Files
rclone_gui/webgui/web/assets/js/state.js
T

223 lines
5.3 KiB
JavaScript

// state.js — minimal pub/sub store + hash router + localStorage-backed
// job metadata cache. Vanilla, no framework.
const JOB_META_KEY = "webgui:jobMeta";
const JOB_META_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
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
// jobMeta is persisted to localStorage so the jobs view can show
// src/dst for jobs after a page reload. Keyed by jobid (number).
jobMeta: loadJobMeta(),
};
function loadJobMeta() {
try {
const raw = localStorage.getItem(JOB_META_KEY);
if (!raw) return new Map();
const arr = JSON.parse(raw);
if (!Array.isArray(arr)) return new Map();
const now = Date.now();
const map = new Map();
for (const [id, meta] of arr) {
if (meta && typeof meta.submittedAt === "number") {
if (now - meta.submittedAt < JOB_META_TTL_MS) {
map.set(Number(id), meta);
}
}
}
return map;
} catch {
return new Map();
}
}
function saveJobMeta() {
try {
const arr = Array.from(state.jobMeta.entries());
localStorage.setItem(JOB_META_KEY, JSON.stringify(arr));
} catch {
// localStorage might be unavailable (private mode, quota); ignore.
}
}
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);
}
// --- Job metadata API ---
export function rememberJob(jobid, { action, src, dst }) {
state.jobMeta.set(Number(jobid), {
action,
src,
dst,
submittedAt: Date.now(),
});
saveJobMeta();
}
export function getJobMeta(jobid) {
return state.jobMeta.get(Number(jobid));
}
export function listJobMeta() {
return Array.from(state.jobMeta.entries())
.map(([jobid, meta]) => ({ jobid: Number(jobid), ...meta }))
.sort((a, b) => b.jobid - a.jobid);
}
export function forgetJob(jobid) {
state.jobMeta.delete(Number(jobid));
saveJobMeta();
}
// --- 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;
}
}