feat(settings): upload a Google Takeout export in Settings › Account
A "YouTube history" section under YouTube access: the watch history and the Watch later csv are dropped or picked (one request each, each file's outcome said on its own), and the section then says what the import did - only the counts it produced - or that it is running, failed or was cut off by a restart. While it runs it asks again until it ends; a failed re-read shows the failure, not the previous answer. The drop zone the Files upload had inline is now ui/DropZone, used by both. The e2e seed clears the account's kept Takeout videos.
This commit is contained in:
@@ -15,7 +15,7 @@ import type { DownloadJob } from "../../lib/api";
|
||||
import { useFilesActions } from "./filesContext";
|
||||
import { formatBytes } from "../../lib/format";
|
||||
import { DISPLAY_NAME_MAX } from "../../lib/api/downloads";
|
||||
import { Pressable } from "../../components/ui/Pressable";
|
||||
import { DropZone } from "../../components/ui/DropZone";
|
||||
import { Input } from "../../components/ui/Input";
|
||||
|
||||
// Local-file ingest. A general upload — ANY file type, no format list: media
|
||||
@@ -46,13 +46,8 @@ export default function UploadDialog({
|
||||
const qc = useQueryClient();
|
||||
const [file, setFile] = useState<File | null>(initialFile);
|
||||
const [name, setName] = useState("");
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [pct, setPct] = useState<number | null>(null); // null = idle; 0–100 while uploading
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
// dragenter/dragleave fire as the cursor crosses the drop zone's OWN children (icon/text), so a
|
||||
// plain boolean flickers. Count enter vs leave and only clear the highlight at depth 0.
|
||||
const dragDepth = useRef(0);
|
||||
const busy = pct !== null;
|
||||
// The per-file cap, so an over-cap file is refused HERE instead of after the user has pushed it up
|
||||
// the wire. Both server-side caps are still the boundary — this is only the pleasant half of it.
|
||||
@@ -86,8 +81,6 @@ export default function UploadDialog({
|
||||
// footer Cancel wired the abort; this covers every other dismissal.
|
||||
useEffect(() => () => abortRef.current?.abort(), []);
|
||||
|
||||
const pick = () => inputRef.current?.click();
|
||||
|
||||
const submit = async () => {
|
||||
// `tooBig` here is belt and braces, NOT the mechanism: `ui/Button` is focus-safe, so while the
|
||||
// button holds focus it swallows the activation itself, and while it does not it is natively
|
||||
@@ -138,74 +131,33 @@ export default function UploadDialog({
|
||||
<Modal initialFocus="field" title={t("downloads.upload.title")} onClose={onClose}>
|
||||
<p className="text-sm text-muted mb-4">{t("downloads.upload.hint")}</p>
|
||||
|
||||
{/* Drop zone doubles as the picker trigger — a click anywhere opens the native file dialog. */}
|
||||
<Pressable
|
||||
align="center"
|
||||
onClick={pick}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault();
|
||||
dragDepth.current += 1;
|
||||
setDragOver(true);
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDragLeave={() => {
|
||||
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
||||
if (dragDepth.current === 0) setDragOver(false);
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
dragDepth.current = 0;
|
||||
setDragOver(false);
|
||||
const f = e.dataTransfer.files?.[0];
|
||||
if (f) pick_(f);
|
||||
}}
|
||||
{/* Drop zone doubles as the picker trigger — a click anywhere opens the native file dialog.
|
||||
With a file picked, its label shows the file's name rather than `dropActive`, so the
|
||||
zone's drag border is the only feedback that it will take a replacement. */}
|
||||
<DropZone
|
||||
className="px-4 py-8"
|
||||
onFiles={([f]) => pick_(f)}
|
||||
disabled={busy}
|
||||
// The refusal is tied to the control it invalidates, not left to a one-shot announcement:
|
||||
// the drop zone reads as invalid and points at the sentence, so a reader who tabs back to
|
||||
// it hears WHY. (Measured, because `aria-invalid` is not in `button`'s ARIA-supported
|
||||
// list: Chromium's accessibility node for a native button does carry `invalid` from this
|
||||
// attribute.) The signal that is NOT colour is the sentence below, visible to everyone —
|
||||
// the border only reinforces it.
|
||||
aria-invalid={tooBig}
|
||||
aria-describedby={tooBig ? refusalId : undefined}
|
||||
// `dragOver` outranks `tooBig`: a drag is transient and the refusal repaints the instant
|
||||
// the drop lands, whereas a zone that stays red under the pointer tells the user their
|
||||
// drop target is dead — in the one state where they are trying to replace the file. With
|
||||
// a file picked, the label below shows its name rather than `dropActive`, so this border
|
||||
// is the ONLY feedback that the zone will accept the drop.
|
||||
className={`w-full flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed px-4 py-8 text-center transition disabled:opacity-60 ${
|
||||
dragOver
|
||||
? "border-accent bg-accent/5"
|
||||
: tooBig
|
||||
? "border-danger"
|
||||
: "border-border hover:border-fg/40"
|
||||
}`}
|
||||
invalid={tooBig}
|
||||
describedBy={tooBig ? refusalId : undefined}
|
||||
>
|
||||
{file ? (
|
||||
<>
|
||||
<FileIcon className="w-6 h-6 text-muted" />
|
||||
<span className="text-sm font-medium break-all">{file.name}</span>
|
||||
<span className="text-xs text-muted">{formatBytes(file.size, lang)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UploadCloud className="w-7 h-7 text-muted" />
|
||||
<span className="text-sm text-muted">
|
||||
{dragOver ? t("downloads.upload.dropActive") : t("downloads.upload.drop")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const f = e.target.files?.[0];
|
||||
if (f) pick_(f);
|
||||
e.target.value = ""; // let re-picking the same file fire onChange again
|
||||
}}
|
||||
/>
|
||||
{(dragOver) =>
|
||||
file ? (
|
||||
<>
|
||||
<FileIcon className="w-6 h-6 text-muted" />
|
||||
<span className="text-sm font-medium break-all">{file.name}</span>
|
||||
<span className="text-xs text-muted">{formatBytes(file.size, lang)}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UploadCloud className="w-7 h-7 text-muted" />
|
||||
<span className="text-sm text-muted">
|
||||
{dragOver ? t("downloads.upload.dropActive") : t("downloads.upload.drop")}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</DropZone>
|
||||
|
||||
{tooBig && (
|
||||
<p key={pickSeq} id={refusalId} className="text-xs text-danger mt-2" role="alert">
|
||||
|
||||
Reference in New Issue
Block a user