fix: share, upload, crop, auto-skip, tag chips and See all hold what they claim
- Share: a link that expired is never copied (the dialog and one-click Share pick a live one, else make one), says it expired, and the copy-on-open claim clears on the next copy. - A password-protected file link asks to open a file, not to watch a video. - Upload: Cancel, the close button or the backdrop while naming a folder makes no folder; a failed refetch keeps the target; a folder being made says so. - Crop: an infinite number is dropped, not a 500; an unprobed clip's size follows a shown-size crop; the edit_spec docs name frame_w/frame_h; the join and rotated paths are tested. - Auto-skip: the wait gives way on a marker too short for it, so the skip still fires. - Tag chips count the followed channels (subscribed_count) and a toggled chip keeps the focus. - Continue watching's See all sorts by last watched when in progress was already on.
This commit is contained in:
@@ -88,6 +88,20 @@ export default function UploadDialog({
|
||||
refocusTarget.current = false;
|
||||
targetBox.current?.querySelector("button")?.focus();
|
||||
}, [naming]);
|
||||
// A press on Cancel, the ✕ or the backdrop blurs the name field before the dialog closes, and the
|
||||
// field saves on blur: that press is "never mind", not "make it". The press is seen before the
|
||||
// blur it causes, in every browser (which control takes the focus, if any, differs: Safari
|
||||
// focuses no button).
|
||||
const dismissing = useRef(false);
|
||||
useEffect(() => {
|
||||
const onPress = (e: PointerEvent) => {
|
||||
const el = e.target instanceof Element ? e.target : null;
|
||||
dismissing.current =
|
||||
!!el && (!!el.closest("[data-dismiss]") || el.matches('[role="dialog"]'));
|
||||
};
|
||||
document.addEventListener("pointerdown", onPress, true);
|
||||
return () => document.removeEventListener("pointerdown", onPress, true);
|
||||
}, []);
|
||||
const makeFolder = useMutation({
|
||||
mutationFn: (folder: string) => api.createFolder(folder),
|
||||
onSuccess: (made) => {
|
||||
@@ -113,8 +127,10 @@ export default function UploadDialog({
|
||||
const pickTarget = ([value]: string[]) => {
|
||||
// After the menu has closed: it hands the focus back as it goes, which took it off a field already
|
||||
// there, and a name field that loses the focus is done, with no name.
|
||||
if (value === NEW_FOLDER) window.setTimeout(() => setNaming(true));
|
||||
else if (value === UPLOADS) setTarget(null);
|
||||
if (value === NEW_FOLDER) {
|
||||
dismissing.current = false; // a press before the naming began (Cancel aborting an upload) is not this one
|
||||
window.setTimeout(() => setNaming(true));
|
||||
} else if (value === UPLOADS) setTarget(null);
|
||||
else setTarget(Number(value));
|
||||
};
|
||||
// A done file is in Files, opened on it; one the worker still converts is in the tray until it is.
|
||||
@@ -234,11 +250,11 @@ export default function UploadDialog({
|
||||
maxLength={DISPLAY_NAME_MAX}
|
||||
/>
|
||||
|
||||
<p id="upload-target-label" className="block text-sm font-medium mt-4 mb-1">
|
||||
{t("downloads.upload.targetLabel")}
|
||||
</p>
|
||||
{foldersQ.isError ? (
|
||||
// The file still goes where the dialog was opened on: only the list could not be read.
|
||||
<p className="block text-sm font-medium mt-4 mb-1">{t("downloads.upload.targetLabel")}</p>
|
||||
{foldersQ.isError && !foldersQ.data ? (
|
||||
// The file still goes where the dialog was opened on: only the list could not be read. A
|
||||
// failed REFETCH keeps the list it had (react-query sets isError beside the data): the
|
||||
// target the file goes to stays shown, as the rail does it.
|
||||
<StateMessage tone="error" compact onRetry={() => void foldersQ.refetch()}>
|
||||
{t("downloads.upload.targetFailed")}
|
||||
</StateMessage>
|
||||
@@ -249,12 +265,20 @@ export default function UploadDialog({
|
||||
onDone={(folder, byKey) => {
|
||||
refocusTarget.current = byKey;
|
||||
setNaming(false);
|
||||
if (!byKey && dismissing.current) return;
|
||||
if (folder?.trim()) makeFolder.mutate(folder.trim());
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div ref={targetBox} aria-labelledby="upload-target-label">
|
||||
{foldersQ.data && !busy && (
|
||||
// The select's own height held while the list loads, so it does not push the form down.
|
||||
<div ref={targetBox} className="min-h-8 flex items-center">
|
||||
{makeFolder.isPending && (
|
||||
// Upload waits for it (`targetSettling`): this says why.
|
||||
<span className="text-sm text-muted">
|
||||
{t("downloads.upload.makingFolder", { name: makeFolder.variables })}
|
||||
</span>
|
||||
)}
|
||||
{foldersQ.data && !busy && !makeFolder.isPending && (
|
||||
<FilterSelect
|
||||
hideLabel
|
||||
label={t("downloads.upload.targetLabel")}
|
||||
@@ -300,6 +324,7 @@ export default function UploadDialog({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => (busy ? abortRef.current?.abort() : onClose())}
|
||||
data-dismiss=""
|
||||
>
|
||||
{t("downloads.actions.cancel")}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user