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:
2026-11-06 00:09:39 +01:00
parent c34f6f3297
commit 8383459f70
39 changed files with 760 additions and 82 deletions
+11 -6
View File
@@ -64,7 +64,7 @@ type Meta = FileMeta | FolderMeta;
// What the link answers before it is opened: a password link says only which kind it is.
type Answer =
| (Meta & { needs_password?: false })
| { needs_password: true; kind?: "file" | "folder" };
| { needs_password: true; kind?: "file" | "folder"; video?: boolean };
// Compact display of a URL: drop the protocol + trailing slash so it fits on one line.
function displayUrl(url: string): string {
@@ -97,8 +97,9 @@ export default function WatchPage() {
"loading" | "ready" | "password" | "gone" | "preparing" | "error"
>(token ? "loading" : "gone");
const [meta, setMeta] = useState<Meta | null>(null);
// A password link's kind, for its title, until it is unlocked.
const [locked, setLocked] = useState<"file" | "folder">("file");
// What a password link opens, for its prompt's words, until it is unlocked. A server from before
// it said `video` only had video links' words, so a file without it reads as one.
const [locked, setLocked] = useState<"video" | "file" | "folder">("video");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const errorId = useId();
@@ -124,7 +125,7 @@ export default function WatchPage() {
.then((m) => {
if (typeof m === "string") setStatus(m);
else if (m.needs_password) {
setLocked(m.kind ?? "file");
setLocked(m.kind === "folder" ? "folder" : m.video === false ? "file" : "video");
setStatus("password");
} else {
setMeta(m);
@@ -246,7 +247,11 @@ export default function WatchPage() {
<div className={`${SURFACE.panel} w-full max-w-sm p-6`}>
<div className="flex items-center gap-2 font-medium mb-4">
<Lock className="w-4 h-4 text-accent" />{" "}
{locked === "folder" ? T.passwordTitleFolder : T.passwordTitle}
{locked === "folder"
? T.passwordTitleFolder
: locked === "file"
? T.passwordTitleFile
: T.passwordTitle}
</div>
<Input
className="w-full"
@@ -272,7 +277,7 @@ export default function WatchPage() {
disabled={!password || unlocking}
className="mt-4"
>
{T.watch}
{locked === "video" ? T.watch : T.open}
</Button>
</div>
</div>