import { CONFIG } from "./config.js"; import { Store } from "./store.js"; import { Bag } from "./bag.js"; import { TRACKS } from "./tracks.js"; import { fetchAllSpacesWithRetry } from "./api.js"; import { mountCard } from "./embed.js"; import * as ui from "./ui.js"; import { initOAuth, signIn, signOut, likeOnHf } from "./oauth.js"; const $ = (id) => document.getElementById(id); const state = { store: new Store({ key: CONFIG.storageKey }), all: [], // all view models byId: new Map(), bag: null, filterTrack: null, // null | "Backyard" | "Wood" current: null, // current vm controller: null, // mountCard controller oauth: null, }; function idsForTrack(track) { const list = track ? state.all.filter((v) => v.track === track) : state.all; return list.map((v) => v.id); } function eligibleIds() { return idsForTrack(state.filterTrack); } function prefetchNext() { if (!CONFIG.prefetchNext || !state.bag) return; const peekId = state.bag.bag[state.bag.cursor]; const vm = peekId && state.byId.get(peekId); if (!vm) return; let link = document.getElementById("prefetch-link"); if (!link) { link = document.createElement("link"); link.id = "prefetch-link"; link.rel = "preconnect"; document.head.append(link); } link.href = vm.embedUrl; } function show(vm) { if (!vm) return; state.controller?.destroy(); state.current = vm; state.store.markSeen(vm.id); ui.renderCard(vm, state.store); ui.setProgress(state.bag.progress()); state.controller = mountCard($("stage"), vm, { onSkip: next }); if (CONFIG.resumeRound) state.store.saveRound({ ...state.bag.serialize(), track: state.filterTrack }); prefetchNext(); } function next() { show(state.byId.get(state.bag.next())); } function prev() { const id = state.bag.prev(); if (id) show(state.byId.get(id)); } function cycleTrack() { const order = [null, ...TRACKS]; const nextTrack = order[(order.indexOf(state.filterTrack) + 1) % order.length]; const ids = idsForTrack(nextTrack); if (!ids.length) { ui.toast(`No spaces in ${nextTrack}`); return; } state.filterTrack = nextTrack; ui.setTrackFilterLabel(state.filterTrack); state.bag.setIds(ids); next(); } function wireKeys() { document.addEventListener("keydown", (e) => { const tag = e.target.tagName; if (tag === "INPUT" || tag === "TEXTAREA") return; switch (e.key) { case " ": case "ArrowRight": e.preventDefault(); next(); break; case "ArrowLeft": prev(); break; case "l": case "L": $("btn-like").click(); break; case "s": case "S": $("btn-save").click(); break; case "o": case "O": if (state.current) window.open(state.current.hfUrl, "_blank"); break; case "?": ui.showShortcuts(); break; } }); } function wireButtons() { $("btn-next").onclick = next; $("btn-prev").onclick = prev; $("btn-track").onclick = cycleTrack; ui.setTrackFilterLabel(null); $("btn-save").onclick = () => { if (!state.current) return; const on = state.store.toggleSaved(state.current.id); ui.setSaveState(on); ui.setSavedCount(state.store.savedIds().length); ui.toast(on ? "Saved ๐พ" : "Removed from saved"); }; $("btn-saved").onclick = () => { const vms = state.store.savedIds().map((id) => state.byId.get(id)).filter(Boolean); ui.openDrawer(vms, { onOpenHere: (id) => { $("drawer").hidden = true; show(state.byId.get(id)); }, onUnsave: (id) => { state.store.toggleSaved(id); ui.setSavedCount(state.store.savedIds().length); }, }); }; ui.setSavedCount(state.store.savedIds().length); $("btn-like").onclick = async () => { const vm = state.current; if (!vm) return; const turningOn = !state.store.isLiked(vm.id); ui.setLikeState(turningOn); // optimistic const where = await likeOnHf(vm.id, turningOn, state.oauth?.accessToken); if (where === "hf") { state.store.setHfLiked(vm.id, turningOn); vm.likes = Math.max(0, (vm.likes || 0) + (turningOn ? 1 : -1)); ui.setHfLikes(vm.likes); ui.toast(turningOn ? "Liked on Hugging Face ๐" : "Unliked"); } else { state.store.setLocalLiked(vm.id, turningOn); ui.toast(state.oauth ? "Couldn't like on HF โ saved locally. Open on HF to like officially โ" : "Saved your like locally โ sign in to like on HF โ"); } }; } function renderAuth() { ui.renderAuth(state.oauth?.userInfo, { onSignIn: signIn, onSignOut: () => { signOut(); state.oauth = null; renderAuth(); }, }); } function showError(msg) { $("stage").innerHTML = `