Compare commits
2 Commits
b84fcdd523
...
0e138be6df
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e138be6df | ||
|
|
a9b94c0bfb |
@@ -8,6 +8,7 @@ import { up as up0006Categories } from "./migrations/0006_categories.ts";
|
|||||||
import { up as up0007PasswordResetTokens } from "./migrations/0007_password_reset_tokens.ts";
|
import { up as up0007PasswordResetTokens } from "./migrations/0007_password_reset_tokens.ts";
|
||||||
import { up as up0008ChatMessages } from "./migrations/0008_chat_messages.ts";
|
import { up as up0008ChatMessages } from "./migrations/0008_chat_messages.ts";
|
||||||
import { up as up0009ChatReply } from "./migrations/0009_chat_reply.ts";
|
import { up as up0009ChatReply } from "./migrations/0009_chat_reply.ts";
|
||||||
|
import { up as up0010YoutubeEmbedStart } from "./migrations/0010_youtube_embed_start.ts";
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,6 +28,7 @@ const MIGRATIONS: Migration[] = [
|
|||||||
{ name: "0007_password_reset_tokens", up: up0007PasswordResetTokens },
|
{ name: "0007_password_reset_tokens", up: up0007PasswordResetTokens },
|
||||||
{ name: "0008_chat_messages", up: up0008ChatMessages },
|
{ name: "0008_chat_messages", up: up0008ChatMessages },
|
||||||
{ name: "0009_chat_reply", up: up0009ChatReply },
|
{ name: "0009_chat_reply", up: up0009ChatReply },
|
||||||
|
{ name: "0010_youtube_embed_start", up: up0010YoutubeEmbedStart },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function runMigrations(db: DatabaseSync): void {
|
export function runMigrations(db: DatabaseSync): void {
|
||||||
|
|||||||
76
api/db/migrations/0010_youtube_embed_start.ts
Normal file
76
api/db/migrations/0010_youtube_embed_start.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { DatabaseSync } from "node:sqlite";
|
||||||
|
|
||||||
|
// Backfills the `start` parameter on stored YouTube embed URLs.
|
||||||
|
//
|
||||||
|
// `rich_content.embedUrl` is computed once, at dump creation, and previously
|
||||||
|
// dropped the timestamp carried by the source URL (`?t=1265`, `?t=1h2m3s`), so
|
||||||
|
// those videos always opened at 0:00 in the global player. The provider now
|
||||||
|
// translates `t`/`start` into the `start` param the iframe player honours; this
|
||||||
|
// migration applies the same translation to rows written before that fix.
|
||||||
|
//
|
||||||
|
// Purely local — it re-derives the param from the already-stored source URL and
|
||||||
|
// makes no network calls. Self-contained (no imports from api/services) so it
|
||||||
|
// stays reproducible even if the provider's parsing evolves later.
|
||||||
|
//
|
||||||
|
// Idempotent: rows whose embed URL already carries a `start` are skipped, so a
|
||||||
|
// fresh database built from schema.sql is simply a no-op.
|
||||||
|
|
||||||
|
/** Parse `t`/`start` — plain seconds, `90s`, or `1h2m3s` / `20m5s`. */
|
||||||
|
function extractStartSeconds(url: string): number | null {
|
||||||
|
let raw: string | null;
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
raw = u.searchParams.get("t") ?? u.searchParams.get("start");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
if (/^\d+$/.test(raw)) return Number(raw);
|
||||||
|
|
||||||
|
const hms = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i.exec(raw);
|
||||||
|
if (!hms || (!hms[1] && !hms[2] && !hms[3])) return null;
|
||||||
|
return Number(hms[1] ?? 0) * 3600 + Number(hms[2] ?? 0) * 60 +
|
||||||
|
Number(hms[3] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function up(db: DatabaseSync): void {
|
||||||
|
const rows = db.prepare(
|
||||||
|
`SELECT id, url, rich_content FROM dumps
|
||||||
|
WHERE kind = 'url' AND url IS NOT NULL AND rich_content IS NOT NULL;`,
|
||||||
|
).all() as { id: string; url: string; rich_content: string }[];
|
||||||
|
|
||||||
|
const update = db.prepare(
|
||||||
|
`UPDATE dumps SET rich_content = ? WHERE id = ?;`,
|
||||||
|
);
|
||||||
|
|
||||||
|
let patched = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
let rich: { type?: string; embedUrl?: string };
|
||||||
|
try {
|
||||||
|
rich = JSON.parse(row.rich_content);
|
||||||
|
} catch {
|
||||||
|
continue; // malformed payload — leave it untouched
|
||||||
|
}
|
||||||
|
if (rich.type !== "youtube" || !rich.embedUrl) continue;
|
||||||
|
|
||||||
|
const start = extractStartSeconds(row.url);
|
||||||
|
if (!start) continue;
|
||||||
|
|
||||||
|
let embed: URL;
|
||||||
|
try {
|
||||||
|
embed = new URL(rich.embedUrl);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (embed.searchParams.has("start")) continue;
|
||||||
|
|
||||||
|
embed.searchParams.set("start", String(start));
|
||||||
|
update.run(JSON.stringify({ ...rich, embedUrl: embed.toString() }), row.id);
|
||||||
|
patched++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patched > 0) {
|
||||||
|
console.log(`[migrate] 0010: added start offset to ${patched} embed(s)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
isCreateUrlDumpRequest,
|
isCreateUrlDumpRequest,
|
||||||
isUpdateDumpRequest,
|
isUpdateDumpRequest,
|
||||||
type PaginatedData,
|
type PaginatedData,
|
||||||
|
type Playlist,
|
||||||
} from "../model/interfaces.ts";
|
} from "../model/interfaces.ts";
|
||||||
|
|
||||||
import { authMiddleware } from "../middleware/auth.ts";
|
import { authMiddleware } from "../middleware/auth.ts";
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
} from "../services/dump-service.ts";
|
} from "../services/dump-service.ts";
|
||||||
import { getDumpVoters } from "../services/vote-service.ts";
|
import { getDumpVoters } from "../services/vote-service.ts";
|
||||||
import { getRelatedDumps } from "../services/backlink-service.ts";
|
import { getRelatedDumps } from "../services/backlink-service.ts";
|
||||||
|
import { getPlaylistsForDump } from "../services/playlist-service.ts";
|
||||||
|
|
||||||
const router = new Router({ prefix: "/api/dumps" });
|
const router = new Router({ prefix: "/api/dumps" });
|
||||||
|
|
||||||
@@ -128,6 +130,18 @@ router.get("/:dumpId/related", async (ctx) => {
|
|||||||
ctx.response.body = responseBody;
|
ctx.response.body = responseBody;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/:dumpId/playlists", async (ctx) => {
|
||||||
|
const requestingUserId = await parseOptionalAuth(ctx) ?? undefined;
|
||||||
|
// Resolve through getDump so private dumps 404 for anyone but their owner.
|
||||||
|
const dump = getDump(ctx.params.dumpId, requestingUserId);
|
||||||
|
const playlists = getPlaylistsForDump(dump.id, requestingUserId);
|
||||||
|
const responseBody: APIResponse<Playlist[]> = {
|
||||||
|
success: true,
|
||||||
|
data: playlists,
|
||||||
|
};
|
||||||
|
ctx.response.body = responseBody;
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/", async (ctx) => {
|
router.get("/", async (ctx) => {
|
||||||
const requestingUserId = await parseOptionalAuth(ctx) ?? undefined;
|
const requestingUserId = await parseOptionalAuth(ctx) ?? undefined;
|
||||||
const { page, limit } = parsePagination(ctx.request.url.searchParams);
|
const { page, limit } = parsePagination(ctx.request.url.searchParams);
|
||||||
|
|||||||
@@ -382,6 +382,26 @@ export function getPlaylistMembershipsForDump(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The playlists a dump appears in — the "in collections" counterpart to
|
||||||
|
* backlinks. Privacy-filtered like `getRelatedDumps`: public playlists for
|
||||||
|
* everyone, plus the requesting user's own private ones. Capped at 20 — a
|
||||||
|
* small, bounded list of supporting context.
|
||||||
|
*/
|
||||||
|
export function getPlaylistsForDump(
|
||||||
|
dumpId: string,
|
||||||
|
requestingUserId?: string,
|
||||||
|
): Playlist[] {
|
||||||
|
const rows = db.prepare(
|
||||||
|
`SELECT ${PLAYLIST_SELECT}
|
||||||
|
INNER JOIN playlist_dumps pd ON pd.playlist_id = p.id
|
||||||
|
WHERE pd.dump_id = ? AND (p.is_public = 1 OR p.user_id = ?)
|
||||||
|
ORDER BY p.created_at DESC LIMIT 20;`,
|
||||||
|
).all(dumpId, requestingUserId ?? "");
|
||||||
|
|
||||||
|
return rows.filter(isPlaylistRow).map(playlistRowToApi);
|
||||||
|
}
|
||||||
|
|
||||||
export function getPlaylistImageInfo(
|
export function getPlaylistImageInfo(
|
||||||
idOrSlug: string,
|
idOrSlug: string,
|
||||||
): { id: string; imageMime: string } | undefined {
|
): { id: string; imageMime: string } | undefined {
|
||||||
|
|||||||
@@ -32,6 +32,30 @@ function extractPlaylistId(url: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the playback offset in seconds from a YouTube URL.
|
||||||
|
* Accepts `t` (plain seconds, `90s`, or `1h2m3s` / `20m5s` forms) and `start`
|
||||||
|
* (plain seconds). Returns null when absent or unparseable.
|
||||||
|
*/
|
||||||
|
function extractStartSeconds(url: string): number | null {
|
||||||
|
let raw: string | null;
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
raw = u.searchParams.get("t") ?? u.searchParams.get("start");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!raw) return null;
|
||||||
|
|
||||||
|
const plain = /^\d+$/.exec(raw);
|
||||||
|
if (plain) return Number(raw);
|
||||||
|
|
||||||
|
const hms = /^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/i.exec(raw);
|
||||||
|
if (!hms || (!hms[1] && !hms[2] && !hms[3])) return null;
|
||||||
|
return Number(hms[1] ?? 0) * 3600 + Number(hms[2] ?? 0) * 60 +
|
||||||
|
Number(hms[3] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
/** Matches /channel/UC…, /@handle, /c/name, /user/name */
|
/** Matches /channel/UC…, /@handle, /c/name, /user/name */
|
||||||
function extractChannelPath(url: string): string | null {
|
function extractChannelPath(url: string): string | null {
|
||||||
try {
|
try {
|
||||||
@@ -134,6 +158,8 @@ export const youtubeProvider: RichContentProvider = {
|
|||||||
|
|
||||||
const embedParams = new URLSearchParams({ rel: "0" });
|
const embedParams = new URLSearchParams({ rel: "0" });
|
||||||
if (listId) embedParams.set("list", listId);
|
if (listId) embedParams.set("list", listId);
|
||||||
|
const start = extractStartSeconds(url);
|
||||||
|
if (start) embedParams.set("start", String(start));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "youtube",
|
type: "youtube",
|
||||||
|
|||||||
47
src/App.css
47
src/App.css
@@ -4155,6 +4155,53 @@ body.has-player .chat-fab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "In collections" reuses the related-section chrome (and its per-theme
|
||||||
|
overrides), so the playlist cards inside it get the same subordinate,
|
||||||
|
tightened treatment the backlink dump cards do. */
|
||||||
|
.collections-section .playlist-card {
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
opacity: 0.78;
|
||||||
|
transition: border-color 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section .playlist-card:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section .playlist-card-inner {
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section .playlist-card-preview {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section .playlist-card-icon {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section .playlist-card-title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collections-section .playlist-card-description {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
.collections-section .playlist-card-preview {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.comment-list {
|
.comment-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -30,12 +30,12 @@ msgstr "{0, plural, one {# dump} other {# dumps}}"
|
|||||||
|
|
||||||
#. placeholder {0}: names[0]
|
#. placeholder {0}: names[0]
|
||||||
#. placeholder {1}: names[1]
|
#. placeholder {1}: names[1]
|
||||||
#: src/components/ChatModal.tsx:528
|
#: src/components/ChatModal.tsx:531
|
||||||
msgid "{0} and {1} are typing…"
|
msgid "{0} and {1} are typing…"
|
||||||
msgstr "{0} and {1} are typing…"
|
msgstr "{0} and {1} are typing…"
|
||||||
|
|
||||||
#. placeholder {0}: names[0]
|
#. placeholder {0}: names[0]
|
||||||
#: src/components/ChatModal.tsx:527
|
#: src/components/ChatModal.tsx:530
|
||||||
msgid "{0} is typing…"
|
msgid "{0} is typing…"
|
||||||
msgstr "{0} is typing…"
|
msgstr "{0} is typing…"
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ msgstr "{visibleCount, plural, one {# comment} other {# comments}}"
|
|||||||
msgid "← Back"
|
msgid "← Back"
|
||||||
msgstr "← Back"
|
msgstr "← Back"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:264
|
#: src/pages/Dump.tsx:291
|
||||||
#: src/pages/Dump.tsx:494
|
#: src/pages/Dump.tsx:521
|
||||||
#: src/pages/DumpEdit.tsx:181
|
#: src/pages/DumpEdit.tsx:181
|
||||||
msgid "← Back to all dumps"
|
msgid "← Back to all dumps"
|
||||||
msgstr "← Back to all dumps"
|
msgstr "← Back to all dumps"
|
||||||
@@ -78,7 +78,7 @@ msgstr "+ Invite someone"
|
|||||||
msgid "+ New playlist"
|
msgid "+ New playlist"
|
||||||
msgstr "+ New playlist"
|
msgstr "+ New playlist"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:335
|
#: src/pages/Dump.tsx:362
|
||||||
msgid "+ Playlist"
|
msgid "+ Playlist"
|
||||||
msgstr "+ Playlist"
|
msgstr "+ Playlist"
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
|
|||||||
#: src/components/CommentThread.tsx:124
|
#: src/components/CommentThread.tsx:124
|
||||||
#: src/components/ConfirmModal.tsx:32
|
#: src/components/ConfirmModal.tsx:32
|
||||||
#: src/components/form/FormActions.tsx:32
|
#: src/components/form/FormActions.tsx:32
|
||||||
#: src/pages/Dump.tsx:376
|
#: src/pages/Dump.tsx:403
|
||||||
#: src/pages/DumpEdit.tsx:460
|
#: src/pages/DumpEdit.tsx:460
|
||||||
#: src/pages/PlaylistDetail.tsx:920
|
#: src/pages/PlaylistDetail.tsx:920
|
||||||
#: src/pages/UserPublicProfile.tsx:1674
|
#: src/pages/UserPublicProfile.tsx:1674
|
||||||
@@ -214,8 +214,8 @@ msgstr "Cancel"
|
|||||||
msgid "Cancel removal"
|
msgid "Cancel removal"
|
||||||
msgstr "Cancel removal"
|
msgstr "Cancel removal"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:619
|
#: src/components/ChatModal.tsx:622
|
||||||
#: src/components/ChatModal.tsx:620
|
#: src/components/ChatModal.tsx:623
|
||||||
msgid "Cancel reply"
|
msgid "Cancel reply"
|
||||||
msgstr "Cancel reply"
|
msgstr "Cancel reply"
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ msgstr "Change password…"
|
|||||||
#: src/components/ChatButton.tsx:17
|
#: src/components/ChatButton.tsx:17
|
||||||
#: src/components/ChatFab.tsx:49
|
#: src/components/ChatFab.tsx:49
|
||||||
#: src/components/ChatFab.tsx:50
|
#: src/components/ChatFab.tsx:50
|
||||||
#: src/components/ChatModal.tsx:533
|
#: src/components/ChatModal.tsx:536
|
||||||
msgid "Chat"
|
msgid "Chat"
|
||||||
msgstr "Chat"
|
msgstr "Chat"
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ msgstr "Earlier"
|
|||||||
#: src/components/ChatModal.tsx:172
|
#: src/components/ChatModal.tsx:172
|
||||||
#: src/components/ChatModal.tsx:173
|
#: src/components/ChatModal.tsx:173
|
||||||
#: src/components/CommentThread.tsx:367
|
#: src/components/CommentThread.tsx:367
|
||||||
#: src/pages/Dump.tsx:490
|
#: src/pages/Dump.tsx:517
|
||||||
#: src/pages/PlaylistDetail.tsx:625
|
#: src/pages/PlaylistDetail.tsx:625
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Edit"
|
msgstr "Edit"
|
||||||
@@ -445,7 +445,7 @@ msgstr "Edit title"
|
|||||||
#. placeholder {0}: relativeTime(message.updatedAt)
|
#. placeholder {0}: relativeTime(message.updatedAt)
|
||||||
#: src/components/ChatModal.tsx:152
|
#: src/components/ChatModal.tsx:152
|
||||||
#: src/components/CommentThread.tsx:317
|
#: src/components/CommentThread.tsx:317
|
||||||
#: src/pages/Dump.tsx:429
|
#: src/pages/Dump.tsx:456
|
||||||
#: src/pages/PlaylistDetail.tsx:664
|
#: src/pages/PlaylistDetail.tsx:664
|
||||||
msgid "edited {0}"
|
msgid "edited {0}"
|
||||||
msgstr "edited {0}"
|
msgstr "edited {0}"
|
||||||
@@ -455,7 +455,7 @@ msgstr "edited {0}"
|
|||||||
#. placeholder {0}: message.updatedAt.toLocaleString()
|
#. placeholder {0}: message.updatedAt.toLocaleString()
|
||||||
#: src/components/ChatModal.tsx:150
|
#: src/components/ChatModal.tsx:150
|
||||||
#: src/components/CommentThread.tsx:315
|
#: src/components/CommentThread.tsx:315
|
||||||
#: src/pages/Dump.tsx:427
|
#: src/pages/Dump.tsx:454
|
||||||
#: src/pages/PlaylistDetail.tsx:661
|
#: src/pages/PlaylistDetail.tsx:661
|
||||||
msgid "Edited {0}"
|
msgid "Edited {0}"
|
||||||
msgstr "Edited {0}"
|
msgstr "Edited {0}"
|
||||||
@@ -632,6 +632,10 @@ msgstr "Hot"
|
|||||||
msgid "If that address is registered you'll receive a reset link shortly."
|
msgid "If that address is registered you'll receive a reset link shortly."
|
||||||
msgstr "If that address is registered you'll receive a reset link shortly."
|
msgstr "If that address is registered you'll receive a reset link shortly."
|
||||||
|
|
||||||
|
#: src/pages/Dump.tsx:551
|
||||||
|
msgid "In collections"
|
||||||
|
msgstr "In collections"
|
||||||
|
|
||||||
#: src/pages/UserRegister.tsx:96
|
#: src/pages/UserRegister.tsx:96
|
||||||
msgid "Invalid invite"
|
msgid "Invalid invite"
|
||||||
msgstr "Invalid invite"
|
msgstr "Invalid invite"
|
||||||
@@ -675,11 +679,11 @@ msgstr "Live updates unavailable."
|
|||||||
msgid "Load more"
|
msgid "Load more"
|
||||||
msgstr "Load more"
|
msgstr "Load more"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:546
|
#: src/components/ChatModal.tsx:549
|
||||||
msgid "Load older messages"
|
msgid "Load older messages"
|
||||||
msgstr "Load older messages"
|
msgstr "Load older messages"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:240
|
#: src/pages/Dump.tsx:267
|
||||||
#: src/pages/DumpEdit.tsx:157
|
#: src/pages/DumpEdit.tsx:157
|
||||||
msgid "Loading dump…"
|
msgid "Loading dump…"
|
||||||
msgstr "Loading dump…"
|
msgstr "Loading dump…"
|
||||||
@@ -705,7 +709,7 @@ msgid "Loading profile…"
|
|||||||
msgstr "Loading profile…"
|
msgstr "Loading profile…"
|
||||||
|
|
||||||
#: src/components/CategoryManager.tsx:52
|
#: src/components/CategoryManager.tsx:52
|
||||||
#: src/components/ChatModal.tsx:545
|
#: src/components/ChatModal.tsx:548
|
||||||
#: src/components/PlaylistMembershipPanel.tsx:28
|
#: src/components/PlaylistMembershipPanel.tsx:28
|
||||||
#: src/components/TextEditor.tsx:289
|
#: src/components/TextEditor.tsx:289
|
||||||
#: src/components/UserListPopover.tsx:192
|
#: src/components/UserListPopover.tsx:192
|
||||||
@@ -824,7 +828,7 @@ msgstr "No followed playlists yet."
|
|||||||
msgid "No invitees yet."
|
msgid "No invitees yet."
|
||||||
msgstr "No invitees yet."
|
msgstr "No invitees yet."
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:553
|
#: src/components/ChatModal.tsx:556
|
||||||
msgid "No messages yet. Say hello!"
|
msgid "No messages yet. Say hello!"
|
||||||
msgstr "No messages yet. Say hello!"
|
msgstr "No messages yet. Say hello!"
|
||||||
|
|
||||||
@@ -942,7 +946,7 @@ msgstr "Posting…"
|
|||||||
#: src/components/JournalCard.tsx:121
|
#: src/components/JournalCard.tsx:121
|
||||||
#: src/components/PlaylistCard.tsx:73
|
#: src/components/PlaylistCard.tsx:73
|
||||||
#: src/components/PlaylistMembershipPanel.tsx:55
|
#: src/components/PlaylistMembershipPanel.tsx:55
|
||||||
#: src/pages/Dump.tsx:435
|
#: src/pages/Dump.tsx:462
|
||||||
#: src/pages/PlaylistDetail.tsx:644
|
#: src/pages/PlaylistDetail.tsx:644
|
||||||
msgid "private"
|
msgid "private"
|
||||||
msgstr "private"
|
msgstr "private"
|
||||||
@@ -984,7 +988,7 @@ msgstr "Registering…"
|
|||||||
msgid "Registration failed"
|
msgid "Registration failed"
|
||||||
msgstr "Registration failed"
|
msgstr "Registration failed"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:502
|
#: src/pages/Dump.tsx:529
|
||||||
msgid "Related"
|
msgid "Related"
|
||||||
msgstr "Related"
|
msgstr "Related"
|
||||||
|
|
||||||
@@ -1014,7 +1018,7 @@ msgstr "Replace file"
|
|||||||
msgid "Reply"
|
msgid "Reply"
|
||||||
msgstr "Reply"
|
msgstr "Reply"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:609
|
#: src/components/ChatModal.tsx:612
|
||||||
msgid "Replying to"
|
msgid "Replying to"
|
||||||
msgstr "Replying to"
|
msgstr "Replying to"
|
||||||
|
|
||||||
@@ -1035,7 +1039,7 @@ msgstr "Reset password"
|
|||||||
msgid "Reset to default"
|
msgid "Reset to default"
|
||||||
msgstr "Reset to default"
|
msgstr "Reset to default"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:257
|
#: src/pages/Dump.tsx:284
|
||||||
#: src/pages/DumpEdit.tsx:174
|
#: src/pages/DumpEdit.tsx:174
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr "Retry"
|
msgstr "Retry"
|
||||||
@@ -1046,7 +1050,7 @@ msgstr "Role"
|
|||||||
|
|
||||||
#: src/components/ChatModal.tsx:222
|
#: src/components/ChatModal.tsx:222
|
||||||
#: src/components/CommentThread.tsx:328
|
#: src/components/CommentThread.tsx:328
|
||||||
#: src/pages/Dump.tsx:368
|
#: src/pages/Dump.tsx:395
|
||||||
#: src/pages/DumpEdit.tsx:463
|
#: src/pages/DumpEdit.tsx:463
|
||||||
#: src/pages/PlaylistDetail.tsx:927
|
#: src/pages/PlaylistDetail.tsx:927
|
||||||
#: src/pages/UserPublicProfile.tsx:1666
|
#: src/pages/UserPublicProfile.tsx:1666
|
||||||
@@ -1056,7 +1060,7 @@ msgstr "Save"
|
|||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:100
|
#: src/components/ChangePasswordModal.tsx:100
|
||||||
#: src/components/CommentThread.tsx:329
|
#: src/components/CommentThread.tsx:329
|
||||||
#: src/pages/Dump.tsx:367
|
#: src/pages/Dump.tsx:394
|
||||||
#: src/pages/PlaylistDetail.tsx:923
|
#: src/pages/PlaylistDetail.tsx:923
|
||||||
#: src/pages/ResetPassword.tsx:126
|
#: src/pages/ResetPassword.tsx:126
|
||||||
#: src/pages/UserPublicProfile.tsx:1663
|
#: src/pages/UserPublicProfile.tsx:1663
|
||||||
@@ -1082,7 +1086,7 @@ msgstr "Search failed"
|
|||||||
msgid "Searching…"
|
msgid "Searching…"
|
||||||
msgstr "Searching…"
|
msgstr "Searching…"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:655
|
#: src/components/ChatModal.tsx:658
|
||||||
msgid "Send"
|
msgid "Send"
|
||||||
msgstr "Send"
|
msgstr "Send"
|
||||||
|
|
||||||
@@ -1107,7 +1111,7 @@ msgstr "Set new password"
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Settings"
|
msgstr "Settings"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:529
|
#: src/components/ChatModal.tsx:532
|
||||||
msgid "Several people are typing…"
|
msgid "Several people are typing…"
|
||||||
msgstr "Several people are typing…"
|
msgstr "Several people are typing…"
|
||||||
|
|
||||||
@@ -1167,7 +1171,7 @@ msgstr "Title is required."
|
|||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr "Today"
|
msgstr "Today"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:632
|
#: src/components/ChatModal.tsx:635
|
||||||
msgid "Type a message…"
|
msgid "Type a message…"
|
||||||
msgstr "Type a message…"
|
msgstr "Type a message…"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -30,12 +30,12 @@ msgstr "{0, plural, one {# reco} other {# recos}}"
|
|||||||
|
|
||||||
#. placeholder {0}: names[0]
|
#. placeholder {0}: names[0]
|
||||||
#. placeholder {1}: names[1]
|
#. placeholder {1}: names[1]
|
||||||
#: src/components/ChatModal.tsx:528
|
#: src/components/ChatModal.tsx:531
|
||||||
msgid "{0} and {1} are typing…"
|
msgid "{0} and {1} are typing…"
|
||||||
msgstr "{0} et {1} sont en train d'écrire…"
|
msgstr "{0} et {1} sont en train d'écrire…"
|
||||||
|
|
||||||
#. placeholder {0}: names[0]
|
#. placeholder {0}: names[0]
|
||||||
#: src/components/ChatModal.tsx:527
|
#: src/components/ChatModal.tsx:530
|
||||||
msgid "{0} is typing…"
|
msgid "{0} is typing…"
|
||||||
msgstr "{0} est en train d'écrire…"
|
msgstr "{0} est en train d'écrire…"
|
||||||
|
|
||||||
@@ -58,8 +58,8 @@ msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
|
|||||||
msgid "← Back"
|
msgid "← Back"
|
||||||
msgstr "← Retour"
|
msgstr "← Retour"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:264
|
#: src/pages/Dump.tsx:291
|
||||||
#: src/pages/Dump.tsx:494
|
#: src/pages/Dump.tsx:521
|
||||||
#: src/pages/DumpEdit.tsx:181
|
#: src/pages/DumpEdit.tsx:181
|
||||||
msgid "← Back to all dumps"
|
msgid "← Back to all dumps"
|
||||||
msgstr "← Retour à toutes les recos"
|
msgstr "← Retour à toutes les recos"
|
||||||
@@ -78,7 +78,7 @@ msgstr "+ Inviter quelqu'un"
|
|||||||
msgid "+ New playlist"
|
msgid "+ New playlist"
|
||||||
msgstr "+ Nouvelle collection"
|
msgstr "+ Nouvelle collection"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:335
|
#: src/pages/Dump.tsx:362
|
||||||
msgid "+ Playlist"
|
msgid "+ Playlist"
|
||||||
msgstr "+ Collection"
|
msgstr "+ Collection"
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
|
|||||||
#: src/components/CommentThread.tsx:124
|
#: src/components/CommentThread.tsx:124
|
||||||
#: src/components/ConfirmModal.tsx:32
|
#: src/components/ConfirmModal.tsx:32
|
||||||
#: src/components/form/FormActions.tsx:32
|
#: src/components/form/FormActions.tsx:32
|
||||||
#: src/pages/Dump.tsx:376
|
#: src/pages/Dump.tsx:403
|
||||||
#: src/pages/DumpEdit.tsx:460
|
#: src/pages/DumpEdit.tsx:460
|
||||||
#: src/pages/PlaylistDetail.tsx:920
|
#: src/pages/PlaylistDetail.tsx:920
|
||||||
#: src/pages/UserPublicProfile.tsx:1674
|
#: src/pages/UserPublicProfile.tsx:1674
|
||||||
@@ -214,8 +214,8 @@ msgstr "Annuler"
|
|||||||
msgid "Cancel removal"
|
msgid "Cancel removal"
|
||||||
msgstr "Annuler la suppression"
|
msgstr "Annuler la suppression"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:619
|
#: src/components/ChatModal.tsx:622
|
||||||
#: src/components/ChatModal.tsx:620
|
#: src/components/ChatModal.tsx:623
|
||||||
msgid "Cancel reply"
|
msgid "Cancel reply"
|
||||||
msgstr "Annuler la réponse"
|
msgstr "Annuler la réponse"
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ msgstr "Changer le mot de passe…"
|
|||||||
#: src/components/ChatButton.tsx:17
|
#: src/components/ChatButton.tsx:17
|
||||||
#: src/components/ChatFab.tsx:49
|
#: src/components/ChatFab.tsx:49
|
||||||
#: src/components/ChatFab.tsx:50
|
#: src/components/ChatFab.tsx:50
|
||||||
#: src/components/ChatModal.tsx:533
|
#: src/components/ChatModal.tsx:536
|
||||||
msgid "Chat"
|
msgid "Chat"
|
||||||
msgstr "Tribune"
|
msgstr "Tribune"
|
||||||
|
|
||||||
@@ -426,7 +426,7 @@ msgstr "Plus tôt"
|
|||||||
#: src/components/ChatModal.tsx:172
|
#: src/components/ChatModal.tsx:172
|
||||||
#: src/components/ChatModal.tsx:173
|
#: src/components/ChatModal.tsx:173
|
||||||
#: src/components/CommentThread.tsx:367
|
#: src/components/CommentThread.tsx:367
|
||||||
#: src/pages/Dump.tsx:490
|
#: src/pages/Dump.tsx:517
|
||||||
#: src/pages/PlaylistDetail.tsx:625
|
#: src/pages/PlaylistDetail.tsx:625
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Modifier"
|
msgstr "Modifier"
|
||||||
@@ -445,7 +445,7 @@ msgstr "Modifier le titre"
|
|||||||
#. placeholder {0}: relativeTime(message.updatedAt)
|
#. placeholder {0}: relativeTime(message.updatedAt)
|
||||||
#: src/components/ChatModal.tsx:152
|
#: src/components/ChatModal.tsx:152
|
||||||
#: src/components/CommentThread.tsx:317
|
#: src/components/CommentThread.tsx:317
|
||||||
#: src/pages/Dump.tsx:429
|
#: src/pages/Dump.tsx:456
|
||||||
#: src/pages/PlaylistDetail.tsx:664
|
#: src/pages/PlaylistDetail.tsx:664
|
||||||
msgid "edited {0}"
|
msgid "edited {0}"
|
||||||
msgstr "modifié {0}"
|
msgstr "modifié {0}"
|
||||||
@@ -455,7 +455,7 @@ msgstr "modifié {0}"
|
|||||||
#. placeholder {0}: message.updatedAt.toLocaleString()
|
#. placeholder {0}: message.updatedAt.toLocaleString()
|
||||||
#: src/components/ChatModal.tsx:150
|
#: src/components/ChatModal.tsx:150
|
||||||
#: src/components/CommentThread.tsx:315
|
#: src/components/CommentThread.tsx:315
|
||||||
#: src/pages/Dump.tsx:427
|
#: src/pages/Dump.tsx:454
|
||||||
#: src/pages/PlaylistDetail.tsx:661
|
#: src/pages/PlaylistDetail.tsx:661
|
||||||
msgid "Edited {0}"
|
msgid "Edited {0}"
|
||||||
msgstr "Modifié le {0}"
|
msgstr "Modifié le {0}"
|
||||||
@@ -632,6 +632,10 @@ msgstr "Tendances"
|
|||||||
msgid "If that address is registered you'll receive a reset link shortly."
|
msgid "If that address is registered you'll receive a reset link shortly."
|
||||||
msgstr "Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu."
|
msgstr "Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu."
|
||||||
|
|
||||||
|
#: src/pages/Dump.tsx:551
|
||||||
|
msgid "In collections"
|
||||||
|
msgstr "Dans les collections"
|
||||||
|
|
||||||
#: src/pages/UserRegister.tsx:96
|
#: src/pages/UserRegister.tsx:96
|
||||||
msgid "Invalid invite"
|
msgid "Invalid invite"
|
||||||
msgstr "Invitation invalide"
|
msgstr "Invitation invalide"
|
||||||
@@ -675,11 +679,11 @@ msgstr "Mises à jour en direct indisponibles."
|
|||||||
msgid "Load more"
|
msgid "Load more"
|
||||||
msgstr "Charger plus"
|
msgstr "Charger plus"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:546
|
#: src/components/ChatModal.tsx:549
|
||||||
msgid "Load older messages"
|
msgid "Load older messages"
|
||||||
msgstr "Charger les messages plus anciens"
|
msgstr "Charger les messages plus anciens"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:240
|
#: src/pages/Dump.tsx:267
|
||||||
#: src/pages/DumpEdit.tsx:157
|
#: src/pages/DumpEdit.tsx:157
|
||||||
msgid "Loading dump…"
|
msgid "Loading dump…"
|
||||||
msgstr "Chargement de la reco…"
|
msgstr "Chargement de la reco…"
|
||||||
@@ -705,7 +709,7 @@ msgid "Loading profile…"
|
|||||||
msgstr "Chargement du profil…"
|
msgstr "Chargement du profil…"
|
||||||
|
|
||||||
#: src/components/CategoryManager.tsx:52
|
#: src/components/CategoryManager.tsx:52
|
||||||
#: src/components/ChatModal.tsx:545
|
#: src/components/ChatModal.tsx:548
|
||||||
#: src/components/PlaylistMembershipPanel.tsx:28
|
#: src/components/PlaylistMembershipPanel.tsx:28
|
||||||
#: src/components/TextEditor.tsx:289
|
#: src/components/TextEditor.tsx:289
|
||||||
#: src/components/UserListPopover.tsx:192
|
#: src/components/UserListPopover.tsx:192
|
||||||
@@ -824,7 +828,7 @@ msgstr "Pas encore de collections suivies."
|
|||||||
msgid "No invitees yet."
|
msgid "No invitees yet."
|
||||||
msgstr "Aucun invité pour le moment."
|
msgstr "Aucun invité pour le moment."
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:553
|
#: src/components/ChatModal.tsx:556
|
||||||
msgid "No messages yet. Say hello!"
|
msgid "No messages yet. Say hello!"
|
||||||
msgstr "Aucun message pour l'instant. Dites bonjour !"
|
msgstr "Aucun message pour l'instant. Dites bonjour !"
|
||||||
|
|
||||||
@@ -942,7 +946,7 @@ msgstr "Publication…"
|
|||||||
#: src/components/JournalCard.tsx:121
|
#: src/components/JournalCard.tsx:121
|
||||||
#: src/components/PlaylistCard.tsx:73
|
#: src/components/PlaylistCard.tsx:73
|
||||||
#: src/components/PlaylistMembershipPanel.tsx:55
|
#: src/components/PlaylistMembershipPanel.tsx:55
|
||||||
#: src/pages/Dump.tsx:435
|
#: src/pages/Dump.tsx:462
|
||||||
#: src/pages/PlaylistDetail.tsx:644
|
#: src/pages/PlaylistDetail.tsx:644
|
||||||
msgid "private"
|
msgid "private"
|
||||||
msgstr "privé"
|
msgstr "privé"
|
||||||
@@ -984,7 +988,7 @@ msgstr "Inscription…"
|
|||||||
msgid "Registration failed"
|
msgid "Registration failed"
|
||||||
msgstr "Inscription échouée"
|
msgstr "Inscription échouée"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:502
|
#: src/pages/Dump.tsx:529
|
||||||
msgid "Related"
|
msgid "Related"
|
||||||
msgstr "Connexe"
|
msgstr "Connexe"
|
||||||
|
|
||||||
@@ -1014,7 +1018,7 @@ msgstr "Remplacer le fichier"
|
|||||||
msgid "Reply"
|
msgid "Reply"
|
||||||
msgstr "Répondre"
|
msgstr "Répondre"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:609
|
#: src/components/ChatModal.tsx:612
|
||||||
msgid "Replying to"
|
msgid "Replying to"
|
||||||
msgstr "En réponse à"
|
msgstr "En réponse à"
|
||||||
|
|
||||||
@@ -1035,7 +1039,7 @@ msgstr "Réinitialiser le mot de passe"
|
|||||||
msgid "Reset to default"
|
msgid "Reset to default"
|
||||||
msgstr "Réinitialiser par défaut"
|
msgstr "Réinitialiser par défaut"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:257
|
#: src/pages/Dump.tsx:284
|
||||||
#: src/pages/DumpEdit.tsx:174
|
#: src/pages/DumpEdit.tsx:174
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr "Réessayer"
|
msgstr "Réessayer"
|
||||||
@@ -1046,7 +1050,7 @@ msgstr "Rôle"
|
|||||||
|
|
||||||
#: src/components/ChatModal.tsx:222
|
#: src/components/ChatModal.tsx:222
|
||||||
#: src/components/CommentThread.tsx:328
|
#: src/components/CommentThread.tsx:328
|
||||||
#: src/pages/Dump.tsx:368
|
#: src/pages/Dump.tsx:395
|
||||||
#: src/pages/DumpEdit.tsx:463
|
#: src/pages/DumpEdit.tsx:463
|
||||||
#: src/pages/PlaylistDetail.tsx:927
|
#: src/pages/PlaylistDetail.tsx:927
|
||||||
#: src/pages/UserPublicProfile.tsx:1666
|
#: src/pages/UserPublicProfile.tsx:1666
|
||||||
@@ -1056,7 +1060,7 @@ msgstr "Enregistrer"
|
|||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:100
|
#: src/components/ChangePasswordModal.tsx:100
|
||||||
#: src/components/CommentThread.tsx:329
|
#: src/components/CommentThread.tsx:329
|
||||||
#: src/pages/Dump.tsx:367
|
#: src/pages/Dump.tsx:394
|
||||||
#: src/pages/PlaylistDetail.tsx:923
|
#: src/pages/PlaylistDetail.tsx:923
|
||||||
#: src/pages/ResetPassword.tsx:126
|
#: src/pages/ResetPassword.tsx:126
|
||||||
#: src/pages/UserPublicProfile.tsx:1663
|
#: src/pages/UserPublicProfile.tsx:1663
|
||||||
@@ -1082,7 +1086,7 @@ msgstr "Recherche échouée"
|
|||||||
msgid "Searching…"
|
msgid "Searching…"
|
||||||
msgstr "Recherche…"
|
msgstr "Recherche…"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:655
|
#: src/components/ChatModal.tsx:658
|
||||||
msgid "Send"
|
msgid "Send"
|
||||||
msgstr "Envoyer"
|
msgstr "Envoyer"
|
||||||
|
|
||||||
@@ -1107,7 +1111,7 @@ msgstr "Définir un nouveau mot de passe"
|
|||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "Paramètres"
|
msgstr "Paramètres"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:529
|
#: src/components/ChatModal.tsx:532
|
||||||
msgid "Several people are typing…"
|
msgid "Several people are typing…"
|
||||||
msgstr "Plusieurs personnes écrivent…"
|
msgstr "Plusieurs personnes écrivent…"
|
||||||
|
|
||||||
@@ -1167,7 +1171,7 @@ msgstr "Un titre est requis."
|
|||||||
msgid "Today"
|
msgid "Today"
|
||||||
msgstr "Aujourd'hui"
|
msgstr "Aujourd'hui"
|
||||||
|
|
||||||
#: src/components/ChatModal.tsx:632
|
#: src/components/ChatModal.tsx:635
|
||||||
msgid "Type a message…"
|
msgid "Type a message…"
|
||||||
msgstr "Écrivez un message…"
|
msgstr "Écrivez un message…"
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,17 @@ import { API_URL, VALIDATION } from "../config/api.ts";
|
|||||||
import type {
|
import type {
|
||||||
Comment,
|
Comment,
|
||||||
Dump,
|
Dump,
|
||||||
|
Playlist,
|
||||||
PublicUser,
|
PublicUser,
|
||||||
RawComment,
|
RawComment,
|
||||||
RawDump,
|
RawDump,
|
||||||
|
RawPlaylist,
|
||||||
UpdateDumpRequest,
|
UpdateDumpRequest,
|
||||||
} from "../model.ts";
|
} from "../model.ts";
|
||||||
import {
|
import {
|
||||||
deserializeComment,
|
deserializeComment,
|
||||||
deserializeDump,
|
deserializeDump,
|
||||||
|
deserializePlaylist,
|
||||||
deserializePublicUser,
|
deserializePublicUser,
|
||||||
parseAPIResponse,
|
parseAPIResponse,
|
||||||
} from "../model.ts";
|
} from "../model.ts";
|
||||||
@@ -33,6 +36,7 @@ import RichContentCard from "../components/RichContentCard.tsx";
|
|||||||
import FilePreview from "../components/FilePreview.tsx";
|
import FilePreview from "../components/FilePreview.tsx";
|
||||||
import { VoteButton } from "../components/VoteButton.tsx";
|
import { VoteButton } from "../components/VoteButton.tsx";
|
||||||
import { DumpCard } from "../components/DumpCard.tsx";
|
import { DumpCard } from "../components/DumpCard.tsx";
|
||||||
|
import { PlaylistCard } from "../components/PlaylistCard.tsx";
|
||||||
import { PageShell } from "../components/PageShell.tsx";
|
import { PageShell } from "../components/PageShell.tsx";
|
||||||
import { PageError } from "../components/PageError.tsx";
|
import { PageError } from "../components/PageError.tsx";
|
||||||
import { Markdown } from "../components/Markdown.tsx";
|
import { Markdown } from "../components/Markdown.tsx";
|
||||||
@@ -67,6 +71,7 @@ export function Dump() {
|
|||||||
|
|
||||||
const [comments, setComments] = useState<Comment[]>([]);
|
const [comments, setComments] = useState<Comment[]>([]);
|
||||||
const [relatedDumps, setRelatedDumps] = useState<Dump[]>([]);
|
const [relatedDumps, setRelatedDumps] = useState<Dump[]>([]);
|
||||||
|
const [collections, setCollections] = useState<Playlist[]>([]);
|
||||||
|
|
||||||
const [titleEditing, setTitleEditing] = useState(false);
|
const [titleEditing, setTitleEditing] = useState(false);
|
||||||
const [titleDraft, setTitleDraft] = useState("");
|
const [titleDraft, setTitleDraft] = useState("");
|
||||||
@@ -82,6 +87,7 @@ export function Dump() {
|
|||||||
removeVote,
|
removeVote,
|
||||||
lastDumpEvent,
|
lastDumpEvent,
|
||||||
lastCommentEvent,
|
lastCommentEvent,
|
||||||
|
lastPlaylistEvent,
|
||||||
connectionEpoch,
|
connectionEpoch,
|
||||||
} = useWS();
|
} = useWS();
|
||||||
|
|
||||||
@@ -180,6 +186,28 @@ export function Dump() {
|
|||||||
return () => controller.abort();
|
return () => controller.abort();
|
||||||
}, [selectedDump, token]);
|
}, [selectedDump, token]);
|
||||||
|
|
||||||
|
// Fetch the collections this dump appears in (public ones, plus the
|
||||||
|
// viewer's own private ones — the server does the filtering)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDump) return;
|
||||||
|
const controller = new AbortController();
|
||||||
|
fetch(`${API_URL}/api/dumps/${selectedDump}/playlists`, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((body) => {
|
||||||
|
if (body.success) {
|
||||||
|
setCollections((body.data as RawPlaylist[]).map(deserializePlaylist));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return () => controller.abort();
|
||||||
|
// lastPlaylistEvent: adding/removing this dump, flipping a collection's
|
||||||
|
// visibility or deleting it all change the list — playlist events are rare
|
||||||
|
// enough to just refetch on any of them.
|
||||||
|
}, [selectedDump, token, lastPlaylistEvent]);
|
||||||
|
|
||||||
// Scroll to and highlight a comment when navigating to #comment-{id}
|
// Scroll to and highlight a comment when navigating to #comment-{id}
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!location.hash.startsWith("#comment-")) return;
|
if (!location.hash.startsWith("#comment-")) return;
|
||||||
@@ -517,6 +545,20 @@ export function Dump() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* In collections (playlists containing this dump) */}
|
||||||
|
{collections.length > 0 && (
|
||||||
|
<section className="related-section collections-section">
|
||||||
|
<h2 className="related-section-title">
|
||||||
|
<Trans>In collections</Trans>
|
||||||
|
</h2>
|
||||||
|
<ul className="dump-feed">
|
||||||
|
{collections.map((collection) => (
|
||||||
|
<PlaylistCard key={collection.id} playlist={collection} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Comments */}
|
{/* Comments */}
|
||||||
<CommentThread
|
<CommentThread
|
||||||
dumpId={dump.id}
|
dumpId={dump.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user