v3: added localization, use global player for uploaded audio/video files

This commit is contained in:
khannurien
2026-04-03 15:29:33 +00:00
parent 378b3ffa46
commit 0ce80398a4
64 changed files with 4248 additions and 941 deletions

View File

@@ -1,5 +1,13 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { Link, useNavigate, useParams } from "react-router";
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro";
import { API_URL } from "../config/api.ts";
import type {
PlaylistWithDumps,
@@ -108,7 +116,7 @@ export function PlaylistDetail() {
const fetchAbortRef = useRef<AbortController | null>(null);
const fetchPlaylist = () => {
const fetchPlaylist = useCallback(() => {
if (!playlistId) return;
fetchAbortRef.current?.abort();
const controller = new AbortController();
@@ -152,12 +160,12 @@ export function PlaylistDetail() {
error: friendlyFetchError(err),
});
});
};
}, [playlistId, token]);
useEffect(() => {
fetchPlaylist();
return () => fetchAbortRef.current?.abort();
}, [playlistId]);
}, [fetchPlaylist]);
// Start the cooldown→dismissing→gone sequence for a dump being removed.
// After the sequence completes, the dump is removed from state.playlist.dumps.
@@ -337,7 +345,7 @@ export function PlaylistDetail() {
} else if (ev.type === "deleted") {
navigate("/");
}
}, [lastPlaylistEvent, playlistUUID]);
}, [lastPlaylistEvent, playlistUUID, navigate, token]);
// Filter out globally deleted dumps (dump was deleted entirely, not just removed from playlist)
useEffect(() => {
@@ -579,7 +587,7 @@ export function PlaylistDetail() {
if (state.status === "loading") {
return (
<PageShell>
<p className="page-loading">Loading playlist</p>
<p className="page-loading"><Trans>Loading playlist</Trans></p>
</PageShell>
);
}
@@ -594,7 +602,7 @@ export function PlaylistDetail() {
type="button"
onClick={() => navigate("/")}
>
Back
<Trans> Back</Trans>
</button>
}
/>
@@ -654,21 +662,21 @@ export function PlaylistDetail() {
disabled={editSaving}
onClick={handleEditSave}
>
{editSaving ? "Saving…" : "Save"}
{editSaving ? <Trans>Saving</Trans> : <Trans>Save</Trans>}
</button>
<button
type="button"
className="form-cancel"
onClick={() => setEditOpen(false)}
>
Cancel
<Trans>Cancel</Trans>
</button>
<button
type="button"
className="btn-danger"
onClick={() => setConfirmDelete(true)}
>
Delete
<Trans>Delete</Trans>
</button>
</div>
)
@@ -687,7 +695,7 @@ export function PlaylistDetail() {
className="playlist-edit-btn"
onClick={openEdit}
>
Edit
<Trans>Edit</Trans>
</button>
)}
</div>
@@ -699,7 +707,7 @@ export function PlaylistDetail() {
className="playlist-edit-textarea"
value={editDescription}
onChange={setEditDescription}
placeholder="Description (optional)"
placeholder={t`Description (optional)`}
autoResize
rows={1}
/>
@@ -719,14 +727,14 @@ export function PlaylistDetail() {
className={editIsPublic ? "active" : ""}
onClick={() => setEditIsPublic(true)}
>
Public
<Trans>Public</Trans>
</button>
<button
type="button"
className={!editIsPublic ? "active" : ""}
onClick={() => setEditIsPublic(false)}
>
Private
<Trans>Private</Trans>
</button>
</div>
)
@@ -737,7 +745,7 @@ export function PlaylistDetail() {
playlist.isPublic ? "" : " playlist-badge--private"
}`}
>
{playlist.isPublic ? "public" : "private"}
{playlist.isPublic ? <Trans>public</Trans> : <Trans>private</Trans>}
</span>
{playlist.ownerUsername && (
<Link
@@ -754,10 +762,10 @@ export function PlaylistDetail() {
</Tooltip>
{playlist.updatedAt && (
<Tooltip
text={`Edited ${playlist.updatedAt.toLocaleString()}`}
text={t`Edited ${playlist.updatedAt.toLocaleString()}`}
>
<span className="playlist-edited-label">
edited {relativeTime(playlist.updatedAt)}
<Trans>edited {relativeTime(playlist.updatedAt)}</Trans>
</span>
</Tooltip>
)}
@@ -765,14 +773,14 @@ export function PlaylistDetail() {
)}
</div>
{editError && (
<ErrorCard title="Failed to save" message={editError} />
<ErrorCard title={t`Failed to save`} message={editError} />
)}
</div>
</div>
</div>
{visibleDumps.length === 0
? <p className="empty-state">No dumps in this playlist yet.</p>
? <p className="empty-state"><Trans>No dumps in this playlist yet.</Trans></p>
: (
<div
className="playlist-dump-list"
@@ -827,7 +835,7 @@ export function PlaylistDetail() {
type="button"
className="playlist-remove-btn"
onClick={() => handleRemoveDump(dump.id)}
aria-label="Remove from playlist"
aria-label={t`Remove from playlist`}
>
</button>
@@ -837,9 +845,9 @@ export function PlaylistDetail() {
type="button"
className="playlist-cancel-btn"
onClick={() => handleCancelRemove(dump.id)}
aria-label="Cancel removal"
aria-label={t`Cancel removal`}
>
Undo
<Trans>Undo</Trans>
</button>
))}
</div>
@@ -849,8 +857,8 @@ export function PlaylistDetail() {
)}
{confirmDelete && (
<ConfirmModal
message="Delete this playlist? This cannot be undone."
confirmLabel="Delete playlist"
message={t`Delete this playlist? This cannot be undone.`}
confirmLabel={t`Delete playlist`}
onConfirm={handleDelete}
onCancel={() => setConfirmDelete(false)}
/>