feat(files): the Downloads page goes — the tray, a Storage tab and a Downloads settings tab take its place

- The Downloads module, its rail icon and badge are gone. A bookmark, the remembered page and an old
  history entry that name it land on Files (urlState.resolvePage); `g d` goes to Files too.
- Users page: a Storage tab — the disk in numbers, every user's use against their own limit with
  their file count, every user's downloads; aligned tables, one menu per row (edit / reset quota,
  open source, copy, the owner's quota). The admin storage rows carry quota + file count, the admin
  job list its owner.
- Settings: a Downloads tab — the download formats in one list (duplicate a built-in, edit or delete
  your own) and the site sign-ins, moved from Account.
- "View" on an added download, a clip or a converting upload opens the tray on that row (unfolded,
  back if closed; a finished one opens as a file; under a player it opens without taking focus).
- The download dialog opens on the format used last.
- DataTable: `pager={false}` for a short list, `cardRow` to give a lone "…" its own card line.
- Welcome and README speak of Files; a new Files screenshot.
This commit is contained in:
2026-10-11 19:37:13 +02:00
parent 1c4f0042d2
commit 078194731b
69 changed files with 1553 additions and 1147 deletions
+33 -5
View File
@@ -4,7 +4,18 @@ import { qk } from "../lib/queryKeys";
import { formatDate, formatDateTime, formatUnit } from "../lib/format";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Activity, Bell, Check, Loader2, Monitor, RefreshCw, Trash2, User } from "lucide-react";
import {
Activity,
Bell,
Check,
Download,
Loader2,
Monitor,
RefreshCw,
Trash2,
User,
} from "lucide-react";
import { DownloadFormats } from "./ProfileEditor";
import DiagnosticsSettings from "./DiagnosticsSettings";
import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme";
import { api, HttpError, type Me } from "../lib/api";
@@ -50,10 +61,11 @@ export interface PrefsController {
saveState: PrefsSaveState;
}
type TabId = "appearance" | "notifications" | "account" | "diagnostics";
type TabId = "appearance" | "notifications" | "downloads" | "account" | "diagnostics";
const TABS: { id: TabId; icon: typeof Monitor }[] = [
{ id: "appearance", icon: Monitor },
{ id: "notifications", icon: Bell },
{ id: "downloads", icon: Download },
{ id: "account", icon: User },
{ id: "diagnostics", icon: Activity },
];
@@ -101,6 +113,7 @@ export default function SettingsPanel() {
<div {...panel.panelProps(tab)} className="flex-1 min-w-0 p-4">
{tab === "appearance" && <Appearance prefs={prefs} />}
{tab === "notifications" && <Notifications prefs={prefs} />}
{tab === "downloads" && <Downloads />}
{tab === "account" && <Account me={me} onOpenWizard={openWizard} />}
{tab === "diagnostics" && <DiagnosticsSettings />}
</div>
@@ -615,7 +628,7 @@ function PlexWatchSync() {
// (e.g. a private Facebook group you belong to) so downloads from it use YOUR login. Write-only —
// the stored blob is never shown back; each row shows only the host + whether it's set + when.
// A cookie-authed download is kept private to your account (never cache-shared with another user).
// Exported for a focused DOM test (write-only behaviour); rendered inside Account below.
// Exported for a focused DOM test (write-only behaviour); rendered on the Downloads tab below.
export function DownloadAuth() {
const { t, i18n } = useTranslation();
const qc = useQueryClient();
@@ -796,6 +809,23 @@ export function AccountHeader({ me }: { me: Me }) {
);
}
// How this account downloads: its formats (the download dialog's choices) and its sign-ins to sites
// that need one. Preferences of the downloading, not of the account's identity.
function Downloads() {
const { t } = useTranslation();
return (
<>
<Section title={t("settings.downloads.formats")}>
<p className="text-xs text-muted leading-relaxed mb-2">
{t("settings.downloads.formatsIntro")}
</p>
<DownloadFormats />
</Section>
<DownloadAuth />
</>
);
}
function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
const { t } = useTranslation();
const confirm = useConfirm();
@@ -825,8 +855,6 @@ function Account({ me, onOpenWizard }: { me: Me; onOpenWizard: () => void }) {
{me.role === "admin" && me.plex_enabled && <PlexWatchSync />}
<DownloadAuth />
{me.google_enabled ? (
// YouTube access requires Google OAuth; hidden when this instance has no Google configured.
<Section title={t("settings.account.youtubeAccess")}>