feat(files): folders managed in place, a grid per folder, drop to upload, open on one file
- Own folders are made, renamed and deleted from the rail (right click, long press, Shift+F10) or the "…" beside an open folder's name; any folder's retention is a submenu (7/30/90 days, until deleted). Deleting confirms where its files go. - List or grid per folder, sharing one sort; the grid is the one media card. - A file dropped anywhere on the page opens Upload with it, bound for the folder on screen. - A link (?item=) or the upload notice opens Files on the file: its page turned to, the keyboard on it, a ring round it. - An in-place name field left by Enter or Escape hands the focus back to what replaced it (the row, the entry, the "…") instead of dropping it on <body>. - The table's saved page size is per account.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Scissors } from "lucide-react";
|
||||
import type { LibraryFile } from "../lib/api";
|
||||
import { navItemAttrs } from "../lib/listNav";
|
||||
import { JobThumb } from "./downloadRow";
|
||||
import { jobTitle } from "./downloadTitle";
|
||||
import { PhoneBadge, RetentionBadge, SourceBadge } from "./fileBadges";
|
||||
import { MediaCard, MediaPlayGlyph } from "./ui/MediaCard";
|
||||
import {
|
||||
MEDIA_BADGE,
|
||||
MEDIA_BAR,
|
||||
MEDIA_HOVER_LAYER,
|
||||
MEDIA_PICTURE,
|
||||
mediaPictureBox,
|
||||
mediaTitleClass,
|
||||
} from "./ui/mediaCardStyles";
|
||||
import { MoreButton } from "./ui/MoreButton";
|
||||
import { Pressable } from "./ui/Pressable";
|
||||
|
||||
// A library file in the Files grid: the one media card (ui/MediaCard) — its picture plays it, the
|
||||
// title under it, then where it came from and what needs saying about it (kept, leaving soon). The
|
||||
// right click, the long press and the "…" open the same menu as its table row (DESIGN.md "Files page":
|
||||
// the grid is the list's other paint, a toggle remembered per folder).
|
||||
|
||||
export function FileCard({
|
||||
file,
|
||||
index,
|
||||
onPlay,
|
||||
menuProps,
|
||||
moreProps,
|
||||
menuOpen,
|
||||
arrived,
|
||||
}: {
|
||||
file: LibraryFile;
|
||||
/** Its place on the page, for the keyboard walk. */
|
||||
index: number;
|
||||
/** Null: nothing to play (a plain file, a shared one its owner keeps from me). */
|
||||
onPlay: (() => void) | null;
|
||||
menuProps: Record<string, unknown>;
|
||||
moreProps: Record<string, unknown>;
|
||||
menuOpen: boolean;
|
||||
/** The file a link or a notice opened the page on. */
|
||||
arrived: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const title = jobTitle(file);
|
||||
const channel = file.display_uploader || file.asset?.uploader || null;
|
||||
const clip = file.job_kind === "edit" || file.job_kind === "clip";
|
||||
const thumb = <JobThumb job={file} box="h-full w-full" />;
|
||||
return (
|
||||
<MediaCard
|
||||
dense
|
||||
rootProps={{ ...menuProps, ...navItemAttrs(index, title), "data-file-id": file.id }}
|
||||
className={clsx(arrived && "ring-2 ring-accent/60")}
|
||||
picture={
|
||||
<div className={mediaPictureBox("wide")}>
|
||||
{onPlay ? (
|
||||
<Pressable
|
||||
onClick={onPlay}
|
||||
aria-label={t("downloads.actions.play")}
|
||||
data-nav-open=""
|
||||
className={MEDIA_PICTURE}
|
||||
>
|
||||
{thumb}
|
||||
<div className={MEDIA_HOVER_LAYER}>
|
||||
<MediaPlayGlyph small />
|
||||
</div>
|
||||
</Pressable>
|
||||
) : (
|
||||
<div className={clsx(MEDIA_PICTURE, "cursor-default")}>{thumb}</div>
|
||||
)}
|
||||
{clip && (
|
||||
<span
|
||||
className={clsx(MEDIA_BADGE, "left-1.5 top-1.5 inline-flex items-center gap-0.5")}
|
||||
>
|
||||
<Scissors className="size-3" aria-hidden />
|
||||
{t("downloads.clipBadge")}
|
||||
</span>
|
||||
)}
|
||||
<span className={MEDIA_BAR}>
|
||||
{/* `media`: legible on any picture, the Plex cards' hover row's look. */}
|
||||
<MoreButton
|
||||
variant="media"
|
||||
label={t("files.rowMenu")}
|
||||
expanded={menuOpen}
|
||||
menuProps={moreProps}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
title={
|
||||
<span className={mediaTitleClass(true)} title={title}>
|
||||
{title}
|
||||
</span>
|
||||
}
|
||||
meta={
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="flex min-w-0 items-center gap-1.5 text-xs text-muted">
|
||||
<SourceBadge file={file} />
|
||||
{channel && <span className="truncate">{channel}</span>}
|
||||
</span>
|
||||
<span className="flex flex-wrap items-center gap-1 empty:hidden">
|
||||
<PhoneBadge file={file} />
|
||||
<RetentionBadge file={file} quiet />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
more={{ props: moreProps, open: menuOpen }}
|
||||
moreLabel={t("files.rowMenu")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user