feat(files): share a folder by link — its page lists what is in it, plays a video above the list
A /watch link may now open one of its owner's own folders (migration 0108: folder_id beside job_id, exactly one set; a folder's links go with it). The page shows what is in the folder when it is opened, newest first: a video or a song plays in the same player a file link has, above the list, each keeping its own resume point; a document only downloads, never opens as a page. A file moved out stops opening through the link at its next request. Password, expiry and stream-only work as on a file link; the unfurl names the folder and counts its files, never their names. Source folders and the Hub cannot be shared. The folder menu gets "Copy link" and "Share with a link…" (the Share dialog's link half); the rail marks a folder a live link opens. Two fixes in the Share dialog on the way: revoking the last link no longer mints a new one at once, and the new-link form goes back to downloadable after making a link.
This commit is contained in:
@@ -151,6 +151,13 @@ export interface ShareLink {
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
// What a new link is made with; an omitted `allow_download` takes the server's default (true).
|
||||
export interface NewShareLink {
|
||||
allow_download?: boolean;
|
||||
expires_days?: number | null;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
// The width of every user-supplied display name on a job, mirroring the backend's DISPLAY_TEXT_MAX
|
||||
// (backend/app/models.py) so an input can stop the user rather than letting the server truncate in
|
||||
// silence. The two are held together by `downloads.contract.test.ts` — a number living in two
|
||||
@@ -258,16 +265,21 @@ export const downloadsApi = {
|
||||
shareRecipients: (): Promise<ShareRecipient[]> => req("/api/downloads/recipients"),
|
||||
// Public watch links (share by link).
|
||||
downloadLinks: (jobId: number): Promise<ShareLink[]> => req(`/api/downloads/${jobId}/links`),
|
||||
createDownloadLink: (
|
||||
jobId: number,
|
||||
body: { allow_download?: boolean; expires_days?: number | null; password?: string },
|
||||
): Promise<ShareLink> =>
|
||||
createDownloadLink: (jobId: number, body: NewShareLink): Promise<ShareLink> =>
|
||||
req(`/api/downloads/${jobId}/links`, { method: "POST", body: JSON.stringify(body) }),
|
||||
updateDownloadLink: (
|
||||
linkId: number,
|
||||
patch: { allow_download?: boolean; expires_days?: number | null; password?: string },
|
||||
): Promise<ShareLink> =>
|
||||
req(`/api/downloads/links/${linkId}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
// A link to one of the user's own folders: it opens whatever is in the folder when it is opened.
|
||||
folderLinks: (folderId: number): Promise<ShareLink[]> =>
|
||||
req(`/api/downloads/folders/${folderId}/links`),
|
||||
createFolderLink: (folderId: number, body: NewShareLink): Promise<ShareLink> =>
|
||||
req(`/api/downloads/folders/${folderId}/links`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
revokeDownloadLink: (linkId: number): Promise<{ ok: true; deleted: number }> =>
|
||||
req(`/api/downloads/links/${linkId}`, { method: "DELETE" }),
|
||||
// The library player's resume checkpoint. A position too early or too close to the end clears it.
|
||||
|
||||
@@ -18,6 +18,8 @@ export interface FileFolder {
|
||||
count: number; // its done files
|
||||
size_bytes: number;
|
||||
created_at: string | null;
|
||||
// A live public link opens it: whatever is moved in is shown to whoever holds the link.
|
||||
has_link: boolean;
|
||||
}
|
||||
|
||||
// The most files one list answers with (backend `routes/files.py` MAX_LIST, held equal by
|
||||
|
||||
Reference in New Issue
Block a user