- Share: a link that expired is never copied (the dialog and one-click Share pick a live one, else make one), says it expired, and the copy-on-open claim clears on the next copy. - A password-protected file link asks to open a file, not to watch a video. - Upload: Cancel, the close button or the backdrop while naming a folder makes no folder; a failed refetch keeps the target; a folder being made says so. - Crop: an infinite number is dropped, not a 500; an unprobed clip's size follows a shown-size crop; the edit_spec docs name frame_w/frame_h; the join and rotated paths are tested. - Auto-skip: the wait gives way on a marker too short for it, so the skip still fires. - Tag chips count the followed channels (subscribed_count) and a toggled chip keeps the focus. - Continue watching's See all sorts by last watched when in progress was already on.
88 lines
4.2 KiB
TypeScript
88 lines
4.2 KiB
TypeScript
import type { QueryClient } from "@tanstack/react-query";
|
|
import { qk } from "./queryKeys";
|
|
import { api, type NewShareLink, type ShareLink } from "./api";
|
|
import { copyText } from "./clipboard";
|
|
import { msg, notify } from "./notifications";
|
|
import { notifyActionError } from "./actionErrors";
|
|
|
|
// The download queue, the per-card download index, the library (its listings and folder counts) and
|
|
// the storage meter all move together whenever a job is enqueued, deleted, finishes, or a file is
|
|
// renamed or moved — invalidate them as one unit.
|
|
export function invalidateDownloads(qc: QueryClient) {
|
|
qc.invalidateQueries({ queryKey: qk.downloads() });
|
|
qc.invalidateQueries({ queryKey: qk.downloadIndex() });
|
|
qc.invalidateQueries({ queryKey: qk.files() });
|
|
qc.invalidateQueries({ queryKey: qk.fileFolders() });
|
|
qc.invalidateQueries({ queryKey: qk.storageUsage() });
|
|
}
|
|
|
|
// What a public link opens: one file, or one of the user's own folders (whatever is in it when the
|
|
// link is opened).
|
|
export type LinkTarget = { kind: "file"; id: number } | { kind: "folder"; id: number };
|
|
|
|
export const linksKey = (target: LinkTarget) =>
|
|
target.kind === "file" ? qk.downloadLinks(target.id) : qk.folderLinks(target.id);
|
|
|
|
export const fetchLinks = (target: LinkTarget) =>
|
|
target.kind === "file" ? api.downloadLinks(target.id) : api.folderLinks(target.id);
|
|
|
|
export const makeLink = (target: LinkTarget, body: NewShareLink) =>
|
|
target.kind === "file"
|
|
? api.createDownloadLink(target.id, body)
|
|
: api.createFolderLink(target.id, body);
|
|
|
|
/** After a link was made, changed or revoked: its list, and — for a folder — the rail's mark of the
|
|
* folders a live link opens (an expiry change can end it too). */
|
|
export function invalidateLinks(qc: QueryClient, target: LinkTarget) {
|
|
void qc.invalidateQueries({ queryKey: linksKey(target) });
|
|
if (target.kind === "folder") void qc.invalidateQueries({ queryKey: qk.fileFolders() });
|
|
}
|
|
|
|
/** Has a link stopped opening? The list still holds an expired link (its owner may want to see or
|
|
* revoke it), but it is no link to hand out. */
|
|
export const linkExpired = (link: ShareLink, now = Date.now()) =>
|
|
link.expires_at !== null && Date.parse(link.expires_at) <= now;
|
|
|
|
/** The link to hand out of a list (newest first): the newest that still opens and whose address can
|
|
* be shown — an expired one sends the recipient to "link gone", one without an address is no use
|
|
* to copy. None: make one. */
|
|
export const liveLink = (links: ShareLink[]) => links.find((l) => l.url && !linkExpired(l));
|
|
|
|
// One-click share: the row's Share button turns "share this" into a single action.
|
|
// Ensure the file or folder has a public link (minting one on demand — omitting `allow_download` so
|
|
// the backend default makes it downloadable), copy it to the clipboard, and toast with an "open in
|
|
// new tab" action. A copy failure still toasts (warning) WITH that action, so the link is never a dead
|
|
// end. Advanced controls (password, expiry, revoke, share-with-a-user) live in the Share dialog.
|
|
// Returns the shared URL, or null if link creation failed — so the caller/tests can react to the outcome.
|
|
export async function quickShareLink(qc: QueryClient, target: LinkTarget): Promise<string | null> {
|
|
let path;
|
|
try {
|
|
const existing = await qc.fetchQuery({
|
|
queryKey: linksKey(target),
|
|
queryFn: () => fetchLinks(target),
|
|
});
|
|
path = liveLink(existing)?.url;
|
|
if (!path) {
|
|
const made = await makeLink(target, {}); // omit allow_download → backend default (true)
|
|
qc.setQueryData(linksKey(target), [made, ...existing]);
|
|
if (target.kind === "folder") void qc.invalidateQueries({ queryKey: qk.fileFolders() });
|
|
path = made.url;
|
|
}
|
|
if (!path) throw new Error("the new link came back without its address");
|
|
} catch (e) {
|
|
notifyActionError(e, msg("downloads.share.linkFailed"));
|
|
return null;
|
|
}
|
|
const url = window.location.origin + path;
|
|
const action = {
|
|
label: msg("downloads.share.openNewTab"),
|
|
onClick: () => window.open(url, "_blank", "noopener,noreferrer"),
|
|
};
|
|
notify(
|
|
(await copyText(url))
|
|
? { level: "success", message: msg("downloads.share.linkCopied"), action }
|
|
: { level: "warning", message: msg("downloads.share.copyFailed"), action },
|
|
);
|
|
return url;
|
|
}
|