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:
@@ -3,7 +3,7 @@ import { Meter } from "./ui/Meter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
import { FolderOpen, FolderPlus, HardDrive, Users } from "lucide-react";
|
||||
import { FolderOpen, FolderPlus, HardDrive, Link2, Users } from "lucide-react";
|
||||
import { api, type FileFolder } from "../lib/api";
|
||||
import { compareText } from "../lib/collate";
|
||||
import { formatBytes } from "../lib/format";
|
||||
@@ -36,16 +36,20 @@ function Entry({
|
||||
view,
|
||||
trigger,
|
||||
folderId,
|
||||
shared = false,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
count?: number;
|
||||
/** A live public link opens the folder: what is moved in is shown to whoever holds the link. */
|
||||
shared?: boolean;
|
||||
view: FilesView;
|
||||
/** The folder's menu triggers (right click, long press, Shift+F10). */
|
||||
trigger?: Record<string, unknown>;
|
||||
/** A folder's entry is found by it, to hand the keyboard back after a rename. */
|
||||
folderId?: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { view: current } = useFiles();
|
||||
const { setView } = useFilesActions();
|
||||
const active = sameView(view, current);
|
||||
@@ -64,6 +68,12 @@ function Entry({
|
||||
>
|
||||
<Icon className={clsx("size-4 shrink-0", !active && "text-muted")} />
|
||||
<span className="min-w-0 flex-1 truncate text-left">{label}</span>
|
||||
{shared && (
|
||||
<span title={t("files.folder.shared")} className="shrink-0">
|
||||
<Link2 aria-hidden className="size-3.5 text-accent" />
|
||||
<span className="sr-only">{t("files.folder.shared")}</span>
|
||||
</span>
|
||||
)}
|
||||
{count !== undefined && <span className="text-xs tabular-nums text-muted">{count}</span>}
|
||||
</Pressable>
|
||||
);
|
||||
@@ -176,6 +186,7 @@ export default function FilesRail({
|
||||
icon={folderIcon(f)}
|
||||
label={folderLabel(f, t)}
|
||||
count={f.count}
|
||||
shared={f.has_link}
|
||||
view={{ kind: "folder", id: f.id }}
|
||||
trigger={menu.triggerProps(f)}
|
||||
folderId={f.id}
|
||||
@@ -267,6 +278,7 @@ export default function FilesRail({
|
||||
ariaLabel={t("files.folder.menu")}
|
||||
items={menu.target ? folderActions.items(menu.target) : []}
|
||||
/>
|
||||
{folderActions.dialog}
|
||||
</FilterRail>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user