v4: an uploaded audio file plays from its cover everywhere, and a card's play badge sits in the middle of its thumbnail — a link played from its thumbnail in every view, but an uploaded file only did on a list card: a journal tile opened the dump, an audio file's page showed a round button by its waveform and never its uploaded cover, and an uploaded video's page was a black box until played. The list card's badge sat in the thumbnail's bottom-left corner, where it looked misplaced, and clicking a playing file restarted it. The new usePlayFile hook builds the player's item and the file's artwork (fileArtworkUrl) for the card, the journal tile and the dump's page, and plays, or pauses once the file is the current one. A card's badge, ▶ or ⏸, is now centred, as is the ↗ on a link out; over a placeholder it takes the place of the glyph. An audio file without a cover shows a placeholder tinted from its file name, not a bare 🎵. In the journal, audio counts as having art (hasThumbnail), so it is an image tile, its glyph raised above the title, and a file tile plays and pauses the file. On the dump's page, the cover (96px, the card's badge on it) plays the file beside the shared Waveform, which replaces FilePreview's copy of it; a video takes the server's still as its poster. 674 frontend tests pass, and tsc, eslint, deno fmt, doc:lint and i18n:compile --strict are clean; checked in screenshots in Smooth light at 1280 and 390px, Brutalist dark on the journal at 390px and Kanagawa dark at 390px, playing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
88
src/hooks/player/usePlayFile.ts
Normal file
88
src/hooks/player/usePlayFile.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Plays an uploaded audio or video file in the global player. FilePreview and
|
||||
* JournalCard call it, so a file plays and pauses the same from a list card, a
|
||||
* journal tile and its dump's page.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { useContext } from "react";
|
||||
import type { Dump } from "../../../shared/dump.ts";
|
||||
import {
|
||||
PlayerContext,
|
||||
type PlayerItem,
|
||||
} from "../../contexts/player/PlayerContext.ts";
|
||||
import { useAuth } from "../auth/useAuth.ts";
|
||||
import {
|
||||
dumpFileUrl,
|
||||
dumpThumbnailUrl,
|
||||
dumpUrl,
|
||||
} from "../../utils/shared/urls.ts";
|
||||
|
||||
/** Checks whether `dump` is an uploaded file the global player can play. */
|
||||
export function isPlayableFile(dump: Dump): boolean {
|
||||
const mime = dump.fileMime ?? "";
|
||||
return dump.kind === "file" &&
|
||||
(mime.startsWith("audio/") || mime.startsWith("video/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the artwork of an uploaded file, for its thumbnail and the player:
|
||||
* the uploaded thumbnail, or the server's still of a video or scaled-down copy
|
||||
* of an image. A bare audio file has none, since the thumbnail endpoint has
|
||||
* nothing to serve for it, and shows a placeholder instead.
|
||||
*/
|
||||
export function fileArtworkUrl(
|
||||
dump: Dump,
|
||||
token?: string | null,
|
||||
): string | undefined {
|
||||
const mime = dump.fileMime ?? "";
|
||||
const hasArtwork = !!dump.thumbnailMime || mime.startsWith("video/") ||
|
||||
mime.startsWith("image/");
|
||||
return hasArtwork ? dumpThumbnailUrl(dump, token) : undefined;
|
||||
}
|
||||
|
||||
/** Returned by {@link usePlayFile} to the views that play an uploaded file. */
|
||||
export interface UsePlayFileResult {
|
||||
/** The file's URL, with the viewer's media token, as the player loads it. */
|
||||
fileUrl: string;
|
||||
/** True while the file is the player's current item, playing or paused. */
|
||||
active: boolean;
|
||||
/** True while the file is playing. */
|
||||
playing: boolean;
|
||||
/**
|
||||
* Plays the file from the start, or pauses and resumes it once it is the
|
||||
* player's current item, so a second click doesn't restart it.
|
||||
*/
|
||||
toggle: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the playback state of the uploaded file `dump` and a function that
|
||||
* plays or pauses it. The player's title links to the dump's page.
|
||||
*/
|
||||
export function usePlayFile(dump: Dump): UsePlayFileResult {
|
||||
const { current, playing, play, togglePlay } = useContext(PlayerContext);
|
||||
const { mediaToken: token } = useAuth();
|
||||
const fileUrl = dumpFileUrl(dump, token);
|
||||
const active = current?.kind === "file" && current.fileUrl === fileUrl;
|
||||
|
||||
const toggle = () => {
|
||||
if (active) {
|
||||
togglePlay();
|
||||
return;
|
||||
}
|
||||
const item: PlayerItem = {
|
||||
kind: "file",
|
||||
fileUrl,
|
||||
mimeType: dump.fileMime ?? "",
|
||||
title: dump.title,
|
||||
dumpHref: dumpUrl(dump),
|
||||
artworkUrl: fileArtworkUrl(dump, token),
|
||||
subtitle: dump.fileName,
|
||||
};
|
||||
play(item);
|
||||
};
|
||||
|
||||
return { fileUrl, active, playing: active && playing, toggle };
|
||||
}
|
||||
Reference in New Issue
Block a user