import { api, type DownloadStatus, type QueueJob } from "./api"; import { createStore } from "./store"; // The download queue as this tab has watched it: which jobs it has seen finish. The tray shows them // ("Done · Open") until it is closed, and announces each once (the done notice). // // Watched where the queue is FETCHED, not in a component's effect: every reader of the queue fetches // through `fetchQueue`, so a finish is seen once whichever of them polled, and nothing has to diff the // previous render's data. /** The statuses the tray lists as under way: waiting, working, or paused by its user. */ export const IN_FLIGHT: ReadonlySet = new Set(["queued", "running", "paused"]); /** The statuses a job leaves on its own, so the queue is worth asking about often. A paused job is * not one: it waits for its user, however long that takes. */ export const MOVING: ReadonlySet = new Set(["queued", "running"]); /** The steps a running job reports that have a name of their own (`downloads.phase.*`). */ export const DOWNLOAD_PHASES: readonly string[] = [ "video", "audio", "editing", "converting", "merging", "audio_extract", "thumbnail", "sponsorblock", "metadata", "processing", ]; let lastSeen: Map | null = null; const finished = createStore([]); const unannounced = createStore([]); const revealing = createStore(null); /** A job to show in the tray, asked for at `at` (ms). */ export interface Reveal { id: number; at: number; } /** Note a fresh queue: a job that is done now and was not on the last look has just finished. The * first look only learns what is there — a file finished before this tab opened is old news. */ export function noteQueue(jobs: QueueJob[]): void { const before = lastSeen; lastSeen = new Map(jobs.map((j) => [j.id, j.status])); if (!before) return; const done = jobs.filter((j) => j.status === "done" && before.get(j.id) !== "done"); if (done.length === 0) return; const ids = new Set(done.map((j) => j.id)); finished.set((list) => [...done, ...list.filter((j) => !ids.has(j.id))]); unannounced.set((list) => [...list, ...done]); } /** The queue, noted (see above). Every reader of `qk.downloads()` fetches with this. */ export async function fetchQueue(): Promise { const jobs = await api.downloads(); noteQueue(jobs); return jobs; } /** The jobs this tab saw finish, newest first, until the tray is closed. */ export const useFinished = (): QueueJob[] => finished.useValue(); /** The tray was closed: what finished is no longer shown. */ export const clearFinished = (): void => finished.set([]); /** A file was deleted: a "Done · Open" row for it would open and link nothing. */ export const forgetFinished = (id: number): void => finished.set((list) => (list.some((j) => j.id === id) ? list.filter((j) => j.id !== id) : list)); /** The finishes not yet announced — the render subscription that wakes the announcer. */ export const useUnannounced = (): QueueJob[] => unannounced.useValue(); /** Take the finishes to announce, clearing them: each is announced once. */ export function takeUnannounced(): QueueJob[] { const list = unannounced.get(); if (list.length) unannounced.set([]); return list; } /** "View" on a job just started (a download, a clip, an upload still converting): the tray opens on * its row — unfolded, back if it was closed, the keyboard on the row. One the tray no longer lists * because it is done already opens as a file. */ export const showInTray = (id: number): void => revealing.set({ id, at: Date.now() }); /** The pending "View", until the tray has answered it. */ export const useRevealing = (): Reveal | null => revealing.useValue(); /** The tray answered the "View" (or the job is not in the queue at all). */ export const doneRevealing = (): void => revealing.set(null); /** Forget what was watched (a test starts from a tab that has looked at nothing). */ export function resetQueueWatch(): void { lastSeen = null; finished.set([]); unannounced.set([]); revealing.set(null); }