diff --git a/api/routes/users.ts b/api/routes/users.ts index 58a101d..090347d 100644 --- a/api/routes/users.ts +++ b/api/routes/users.ts @@ -15,6 +15,7 @@ import { parseOptionalAuth } from "../lib/auth.ts"; import { parsePagination } from "../lib/pagination.ts"; import { createUser, + getInviteTree, getUserById, getUserByUsername, searchUsers, @@ -30,7 +31,10 @@ import { getVotedDumpsByUser, } from "../services/dump-service.ts"; import { listPlaylistsByUser } from "../services/playlist-service.ts"; -import { getFollowedPlaylistsByUser } from "../services/follow-service.ts"; +import { + getFollowedPlaylistsByUser, + getFollowedUsersByUser, +} from "../services/follow-service.ts"; // Users router const router = new Router({ prefix: "/api/users" }); @@ -189,6 +193,21 @@ router.get("/by-id/:userId", (ctx) => { ctx.response.body = { success: true, data: publicUser }; }); +// Followed users for a user (public) +router.get("/:username/followed-users", (ctx) => { + const user = getUserByUsername(ctx.params.username); + const { page, limit } = parsePagination(ctx.request.url.searchParams); + const { items, total } = getFollowedUsersByUser(user.id, page, limit); + ctx.response.body = { + success: true, + data: { + items: items.map(({ passwordHash: _, email: _e, ...pub }) => pub), + total, + hasMore: page * limit < total, + }, + }; +}); + // Followed playlists for a user (public only) router.get("/:username/followed-playlists", (ctx) => { const user = getUserByUsername(ctx.params.username); @@ -225,6 +244,13 @@ router.get("/:username/playlists", async (ctx) => { }; }); +// Invite tree for a user (public) +router.get("/:username/invitees", (ctx) => { + const user = getUserByUsername(ctx.params.username); + const tree = getInviteTree(user.id); + ctx.response.body = { success: true, data: tree }; +}); + // Public user profile by username (no passwordHash) router.get("/:username", (ctx) => { const user = getUserByUsername(ctx.params.username); diff --git a/api/services/follow-service.ts b/api/services/follow-service.ts index ac4d69a..7a29dc3 100644 --- a/api/services/follow-service.ts +++ b/api/services/follow-service.ts @@ -4,6 +4,7 @@ import { type Dump, type FollowStatus, type Playlist, + type User, } from "../model/interfaces.ts"; import { notifyPlaylistOwnerNewFollower, @@ -15,7 +16,9 @@ import { isDumpRow, isFollowRow, isPlaylistRow, + isUserRow, playlistRowToApi, + userRowToApi, } from "../model/db.ts"; // Mirrors dump-service SELECT_COLS_ALIASED — kept local to avoid circular imports @@ -256,3 +259,39 @@ export function getFollowedPlaylistsByUser( } return { items: rawRows.map(playlistRowToApi), total: totalRow?.count ?? 0 }; } + +// ── Followed users (as user objects) ───────────────────────────────────────── + +export function getFollowedUsersByUser( + userId: string, + page: number, + limit: number, +): { items: User[]; total: number } { + const offset = (page - 1) * limit; + + const totalRow = db.prepare( + `SELECT COUNT(*) as count FROM follows WHERE follower_id = ? AND followed_user_id IS NOT NULL;`, + ).get(userId) as { count: number } | undefined; + + const rawRows = db.prepare( + `SELECT u.id, u.username, u.password_hash, u.is_admin, u.created_at, u.updated_at, + u.avatar_mime, u.description, u.invited_by, u.email, + i.username as invited_by_username + FROM users u + LEFT JOIN users i ON i.id = u.invited_by + INNER JOIN follows f ON f.followed_user_id = u.id + WHERE f.follower_id = ? + ORDER BY f.created_at DESC + LIMIT ? OFFSET ?;`, + ).all(userId, limit, offset); + + if (!rawRows.every(isUserRow)) { + throw new APIException( + APIErrorCode.SERVER_ERROR, + 500, + "Malformed user data", + ); + } + + return { items: rawRows.map(userRowToApi), total: totalRow?.count ?? 0 }; +} diff --git a/api/services/user-service.ts b/api/services/user-service.ts index 5f8bb63..b149b3d 100644 --- a/api/services/user-service.ts +++ b/api/services/user-service.ts @@ -178,6 +178,44 @@ export function updateUserAvatar(userId: string, mime: string): void { } } +export function getInviteTree( + userId: string, +): { + id: string; + username: string; + avatarMime?: string; + invitedById: string; + createdAt: Date; +}[] { + const rows = db.prepare(` + WITH RECURSIVE tree AS ( + SELECT id, username, avatar_mime, invited_by, created_at, 0 AS depth + FROM users + WHERE invited_by = ? + UNION ALL + SELECT u.id, u.username, u.avatar_mime, u.invited_by, u.created_at, t.depth + 1 + FROM users u + INNER JOIN tree t ON u.invited_by = t.id + WHERE t.depth < 10 + ) + SELECT * FROM tree ORDER BY created_at; + `).all(userId) as { + id: string; + username: string; + avatar_mime: string | null; + invited_by: string; + created_at: string; + }[]; + + return rows.map((r) => ({ + id: r.id, + username: r.username, + avatarMime: r.avatar_mime ?? undefined, + invitedById: r.invited_by, + createdAt: new Date(r.created_at), + })); +} + export function deleteUser(userId: string): void { disconnectUser(userId); diff --git a/index.html b/index.html index 3562585..7e7c091 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ - + @@ -14,6 +14,7 @@
+ diff --git a/src/App.css b/src/App.css index 69bde4c..2ce29d6 100644 --- a/src/App.css +++ b/src/App.css @@ -1272,6 +1272,12 @@ body.has-player .fab-new { gap: 1.5rem; } +.profile-tabs { + margin-top: 0.5rem; + padding-bottom: 0.75rem; + border-bottom: 2px solid var(--color-border); +} + .profile-columns { display: grid; grid-template-columns: 1fr; @@ -1651,7 +1657,7 @@ body.has-player .fab-new { align-items: flex-start; gap: 0.6rem; background: var(--color-danger-bg); - color: var(--color-on-accent); + color: var(--color-text); padding: 0.7rem 1rem; border-radius: 8px; font-size: 0.9rem; @@ -1694,7 +1700,7 @@ body.has-player .fab-new { border: none; border-radius: 10px; background: var(--color-danger-bg); - color: var(--color-on-accent); + color: var(--color-text); line-height: 1.5; overflow-wrap: anywhere; word-break: break-word; @@ -1747,6 +1753,7 @@ body.has-player .fab-new { /* ── Shared header ── */ .app-header { + position: relative; display: flex; align-items: center; gap: 1rem; @@ -1834,6 +1841,22 @@ body.has-player .fab-new { display: none; } +.app-header-center:has(.search-bar--expanded) .header-center-slot { + flex: 1; + justify-content: center; +} + +/* Expanded: pin center absolutely so it aligns with the feed's visual center */ +@media (min-width: 860px) { + .app-header-center:has(.search-bar--expanded) { + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 420px; + pointer-events: auto; + } +} + /* As the center column shrinks (viewport narrow, search collapsed), shed content in order: presence first, then tabs (still in .index-below-header) */ @container (max-width: 460px) { @@ -1868,6 +1891,16 @@ body.has-player .fab-new { flex-wrap: nowrap; } +/* Normalize all buttons in the header nav to the same height */ +.app-header-nav button { + padding: 0.35rem 0.85rem; + font-size: 0.95rem; +} + +.app-header-nav .btn-primary { + padding: 0.35rem 1rem; +} + /* Text links — visible only at wide viewports */ .nav-links { display: none; @@ -2882,7 +2915,7 @@ body.has-player .fab-new { .playlist-dump-list { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 1rem; } .playlist-dump-item { @@ -3277,6 +3310,111 @@ body.has-player .fab-new { padding: 0.2rem 0; } +/* ── Followed user list ── */ +.followed-user-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.followed-user-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.4rem 0.5rem; + border-radius: 6px; +} + +.followed-user-card-link { + display: flex; + align-items: center; + gap: 0.6rem; + text-decoration: none; + color: var(--color-text); + min-width: 0; + flex: 1; +} + +.followed-user-card-link:hover .followed-user-card-name { + color: var(--color-accent); +} + +.followed-user-card-name { + font-size: 0.9rem; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Invite tree ── */ +.invite-tree { + list-style: none; + padding: 0; + margin: 0; +} + +.invite-tree-node { + position: relative; + padding: 0.2rem 0; +} + +.invite-tree-node:last-child { + padding-bottom: 0; +} + +/* Child list: indented, left edge aligns with parent avatar center */ +.invite-tree-node > .invite-tree { + position: relative; + margin-left: 11px; /* center 2px line on 24px avatar: 12px - 1px */ + padding-left: calc(12px + 0.5rem); +} + +/* Vertical connector: starts below parent avatar, stops at last child avatar center */ +.invite-tree-node > .invite-tree::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 12px; + width: 2px; + background: var(--color-border-subtle); +} + +/* Horizontal connector: from vertical line to each child's avatar left edge */ +.invite-tree-node > .invite-tree > .invite-tree-node::before { + content: ""; + position: absolute; + left: calc(-12px - 0.5rem); + top: calc(0.2rem + 11px); /* centered on 24px avatar */ + width: calc(12px + 0.5rem); + height: 2px; + background: var(--color-border-subtle); +} + +.invite-tree-user { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--color-text); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; +} + +.invite-tree-user:hover { + color: var(--color-accent); +} + +.invite-tree-empty { + color: var(--color-text-muted); + font-size: 0.9rem; +} + /* ── Follow button ── */ .follow-btn { padding: 0.25rem 0.9rem; @@ -3977,7 +4115,7 @@ body.has-player .fab-new { align-items: center; gap: 0.4rem; width: 100%; - max-width: 380px; + max-width: 420px; } .search-bar--collapsible { @@ -3987,7 +4125,7 @@ body.has-player .fab-new { .search-bar--collapsible.search-bar--expanded { width: 100%; - max-width: 320px; + max-width: 420px; } .search-bar-input { @@ -4021,7 +4159,7 @@ body.has-player .fab-new { /* Expanded state: animate it open */ .search-bar--collapsible.search-bar--expanded .search-bar-input { - max-width: 280px; + max-width: 380px; opacity: 1; padding-left: 0.7rem; padding-right: 0.7rem; @@ -4115,3 +4253,21 @@ body.has-player .fab-new { overflow: hidden; text-overflow: ellipsis; } + +/* ── Profile appearance settings ── */ +.profile-appearance-grid { + display: grid; + grid-template-columns: max-content max-content; + gap: 0.75rem 1rem; + align-items: center; +} + +.profile-appearance-row { + display: contents; +} + +.profile-appearance-label { + font-size: 0.85rem; + font-weight: 600; + opacity: 0.7; +} diff --git a/src/App.tsx b/src/App.tsx index cae3f5c..dc05984 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,6 +19,7 @@ import { AuthProvider } from "./contexts/AuthProvider.tsx"; import { PlayerProvider } from "./contexts/PlayerProvider.tsx"; import { WSProvider } from "./contexts/WSProvider.tsx"; import { FollowProvider } from "./contexts/FollowProvider.tsx"; +import { ThemeProvider } from "./contexts/ThemeProvider.tsx"; import { GlobalPlayer } from "./components/GlobalPlayer.tsx"; import "./App.css"; @@ -75,18 +76,20 @@ function AppRoutes() { function App() { return ( - - - - - - - - - - - - + + + + + + + + + + + + + + ); } diff --git a/src/components/PageShell.tsx b/src/components/PageShell.tsx index 9219a3b..e66480c 100644 --- a/src/components/PageShell.tsx +++ b/src/components/PageShell.tsx @@ -4,12 +4,13 @@ import { SearchBar } from "./SearchBar.tsx"; interface PageShellProps { children: ReactNode; + feed?: ReactNode; centered?: boolean; centerSlot?: ReactNode; } export function PageShell( - { children, centered = false, centerSlot }: PageShellProps, + { children, feed, centered = false, centerSlot }: PageShellProps, ) { return (
@@ -19,6 +20,7 @@ export function PageShell( > {children} + {feed}
); } diff --git a/src/contexts/ThemeContext.ts b/src/contexts/ThemeContext.ts new file mode 100644 index 0000000..c2465aa --- /dev/null +++ b/src/contexts/ThemeContext.ts @@ -0,0 +1,18 @@ +import { createContext } from "react"; + +export type StyleName = "smooth" | "brutalist"; +export type ColorScheme = "auto" | "light" | "dark"; + +export interface ThemeContextValue { + style: StyleName; + colorScheme: ColorScheme; + setStyle(s: StyleName): void; + setColorScheme(c: ColorScheme): void; +} + +export const ThemeContext = createContext({ + style: "smooth", + colorScheme: "auto", + setStyle: () => {}, + setColorScheme: () => {}, +}); diff --git a/src/contexts/ThemeProvider.tsx b/src/contexts/ThemeProvider.tsx new file mode 100644 index 0000000..0e3df22 --- /dev/null +++ b/src/contexts/ThemeProvider.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react"; +import { + type ColorScheme, + type StyleName, + ThemeContext, +} from "./ThemeContext.ts"; + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [style, setStyleState] = useState(() => { + const stored = localStorage.getItem("style"); + // migrate legacy "default" value + if (stored === "default" || stored === null) return "smooth"; + return stored as StyleName; + }); + const [colorScheme, setColorSchemeState] = useState( + () => + (localStorage.getItem("color-scheme") as ColorScheme | null) ?? "auto", + ); + + useEffect(() => { + const el = document.documentElement; + if (style === "smooth") { + el.removeAttribute("data-style"); + } else { + el.setAttribute("data-style", style); + } + localStorage.setItem("style", style); + }, [style]); + + useEffect(() => { + const el = document.documentElement; + if (colorScheme === "auto") { + el.removeAttribute("data-color-scheme"); + } else { + el.setAttribute("data-color-scheme", colorScheme); + } + localStorage.setItem("color-scheme", colorScheme); + }, [colorScheme]); + + return ( + + {children} + + ); +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 0000000..1013944 --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { ThemeContext } from "../contexts/ThemeContext.ts"; + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/src/index.css b/src/index.css index bb80946..79b66f1 100644 --- a/src/index.css +++ b/src/index.css @@ -1,78 +1,3 @@ -:root { - font-family: "Saira", sans-serif; - font-optical-sizing: auto; - font-weight: 400; - font-style: normal; - line-height: 1.5; - - color-scheme: light dark; - - /* Text */ - --color-text: #f9fafb; - --color-text-secondary: #9ca3af; - --color-text-muted: #6b7280; - --color-on-accent: #fff; - - /* Surfaces */ - --color-bg: #111827; - --color-surface: #1f2937; - - /* Borders */ - --color-border: transparent; - --color-border-subtle: rgba(128, 128, 128, 0.18); - - /* Accent */ - --color-accent: #7c83ff; - --color-accent-hover: #4a50e0; - - /* Links */ - --color-link: #646cff; - --color-link-hover: #535bf2; - - /* Danger */ - --color-danger: #e55; - --color-danger-bg: #a02b2b; - --color-danger-hover: #c03030; - - /* Success */ - --color-success: #4caf7d; - --color-success-bg: #1a6b3a; - --color-success-hover: #2d9e58; - - /* Overlays */ - --color-overlay: rgba(0, 0, 0, 0.45); - --color-header-user-bg: rgba(0, 0, 0, 0.2); - --color-header-user-bg-hover: rgba(0, 0, 0, 0.32); - - /* Misc */ - --color-option-bg: #37366e; - --color-option-border: #111827; - - /* Service brand colors (fixed, not theme-dependent) */ - --color-youtube: #c00; - --color-youtube-hover: #f00; - --color-bandcamp: #1da0c3; - --color-bandcamp-hover: #25c8f0; - --color-soundcloud: #f50; - --color-soundcloud-hover: #f73; -} - -@media (prefers-color-scheme: light) { - :root { - --color-text: #213547; - --color-text-secondary: #64748b; - --color-text-muted: #94a3b8; - --color-bg: #ffffff; - --color-surface: #f9f9f9; - --color-border: transparent; - --color-link-hover: #747bff; - --color-option-bg: #f5f5f5; - --color-option-border: #cccccc; - --color-header-user-bg: rgba(0, 0, 0, 0.08); - --color-header-user-bg-hover: rgba(0, 0, 0, 0.15); - } -} - *, *::before, *::after { @@ -83,6 +8,7 @@ body { margin: 0; min-height: 100vh; overflow-x: clip; + scrollbar-gutter: stable; background-color: var(--color-bg); color: var(--color-text); } diff --git a/src/locales/en.js b/src/locales/en.js index 5ba2323..cdd1011 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -1,5 +1,5 @@ /*eslint-disable*/ module.exports = { messages: JSON.parse( - '{"-K9EZb":["Add email…"],"-Ya-b9":["Failed to save"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1HfJWf":["Search dumps, users, playlists…"],"1cbYY_":["Upvoted (",["0"],["1"],")"],"1utXA6":["Dumps"],"26iNma":["Post comment"],"2Hlmdt":["Write a reply…"],"2ygf_L":["← Back"],"3KKSM4":["private"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," followed your playlist <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Dumped!"],"4GKuCs":["Login failed"],"4RtQ1k":["Unfollow ",["targetUsername"]],"4yj9xV":["Live updates are temporarily disconnected. Trying to reconnect…"],"5cC8f2":["Edited ",["0"]],"5oD9f_":["Earlier"],"6Qly-0":["a comment"],"6gRgw8":["Retry"],"7JBW66":["Forbidden"],"7PHCIN":["Cancel removal"],"7d1a0d":["Public"],"7sNhEz":["Username"],"8ZsakT":["Password"],"8pxhI8":["Please select a file."],"9l4qcT":["Drop a replacement here"],"9uI_rE":["Undo"],"A0y396":["+ Invite someone"],"A1taO8":["Search"],"AQbgNR":["Follow ",["targetUsername"]],"ATGYL1":["Email address"],"AZctoV":[["0","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"Ade-6d":["Live updates unavailable."],"CI50ct":["Upvoted"],"Cj24wt":["Registering…"],"DPfwMq":["Done"],"DdeHXH":["Delete this dump? This cannot be undone."],"Dp1JhP":["<0>",["0"]," upvoted <1>",["1"],""],"ECiS12":["View dump →"],"ExR0Fr":["URL is required."],"F5Js1v":["Unfollow playlist"],"FgAxTj":["Log out"],"Fxf4jq":["Description (optional)"],"GNSsCc":["Failed to create playlist"],"GbqhrN":[["0"],"–",["1"]," characters: letters, numbers, or underscores"],"GsRMX3":["Playlist not found"],"H8pzW-":["Failed to update avatar"],"HTLDA4":["Loading more…"],"IZX7TO":["Failed to generate invite"],"IagCbF":["URL"],"ImOQa9":["Reply"],"J2eKUI":["File"],"Jd58Fo":["Hot"],"K1JdNl":["Username already exists"],"KDGWg5":["Remove from playlist"],"K_F6pa":["Saving…"],"LLyMkV":["Followed (",["0"],["1"],")"],"LPAv9E":[["days"],"d ago"],"Lld1jm":["Tell people about yourself…"],"MHrjPM":["Title"],"MKEPCY":["Follow"],"Mq2B8E":[["mins"],"m ago"],"NR0xa9":["Tell the community what makes this worth their time..."],"Nn4kr3":["+ New dump"],"Oprv1v":["Password (min. ",["0"]," characters)"],"Oz0N9s":["new"],"PiH3UR":["Copied!"],"Pwqkdw":["Loading…"],"Q6n4F4":["Refresh metadata"],"QKsaQr":["or <0>browse files"],"QLtPBd":["No dumps in this playlist yet."],"RCcPrX":["Delete this playlist? This cannot be undone."],"RTksSy":["<0>",["0"]," started following you"],"RaKjrM":["Failed to save edit"],"RcUHRT":["Followed"],"SBTElJ":["Searching…"],"Sxm8rQ":["Users"],"T9bjWt":["<0>",["0"]," was added to <1>",["1"],""],"TM1ZbA":["Playlists (",["0"],["1"],")"],"Tv9vbB":["Follow playlist"],"U7u3q-":["+ New"],"UOZith":["Failed to post"],"UTiUFs":["Fetching…"],"VnNJbN":["From playlists"],"VyTYmS":["Change avatar"],"WpXcBJ":["Nothing here yet."],"WtkMN8":["All ",["0","plural",{"one":["#"," upvoted dump"],"other":["#"," upvoted dumps"]}]," loaded."],"XILg0L":["Invalid email address"],"XJy2oN":["Logging in…"],"Xan6QP":["New dump"],"Xi0Mn4":["← Back to profile"],"XnL-Eu":["No users match \\"",["q"],"\\"."],"YK1Dhc":["a post"],"YaSA2K":["Comment not found"],"YpkCca":["No followed playlists yet."],"ZCpU0u":["No playlists match \\"",["q"],"\\"."],"ZmD2o6":["Create & Add"],"_84wxb":["All ",["0","plural",{"one":["#"," dump"],"other":["#"," dumps"]}]," loaded."],"_DwR-n":["Creating…"],"_aept4":["Post reply"],"_t4W-i":["From people"],"aDvLhk":["Add a comment…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Upload failed"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"bQhwn-":["Loading playlist…"],"cILfnJ":["Remove file"],"cYP9Sb":["+ Playlist"],"cnGeoo":["Delete"],"d8DZWS":["Open search"],"dAs22m":["Replace file"],"dEgA5A":["Cancel"],"dMizp8":["New playlist"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Why are you dumping this?"],"eFSqvc":["Failed to post reply"],"ePK91l":["Edit"],"ecUA8p":["Today"],"ef9nPf":["Loading dump…"],"en9o7K":["Failed to post comment"],"fGxPOv":["Add a bio…"],"fI-mNw":["Playlists"],"f_akpP":["Max 50 MB"],"fgLNSM":["Register"],"gANddk":["Uploading…"],"gGx5tM":["Editing"],"gIQQwD":["Failed to load"],"gLfZlz":["Add to playlist"],"gjJ-sb":["Can\'t connect to the live updates server. Upvotes and notifications may not sync until it reconnects."],"hD7w09":["You\'ve reached the end."],"hJSliC":["<0>",["0"]," posted <1>",["1"],""],"hYgDIe":["Create"],"he3ygx":["Copy"],"iDNBZe":["Notifications"],"ijVyoK":["Could not reach the server. Please try again."],"isRobC":["New"],"jGrTH0":["Not authenticated"],"jbernk":["Loading profile…"],"joEmfT":["Server unreachable"],"jrZTZl":["No dumps yet. Be the first!"],"kLttbL":["Registration failed"],"kYYCil":["File too large (max 50 MB)"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Created (",["0"],["1"],")"],"lUanmi":["You\'ll be notified when someone follows your playlists, upvotes your dumps, or posts new content."],"lY5h1V":[["0","plural",{"one":["#"," dump"],"other":["#"," dumps"]}]],"lcfvr_":["Delete this comment?"],"mt6O6E":["This is a mirage."],"nbm5sI":["No dumps match \\"",["q"],"\\"."],"nrjqON":["Checking invite…"],"nwtY4N":["Something went wrong"],"pCpd9p":["<0>",["0"]," mentioned you in <1>",["where"],""],"qIMfNQ":["Delete playlist"],"qbDAcy":["Dump it"],"qgx_78":["Follow some public playlists to see their dumps here."],"qvFa8r":["public"],"rCbqPX":["This invite link is missing, expired, or already used."],"rg9pXu":["Search failed"],"rtpJqV":["Dumps (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Log in"],"sTiqbm":["invited by"],"sdP5Aa":["[deleted]"],"shHs8T":["Enter a query to search."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invalid invite"],"tfDRzk":["Save"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"u1lDX2":["Fetching preview…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Drop a file here"],"uMGUnV":["No playlists yet."],"ub1EEL":["edited ",["0"]],"vJBF1r":["Posting…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," unread)"],"vuosjb":["User menu"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":[["hrs"],"h ago"],"wbXKOv":["File too large (max 50 MB)."],"wdiqRH":["Admin access required"],"wixIgH":["Already have an account? <0>Log in"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Back to all dumps"],"xOTzt5":["just now"],"xPHtx0":["Submit search"],"xVuNgt":["+ New playlist"],"xc9O_u":["Delete dump"],"y6sq5j":["Following"],"yA_6BX":["View all →"],"yBBtRm":["Follow some users to see their dumps here."],"yQ2kGp":["Load more"],"y_0uwd":["Yesterday"],"yz7wBu":["Close"],"z1uNN0":["No emoji found."],"zVuxvN":["Refreshing…"],"zwBp5t":["Private"]}', + '{"-K9EZb":["Add email…"],"-OxI15":["Followed playlists"],"-Ya-b9":["Failed to save"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1CalO6":["Style"],"1HfJWf":["Search dumps, users, playlists…"],"1cbYY_":["Upvoted (",["0"],["1"],")"],"1njn7W":["Light"],"1utXA6":["Dumps"],"26iNma":["Post comment"],"2Hlmdt":["Write a reply…"],"2ygf_L":["← Back"],"3KKSM4":["private"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," followed your playlist <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Dumped!"],"4GKuCs":["Login failed"],"4HH9iB":["Not following anyone yet."],"4RtQ1k":["Unfollow ",["targetUsername"]],"4yj9xV":["Live updates are temporarily disconnected. Trying to reconnect…"],"5cC8f2":["Edited ",["0"]],"5oD9f_":["Earlier"],"6Qly-0":["a comment"],"6gRgw8":["Retry"],"7JBW66":["Forbidden"],"7PHCIN":["Cancel removal"],"7d1a0d":["Public"],"7sNhEz":["Username"],"8ZsakT":["Password"],"8pxhI8":["Please select a file."],"9l4qcT":["Drop a replacement here"],"9uI_rE":["Undo"],"A0y396":["+ Invite someone"],"A1taO8":["Search"],"AQbgNR":["Follow ",["targetUsername"]],"ATGYL1":["Email address"],"AZctoV":[["0","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"Ade-6d":["Live updates unavailable."],"CI50ct":["Upvoted"],"Cj24wt":["Registering…"],"DPfwMq":["Done"],"DdeHXH":["Delete this dump? This cannot be undone."],"Dp1JhP":["<0>",["0"]," upvoted <1>",["1"],""],"ECiS12":["View dump →"],"ExR0Fr":["URL is required."],"F5Js1v":["Unfollow playlist"],"FgAxTj":["Log out"],"Fxf4jq":["Description (optional)"],"GNSsCc":["Failed to create playlist"],"GbqhrN":[["0"],"–",["1"]," characters: letters, numbers, or underscores"],"GsRMX3":["Playlist not found"],"H8pzW-":["Failed to update avatar"],"HTLDA4":["Loading more…"],"IZX7TO":["Failed to generate invite"],"IagCbF":["URL"],"ImOQa9":["Reply"],"J2eKUI":["File"],"Jd58Fo":["Hot"],"K1JdNl":["Username already exists"],"KDGWg5":["Remove from playlist"],"K_F6pa":["Saving…"],"LLyMkV":["Followed (",["0"],["1"],")"],"LPAv9E":[["days"],"d ago"],"Lld1jm":["Tell people about yourself…"],"MHrjPM":["Title"],"MKEPCY":["Follow"],"Mq2B8E":[["mins"],"m ago"],"NR0xa9":["Tell the community what makes this worth their time..."],"Nn4kr3":["+ New dump"],"Oprv1v":["Password (min. ",["0"]," characters)"],"Oz0N9s":["new"],"PiH3UR":["Copied!"],"Pwqkdw":["Loading…"],"Q6n4F4":["Refresh metadata"],"QKsaQr":["or <0>browse files"],"QLtPBd":["No dumps in this playlist yet."],"R9Khdg":["Auto"],"RCcPrX":["Delete this playlist? This cannot be undone."],"RTksSy":["<0>",["0"]," started following you"],"RaKjrM":["Failed to save edit"],"RcUHRT":["Followed"],"SBTElJ":["Searching…"],"Sxm8rQ":["Users"],"T9bjWt":["<0>",["0"]," was added to <1>",["1"],""],"TM1ZbA":["Playlists (",["0"],["1"],")"],"Tv9vbB":["Follow playlist"],"Tz0i8g":["Settings"],"U7u3q-":["+ New"],"UOZith":["Failed to post"],"UTiUFs":["Fetching…"],"VnNJbN":["From playlists"],"VyTYmS":["Change avatar"],"WpXcBJ":["Nothing here yet."],"WtkMN8":["All ",["0","plural",{"one":["#"," upvoted dump"],"other":["#"," upvoted dumps"]}]," loaded."],"XILg0L":["Invalid email address"],"XJy2oN":["Logging in…"],"Xan6QP":["New dump"],"Xi0Mn4":["← Back to profile"],"XnL-Eu":["No users match \\"",["q"],"\\"."],"YK1Dhc":["a post"],"YaSA2K":["Comment not found"],"YpkCca":["No followed playlists yet."],"ZCpU0u":["No playlists match \\"",["q"],"\\"."],"ZmD2o6":["Create & Add"],"_84wxb":["All ",["0","plural",{"one":["#"," dump"],"other":["#"," dumps"]}]," loaded."],"_DwR-n":["Creating…"],"_aept4":["Post reply"],"_t4W-i":["From people"],"aAIQg2":["Appearance"],"aDvLhk":["Add a comment…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Upload failed"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," comment"],"other":["#"," comments"]}]],"bQhwn-":["Loading playlist…"],"cILfnJ":["Remove file"],"cYP9Sb":["+ Playlist"],"cnGeoo":["Delete"],"d8DZWS":["Open search"],"dAs22m":["Replace file"],"dEgA5A":["Cancel"],"dMizp8":["New playlist"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Why are you dumping this?"],"eFSqvc":["Failed to post reply"],"ePK91l":["Edit"],"ecUA8p":["Today"],"ef9nPf":["Loading dump…"],"en9o7K":["Failed to post comment"],"fGxPOv":["Add a bio…"],"fI-mNw":["Playlists"],"f_akpP":["Max 50 MB"],"fgLNSM":["Register"],"gANddk":["Uploading…"],"gGx5tM":["Editing"],"gIQQwD":["Failed to load"],"gLfZlz":["Add to playlist"],"gjJ-sb":["Can\'t connect to the live updates server. Upvotes and notifications may not sync until it reconnects."],"hD7w09":["You\'ve reached the end."],"hJSliC":["<0>",["0"]," posted <1>",["1"],""],"hYgDIe":["Create"],"he3ygx":["Copy"],"iDNBZe":["Notifications"],"ijVyoK":["Could not reach the server. Please try again."],"isRobC":["New"],"jGrTH0":["Not authenticated"],"jbernk":["Loading profile…"],"joEmfT":["Server unreachable"],"jrZTZl":["No dumps yet. Be the first!"],"kLttbL":["Registration failed"],"kYYCil":["File too large (max 50 MB)"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Created (",["0"],["1"],")"],"lUanmi":["You\'ll be notified when someone follows your playlists, upvotes your dumps, or posts new content."],"lY5h1V":[["0","plural",{"one":["#"," dump"],"other":["#"," dumps"]}]],"lcfvr_":["Delete this comment?"],"mt6O6E":["This is a mirage."],"nbm5sI":["No dumps match \\"",["q"],"\\"."],"nrjqON":["Checking invite…"],"nwtY4N":["Something went wrong"],"pCpd9p":["<0>",["0"]," mentioned you in <1>",["where"],""],"pvnfJD":["Dark"],"qIMfNQ":["Delete playlist"],"qbDAcy":["Dump it"],"qgx_78":["Follow some public playlists to see their dumps here."],"qvFa8r":["public"],"rCbqPX":["This invite link is missing, expired, or already used."],"rg9pXu":["Search failed"],"rtpJqV":["Dumps (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Log in"],"sTiqbm":["invited by"],"sdP5Aa":["[deleted]"],"shHs8T":["Enter a query to search."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invalid invite"],"tfDRzk":["Save"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"tvmuQ0":["Color scheme"],"u1lDX2":["Fetching preview…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Drop a file here"],"uMGUnV":["No playlists yet."],"ub1EEL":["edited ",["0"]],"vJBF1r":["Posting…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," unread)"],"vuosjb":["User menu"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":[["hrs"],"h ago"],"wbXKOv":["File too large (max 50 MB)."],"wdiqRH":["Admin access required"],"wixIgH":["Already have an account? <0>Log in"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Back to all dumps"],"xOTzt5":["just now"],"xPHtx0":["Submit search"],"xVuNgt":["+ New playlist"],"xc9O_u":["Delete dump"],"y6sq5j":["Following"],"yA_6BX":["View all →"],"yBBtRm":["Follow some users to see their dumps here."],"yQ2kGp":["Load more"],"y_0uwd":["Yesterday"],"yz7wBu":["Close"],"z1uNN0":["No emoji found."],"zVuxvN":["Refreshing…"],"zwBp5t":["Private"]}', ), }; diff --git a/src/locales/en.po b/src/locales/en.po index df5af77..0600e21 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -13,7 +13,7 @@ msgstr "" "Language-Team: \n" "Plural-Forms: \n" -#: src/components/CommentThread.tsx:170 +#: src/components/CommentThread.tsx:176 msgid "[deleted]" msgstr "[deleted]" @@ -23,13 +23,13 @@ msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, one {# comment} other {# comments}}" #. placeholder {0}: playlist.dumpCount -#: src/components/PlaylistCard.tsx:84 +#: src/components/PlaylistCard.tsx:86 msgid "{0, plural, one {# dump} other {# dumps}}" msgstr "{0, plural, one {# dump} other {# dumps}}" #. placeholder {0}: VALIDATION.USERNAME_MIN #. placeholder {1}: VALIDATION.USERNAME_MAX -#: src/pages/UserRegister.tsx:128 +#: src/pages/UserRegister.tsx:132 msgid "{0}–{1} characters: letters, numbers, or underscores" msgstr "{0}–{1} characters: letters, numbers, or underscores" @@ -49,28 +49,28 @@ msgstr "{label} ({count})" msgid "{mins}m ago" msgstr "{mins}m ago" -#: src/components/CommentThread.tsx:436 +#: src/components/CommentThread.tsx:459 msgid "{visibleCount, plural, one {# comment} other {# comments}}" msgstr "{visibleCount, plural, one {# comment} other {# comments}}" -#: src/pages/PlaylistDetail.tsx:605 -#: src/pages/UserPublicProfile.tsx:606 +#: src/pages/PlaylistDetail.tsx:611 +#: src/pages/UserPublicProfile.tsx:728 msgid "← Back" msgstr "← Back" -#: src/pages/Dump.tsx:216 -#: src/pages/Dump.tsx:318 -#: src/pages/DumpEdit.tsx:166 +#: src/pages/Dump.tsx:218 +#: src/pages/Dump.tsx:321 +#: src/pages/DumpEdit.tsx:170 msgid "← Back to all dumps" msgstr "← Back to all dumps" -#: src/pages/UserDumps.tsx:61 -#: src/pages/UserPlaylists.tsx:352 -#: src/pages/UserUpvoted.tsx:133 +#: src/pages/UserDumps.tsx:63 +#: src/pages/UserPlaylists.tsx:354 +#: src/pages/UserUpvoted.tsx:135 msgid "← Back to profile" msgstr "← Back to profile" -#: src/pages/UserPublicProfile.tsx:90 +#: src/pages/UserPublicProfile.tsx:93 msgid "+ Invite someone" msgstr "+ Invite someone" @@ -78,17 +78,17 @@ msgstr "+ Invite someone" msgid "+ New" msgstr "+ New" -#: src/pages/UserDumps.tsx:82 -#: src/pages/UserPublicProfile.tsx:922 +#: src/pages/UserDumps.tsx:114 +#: src/pages/UserPublicProfile.tsx:1282 msgid "+ New dump" msgstr "+ New dump" #: src/components/NewPlaylistForm.tsx:30 -#: src/components/PlaylistMembershipPanel.tsx:72 +#: src/components/PlaylistMembershipPanel.tsx:80 msgid "+ New playlist" msgstr "+ New playlist" -#: src/pages/Dump.tsx:248 +#: src/pages/Dump.tsx:250 msgid "+ Playlist" msgstr "+ Playlist" @@ -134,20 +134,20 @@ msgstr "a comment" msgid "a post" msgstr "a post" -#: src/pages/UserPublicProfile.tsx:802 +#: src/pages/UserPublicProfile.tsx:931 msgid "Add a bio…" msgstr "Add a bio…" -#: src/components/CommentThread.tsx:456 +#: src/components/CommentThread.tsx:479 msgid "Add a comment…" msgstr "Add a comment…" -#: src/pages/UserPublicProfile.tsx:718 +#: src/pages/UserPublicProfile.tsx:842 msgid "Add email…" msgstr "Add email…" #: src/components/AddToPlaylistModal.tsx:64 -#: src/components/DumpCreateModal.tsx:275 +#: src/components/DumpCreateModal.tsx:284 msgid "Add to playlist" msgstr "Add to playlist" @@ -155,39 +155,45 @@ msgstr "Add to playlist" #~ msgid "Admin access required" #~ msgstr "Admin access required" -#. placeholder {0}: dumps.length #: src/pages/UserDumps.tsx:114 msgid "All {0, plural, one {# dump} other {# dumps}} loaded." msgstr "All {0, plural, one {# dump} other {# dumps}} loaded." -#. placeholder {0}: votes.length #: src/pages/UserUpvoted.tsx:187 msgid "All {0, plural, one {# upvoted dump} other {# upvoted dumps}} loaded." msgstr "All {0, plural, one {# upvoted dump} other {# upvoted dumps}} loaded." -#: src/pages/UserRegister.tsx:160 +#: src/pages/UserRegister.tsx:165 msgid "Already have an account? <0>Log in" msgstr "Already have an account? <0>Log in" +#: src/pages/UserPublicProfile.tsx:1186 +msgid "Appearance" +msgstr "Appearance" + +#: src/pages/UserPublicProfile.tsx:1220 +msgid "Auto" +msgstr "Auto" + #: src/contexts/WSProvider.tsx:168 #: src/contexts/WSProvider.tsx:360 msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects." msgstr "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects." -#: src/components/CommentThread.tsx:268 -#: src/components/CommentThread.tsx:353 -#: src/components/CommentThread.tsx:483 +#: src/components/CommentThread.tsx:281 +#: src/components/CommentThread.tsx:373 +#: src/components/CommentThread.tsx:510 #: src/components/ConfirmModal.tsx:32 -#: src/components/DumpCreateModal.tsx:408 -#: src/components/PlaylistCreateForm.tsx:105 -#: src/pages/DumpEdit.tsx:288 -#: src/pages/PlaylistDetail.tsx:672 -#: src/pages/UserPublicProfile.tsx:700 -#: src/pages/UserPublicProfile.tsx:773 +#: src/components/DumpCreateModal.tsx:422 +#: src/components/PlaylistCreateForm.tsx:112 +#: src/pages/DumpEdit.tsx:299 +#: src/pages/PlaylistDetail.tsx:680 +#: src/pages/UserPublicProfile.tsx:824 +#: src/pages/UserPublicProfile.tsx:902 msgid "Cancel" msgstr "Cancel" -#: src/pages/PlaylistDetail.tsx:848 +#: src/pages/PlaylistDetail.tsx:863 msgid "Cancel removal" msgstr "Cancel removal" @@ -195,11 +201,11 @@ msgstr "Cancel removal" #~ msgid "Cannot edit a deleted comment" #~ msgstr "Cannot edit a deleted comment" -#: src/pages/UserPublicProfile.tsx:633 +#: src/pages/UserPublicProfile.tsx:755 msgid "Change avatar" msgstr "Change avatar" -#: src/pages/UserRegister.tsx:94 +#: src/pages/UserRegister.tsx:95 msgid "Checking invite…" msgstr "Checking invite…" @@ -207,79 +213,87 @@ msgstr "Checking invite…" msgid "Close" msgstr "Close" +#: src/pages/UserPublicProfile.tsx:1212 +msgid "Color scheme" +msgstr "Color scheme" + #: api/comments: #~ msgid "Comment not found" #~ msgstr "Comment not found" -#: src/pages/UserPublicProfile.tsx:81 +#: src/pages/UserPublicProfile.tsx:84 msgid "Copied!" msgstr "Copied!" -#: src/pages/UserPublicProfile.tsx:81 +#: src/pages/UserPublicProfile.tsx:84 msgid "Copy" msgstr "Copy" -#: src/components/CommentThread.tsx:108 -#: src/components/CommentThread.tsx:147 -#: src/components/CommentThread.tsx:425 +#: src/components/CommentThread.tsx:111 +#: src/components/CommentThread.tsx:153 +#: src/components/CommentThread.tsx:448 msgid "Could not reach the server. Please try again." msgstr "Could not reach the server. Please try again." -#: src/components/PlaylistCreateForm.tsx:116 +#: src/components/PlaylistCreateForm.tsx:124 msgid "Create" msgstr "Create" -#: src/components/PlaylistCreateForm.tsx:115 +#: src/components/PlaylistCreateForm.tsx:123 msgid "Create & Add" msgstr "Create & Add" #. placeholder {0}: created.items.length #. placeholder {1}: created.hasMore ? "+" : "" -#: src/pages/UserPlaylists.tsx:386 +#: src/pages/UserPlaylists.tsx:388 msgid "Created ({0}{1})" msgstr "Created ({0}{1})" -#: src/components/PlaylistCreateForm.tsx:113 +#: src/components/PlaylistCreateForm.tsx:121 msgid "Creating…" msgstr "Creating…" -#: src/components/CommentThread.tsx:306 -#: src/components/CommentThread.tsx:312 +#: src/pages/UserPublicProfile.tsx:1234 +msgid "Dark" +msgstr "Dark" + +#: src/components/CommentThread.tsx:319 +#: src/components/CommentThread.tsx:325 #: src/components/ConfirmModal.tsx:16 -#: src/pages/PlaylistDetail.tsx:679 +#: src/pages/PlaylistDetail.tsx:687 msgid "Delete" msgstr "Delete" -#: src/pages/DumpEdit.tsx:284 -#: src/pages/DumpEdit.tsx:300 +#: src/pages/DumpEdit.tsx:295 +#: src/pages/DumpEdit.tsx:315 msgid "Delete dump" msgstr "Delete dump" -#: src/components/PlaylistCard.tsx:107 -#: src/pages/PlaylistDetail.tsx:861 -#: src/pages/UserPlaylists.tsx:443 +#: src/components/PlaylistCard.tsx:109 +#: src/pages/PlaylistDetail.tsx:876 +#: src/pages/UserPlaylists.tsx:465 msgid "Delete playlist" msgstr "Delete playlist" -#: src/components/CommentThread.tsx:311 +#: src/components/CommentThread.tsx:324 msgid "Delete this comment?" msgstr "Delete this comment?" -#: src/pages/DumpEdit.tsx:299 +#: src/pages/DumpEdit.tsx:314 msgid "Delete this dump? This cannot be undone." msgstr "Delete this dump? This cannot be undone." -#: src/pages/PlaylistDetail.tsx:860 -#: src/pages/UserPlaylists.tsx:442 +#: src/pages/PlaylistDetail.tsx:875 +#: src/pages/UserPlaylists.tsx:464 msgid "Delete this playlist? This cannot be undone." msgstr "Delete this playlist? This cannot be undone." -#: src/components/PlaylistCreateForm.tsx:76 -#: src/pages/PlaylistDetail.tsx:710 +#: src/components/PlaylistCreateForm.tsx:80 +#: src/pages/PlaylistDetail.tsx:718 msgid "Description (optional)" msgstr "Description (optional)" -#: src/components/DumpCreateModal.tsx:453 +#: src/components/DumpCreateModal.tsx:468 msgid "Done" msgstr "Done" @@ -287,11 +301,11 @@ msgstr "Done" msgid "Drop a file here" msgstr "Drop a file here" -#: src/pages/DumpEdit.tsx:242 +#: src/pages/DumpEdit.tsx:252 msgid "Drop a replacement here" msgstr "Drop a replacement here" -#: src/components/DumpCreateModal.tsx:419 +#: src/components/DumpCreateModal.tsx:434 msgid "Dump it" msgstr "Dump it" @@ -299,117 +313,121 @@ msgstr "Dump it" #~ msgid "Dump not found" #~ msgstr "Dump not found" -#: src/components/DumpCreateModal.tsx:430 +#: src/components/DumpCreateModal.tsx:445 msgid "Dumped!" msgstr "Dumped!" #: src/pages/Search.tsx:172 -#: src/pages/UserDumps.tsx:75 +#: src/pages/UserDumps.tsx:107 +#: src/pages/UserPublicProfile.tsx:950 msgid "Dumps" msgstr "Dumps" #. placeholder {0}: dumps.items.length #. placeholder {1}: dumps.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:817 +#: src/pages/UserPublicProfile.tsx:987 msgid "Dumps ({0}{1})" msgstr "Dumps ({0}{1})" -#: src/pages/Notifications.tsx:341 +#: src/pages/Notifications.tsx:349 msgid "Earlier" msgstr "Earlier" -#: src/components/CommentThread.tsx:297 -#: src/pages/Dump.tsx:315 -#: src/pages/PlaylistDetail.tsx:698 +#: src/components/CommentThread.tsx:310 +#: src/pages/Dump.tsx:317 +#: src/pages/PlaylistDetail.tsx:706 msgid "Edit" msgstr "Edit" #. placeholder {0}: relativeTime(comment.updatedAt) #. placeholder {0}: relativeTime(dump.updatedAt) #. placeholder {0}: relativeTime(playlist.updatedAt) -#: src/components/CommentThread.tsx:231 -#: src/pages/Dump.tsx:276 -#: src/pages/PlaylistDetail.tsx:768 +#: src/components/CommentThread.tsx:237 +#: src/pages/Dump.tsx:278 +#: src/pages/PlaylistDetail.tsx:779 msgid "edited {0}" msgstr "edited {0}" #. placeholder {0}: comment.updatedAt.toLocaleString() #. placeholder {0}: dump.updatedAt.toLocaleString() #. placeholder {0}: playlist.updatedAt.toLocaleString() -#: src/components/CommentThread.tsx:229 -#: src/pages/Dump.tsx:274 -#: src/pages/PlaylistDetail.tsx:765 +#: src/components/CommentThread.tsx:235 +#: src/pages/Dump.tsx:276 +#: src/pages/PlaylistDetail.tsx:776 msgid "Edited {0}" msgstr "Edited {0}" -#: src/pages/DumpEdit.tsx:180 +#: src/pages/DumpEdit.tsx:185 msgid "Editing" msgstr "Editing" -#: src/pages/UserRegister.tsx:135 +#: src/pages/UserRegister.tsx:140 msgid "Email address" msgstr "Email address" -#: src/pages/Search.tsx:206 +#: src/pages/Search.tsx:207 msgid "Enter a query to search." msgstr "Enter a query to search." -#: src/components/PlaylistCreateForm.tsx:59 -#: src/components/PlaylistCreateForm.tsx:97 +#: src/components/PlaylistCreateForm.tsx:62 +#: src/components/PlaylistCreateForm.tsx:103 msgid "Failed to create playlist" msgstr "Failed to create playlist" -#: src/pages/UserPublicProfile.tsx:62 #: src/pages/UserPublicProfile.tsx:65 -#: src/pages/UserPublicProfile.tsx:92 +#: src/pages/UserPublicProfile.tsx:68 +#: src/pages/UserPublicProfile.tsx:96 msgid "Failed to generate invite" msgstr "Failed to generate invite" -#: src/pages/index/FollowedFeed.tsx:77 -#: src/pages/index/HotFeed.tsx:30 -#: src/pages/index/JournalFeed.tsx:42 -#: src/pages/index/NewFeed.tsx:30 -#: src/pages/Notifications.tsx:321 +#: src/pages/index/FollowedFeed.tsx:81 +#: src/pages/index/HotFeed.tsx:36 +#: src/pages/index/JournalFeed.tsx:48 +#: src/pages/index/NewFeed.tsx:36 +#: src/pages/Notifications.tsx:323 +#: src/pages/UserPublicProfile.tsx:1081 +#: src/pages/UserPublicProfile.tsx:1118 +#: src/pages/UserPublicProfile.tsx:1160 msgid "Failed to load" msgstr "Failed to load" -#: src/components/DumpCreateModal.tsx:313 +#: src/components/DumpCreateModal.tsx:322 msgid "Failed to post" msgstr "Failed to post" -#: src/components/CommentThread.tsx:462 +#: src/components/CommentThread.tsx:486 msgid "Failed to post comment" msgstr "Failed to post comment" -#: src/components/CommentThread.tsx:334 +#: src/components/CommentThread.tsx:349 msgid "Failed to post reply" msgstr "Failed to post reply" -#: src/pages/PlaylistDetail.tsx:776 -#: src/pages/UserPublicProfile.tsx:546 -#: src/pages/UserPublicProfile.tsx:581 -#: src/pages/UserPublicProfile.tsx:704 -#: src/pages/UserPublicProfile.tsx:776 +#: src/pages/PlaylistDetail.tsx:789 +#: src/pages/UserPublicProfile.tsx:663 +#: src/pages/UserPublicProfile.tsx:701 +#: src/pages/UserPublicProfile.tsx:828 +#: src/pages/UserPublicProfile.tsx:905 msgid "Failed to save" msgstr "Failed to save" -#: src/components/CommentThread.tsx:249 +#: src/components/CommentThread.tsx:257 msgid "Failed to save edit" msgstr "Failed to save edit" -#: src/pages/UserPublicProfile.tsx:726 +#: src/pages/UserPublicProfile.tsx:851 msgid "Failed to update avatar" msgstr "Failed to update avatar" -#: src/components/DumpCreateModal.tsx:347 +#: src/components/DumpCreateModal.tsx:359 msgid "Fetching preview…" msgstr "Fetching preview…" -#: src/components/DumpCreateModal.tsx:417 +#: src/components/DumpCreateModal.tsx:432 msgid "Fetching…" msgstr "Fetching…" -#: src/components/DumpCreateModal.tsx:306 +#: src/components/DumpCreateModal.tsx:315 #: src/components/FileDropZone.tsx:31 msgid "File" msgstr "File" @@ -426,7 +444,7 @@ msgstr "File" #~ msgid "File too large (max 50 MB)" #~ msgstr "File too large (max 50 MB)" -#: src/components/DumpCreateModal.tsx:200 +#: src/components/DumpCreateModal.tsx:209 msgid "File too large (max 50 MB)." msgstr "File too large (max 50 MB)." @@ -443,26 +461,32 @@ msgstr "Follow {targetUsername}" msgid "Follow playlist" msgstr "Follow playlist" -#: src/pages/index/FollowedFeed.tsx:358 +#: src/pages/index/FollowedFeed.tsx:371 msgid "Follow some public playlists to see their dumps here." msgstr "Follow some public playlists to see their dumps here." -#: src/pages/index/FollowedFeed.tsx:344 +#: src/pages/index/FollowedFeed.tsx:357 msgid "Follow some users to see their dumps here." msgstr "Follow some users to see their dumps here." #: src/components/FeedTabBar.tsx:47 +#: src/pages/UserPublicProfile.tsx:964 msgid "Followed" msgstr "Followed" #. placeholder {0}: followed.items.length #. placeholder {1}: followed.hasMore ? "+" : "" -#: src/pages/UserPlaylists.tsx:416 +#: src/pages/UserPlaylists.tsx:430 msgid "Followed ({0}{1})" msgstr "Followed ({0}{1})" +#: src/pages/UserPublicProfile.tsx:1109 +msgid "Followed playlists" +msgstr "Followed playlists" + #: src/components/FollowButton.tsx:37 #: src/components/FollowButton.tsx:64 +#: src/pages/UserPublicProfile.tsx:1072 msgid "Following" msgstr "Following" @@ -470,11 +494,11 @@ msgstr "Following" #~ msgid "Forbidden" #~ msgstr "Forbidden" -#: src/pages/index/FollowedFeed.tsx:324 +#: src/pages/index/FollowedFeed.tsx:337 msgid "From people" msgstr "From people" -#: src/pages/index/FollowedFeed.tsx:331 +#: src/pages/index/FollowedFeed.tsx:344 msgid "From playlists" msgstr "From playlists" @@ -486,7 +510,7 @@ msgstr "Hot" #~ msgid "Invalid email address" #~ msgstr "Invalid email address" -#: src/pages/UserRegister.tsx:104 +#: src/pages/UserRegister.tsx:106 msgid "Invalid invite" msgstr "Invalid invite" @@ -507,10 +531,15 @@ msgstr "Invalid invite" #~ msgid "Invite already used" #~ msgstr "Invite already used" -#: src/pages/UserPublicProfile.tsx:651 +#: src/pages/UserPublicProfile.tsx:773 msgid "invited by" msgstr "invited by" +#: src/pages/UserPublicProfile.tsx:971 +#: src/pages/UserPublicProfile.tsx:1149 +msgid "Invitees" +msgstr "Invitees" + #: src/components/FeedTabBar.tsx:39 msgid "Journal" msgstr "Journal" @@ -519,77 +548,84 @@ msgstr "Journal" msgid "just now" msgstr "just now" +#: src/pages/UserPublicProfile.tsx:1227 +msgid "Light" +msgstr "Light" + #: src/contexts/WSProvider.tsx:359 msgid "Live updates are temporarily disconnected. Trying to reconnect…" msgstr "Live updates are temporarily disconnected. Trying to reconnect…" -#: src/components/AppHeader.tsx:83 +#: src/components/AppHeader.tsx:84 msgid "Live updates unavailable." msgstr "Live updates unavailable." -#: src/pages/Notifications.tsx:386 +#: src/pages/Notifications.tsx:396 msgid "Load more" msgstr "Load more" -#: src/pages/Dump.tsx:193 -#: src/pages/DumpEdit.tsx:143 +#: src/pages/Dump.tsx:194 +#: src/pages/DumpEdit.tsx:146 msgid "Loading dump…" msgstr "Loading dump…" -#: src/pages/index/FollowedFeed.tsx:103 -#: src/pages/index/HotFeed.tsx:52 -#: src/pages/index/JournalFeed.tsx:65 -#: src/pages/index/NewFeed.tsx:52 -#: src/pages/Search.tsx:239 -#: src/pages/UserDumps.tsx:111 -#: src/pages/UserPlaylists.tsx:409 -#: src/pages/UserPlaylists.tsx:436 -#: src/pages/UserUpvoted.tsx:183 +#: src/pages/index/FollowedFeed.tsx:109 +#: src/pages/index/HotFeed.tsx:64 +#: src/pages/index/JournalFeed.tsx:77 +#: src/pages/index/NewFeed.tsx:64 +#: src/pages/Search.tsx:244 +#: src/pages/UserDumps.tsx:93 +#: src/pages/UserPlaylists.tsx:417 +#: src/pages/UserPlaylists.tsx:452 +#: src/pages/UserUpvoted.tsx:177 msgid "Loading more…" msgstr "Loading more…" -#: src/pages/PlaylistDetail.tsx:590 +#: src/pages/PlaylistDetail.tsx:595 msgid "Loading playlist…" msgstr "Loading playlist…" -#: src/pages/UserPublicProfile.tsx:590 +#: src/pages/UserPublicProfile.tsx:711 msgid "Loading profile…" msgstr "Loading profile…" -#: src/components/PlaylistMembershipPanel.tsx:26 -#: src/components/TextEditor.tsx:273 -#: src/pages/index/FollowedFeed.tsx:74 -#: src/pages/index/HotFeed.tsx:29 -#: src/pages/index/JournalFeed.tsx:41 -#: src/pages/index/NewFeed.tsx:29 -#: src/pages/Notifications.tsx:318 -#: src/pages/Notifications.tsx:386 -#: src/pages/UserDumps.tsx:50 -#: src/pages/UserPlaylists.tsx:341 -#: src/pages/UserUpvoted.tsx:122 +#: src/components/PlaylistMembershipPanel.tsx:28 +#: src/components/TextEditor.tsx:289 +#: src/pages/index/FollowedFeed.tsx:76 +#: src/pages/index/HotFeed.tsx:32 +#: src/pages/index/JournalFeed.tsx:44 +#: src/pages/index/NewFeed.tsx:32 +#: src/pages/Notifications.tsx:319 +#: src/pages/Notifications.tsx:395 +#: src/pages/UserDumps.tsx:51 +#: src/pages/UserPlaylists.tsx:342 +#: src/pages/UserPublicProfile.tsx:1077 +#: src/pages/UserPublicProfile.tsx:1114 +#: src/pages/UserPublicProfile.tsx:1154 +#: src/pages/UserUpvoted.tsx:123 msgid "Loading…" msgstr "Loading…" #: src/components/AppHeader.tsx:74 -#: src/pages/UserLogin.tsx:62 -#: src/pages/UserLogin.tsx:91 +#: src/pages/UserLogin.tsx:63 +#: src/pages/UserLogin.tsx:93 msgid "Log in" msgstr "Log in" -#: src/pages/UserPublicProfile.tsx:610 -#: src/pages/UserPublicProfile.tsx:738 +#: src/pages/UserPublicProfile.tsx:732 +#: src/pages/UserPublicProfile.tsx:865 msgid "Log out" msgstr "Log out" -#: src/pages/UserLogin.tsx:90 +#: src/pages/UserLogin.tsx:92 msgid "Logging in…" msgstr "Logging in…" -#: src/pages/UserLogin.tsx:65 +#: src/pages/UserLogin.tsx:67 msgid "Login failed" msgstr "Login failed" -#: src/components/FileDropZone.tsx:141 +#: src/components/FileDropZone.tsx:145 msgid "Max 50 MB" msgstr "Max 50 MB" @@ -601,7 +637,7 @@ msgstr "new" msgid "New" msgstr "New" -#: src/components/DumpCreateModal.tsx:275 +#: src/components/DumpCreateModal.tsx:284 msgid "New dump" msgstr "New dump" @@ -609,39 +645,44 @@ msgstr "New dump" msgid "New playlist" msgstr "New playlist" -#: src/pages/PlaylistDetail.tsx:783 +#: src/pages/PlaylistDetail.tsx:798 msgid "No dumps in this playlist yet." msgstr "No dumps in this playlist yet." -#: src/pages/Search.tsx:220 +#: src/pages/Search.tsx:224 msgid "No dumps match \"{q}\"." msgstr "No dumps match \"{q}\"." -#: src/pages/index/HotFeed.tsx:32 -#: src/pages/index/JournalFeed.tsx:44 -#: src/pages/index/NewFeed.tsx:32 +#: src/pages/index/HotFeed.tsx:40 +#: src/pages/index/JournalFeed.tsx:52 +#: src/pages/index/NewFeed.tsx:40 msgid "No dumps yet. Be the first!" msgstr "No dumps yet. Be the first!" -#: src/components/TextEditor.tsx:274 +#: src/components/TextEditor.tsx:292 msgid "No emoji found." msgstr "No emoji found." -#: src/pages/UserPlaylists.tsx:424 +#: src/pages/UserPlaylists.tsx:439 +#: src/pages/UserPublicProfile.tsx:1122 msgid "No followed playlists yet." msgstr "No followed playlists yet." -#: src/pages/Search.tsx:273 +#: src/pages/UserPublicProfile.tsx:1167 +msgid "No invitees yet." +msgstr "No invitees yet." + +#: src/pages/Search.tsx:283 msgid "No playlists match \"{q}\"." msgstr "No playlists match \"{q}\"." -#: src/components/PlaylistMembershipPanel.tsx:28 -#: src/pages/UserPlaylists.tsx:392 -#: src/pages/UserPublicProfile.tsx:865 +#: src/components/PlaylistMembershipPanel.tsx:34 +#: src/pages/UserPlaylists.tsx:397 +#: src/pages/UserPublicProfile.tsx:1043 msgid "No playlists yet." msgstr "No playlists yet." -#: src/pages/Search.tsx:249 +#: src/pages/Search.tsx:257 msgid "No users match \"{q}\"." msgstr "No users match \"{q}\"." @@ -649,11 +690,15 @@ msgstr "No users match \"{q}\"." #~ msgid "Not authenticated" #~ msgstr "Not authenticated" -#: src/pages/Notifications.tsx:327 -#: src/pages/UserDumps.tsx:92 -#: src/pages/UserPublicProfile.tsx:930 -#: src/pages/UserPublicProfile.tsx:1047 -#: src/pages/UserUpvoted.tsx:154 +#: src/pages/UserPublicProfile.tsx:1085 +msgid "Not following anyone yet." +msgstr "Not following anyone yet." + +#: src/pages/Notifications.tsx:330 +#: src/pages/UserDumps.tsx:123 +#: src/pages/UserPublicProfile.tsx:1292 +#: src/pages/UserPublicProfile.tsx:1415 +#: src/pages/UserUpvoted.tsx:195 msgid "Nothing here yet." msgstr "Nothing here yet." @@ -674,12 +719,12 @@ msgstr "Open search" msgid "or <0>browse files" msgstr "or <0>browse files" -#: src/pages/UserLogin.tsx:80 +#: src/pages/UserLogin.tsx:82 msgid "Password" msgstr "Password" #. placeholder {0}: VALIDATION.PASSWORD_MIN -#: src/pages/UserRegister.tsx:142 +#: src/pages/UserRegister.tsx:147 msgid "Password (min. {0} characters)" msgstr "Password (min. {0} characters)" @@ -698,78 +743,79 @@ msgstr "Password (min. {0} characters)" #: src/components/AppHeader.tsx:50 #: src/components/UserMenu.tsx:62 #: src/pages/Search.tsx:175 -#: src/pages/UserPlaylists.tsx:366 +#: src/pages/UserPlaylists.tsx:368 +#: src/pages/UserPublicProfile.tsx:957 msgid "Playlists" msgstr "Playlists" #. placeholder {0}: playlists.items.length #. placeholder {1}: playlists.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:845 +#: src/pages/UserPublicProfile.tsx:1016 msgid "Playlists ({0}{1})" msgstr "Playlists ({0}{1})" -#: src/components/DumpCreateModal.tsx:193 +#: src/components/DumpCreateModal.tsx:202 msgid "Please select a file." msgstr "Please select a file." -#: src/components/CommentThread.tsx:472 +#: src/components/CommentThread.tsx:499 msgid "Post comment" msgstr "Post comment" -#: src/components/CommentThread.tsx:342 +#: src/components/CommentThread.tsx:362 msgid "Post reply" msgstr "Post reply" -#: src/components/CommentThread.tsx:342 -#: src/components/CommentThread.tsx:472 +#: src/components/CommentThread.tsx:361 +#: src/components/CommentThread.tsx:498 msgid "Posting…" msgstr "Posting…" #: src/components/DumpCard.tsx:91 -#: src/components/PlaylistCard.tsx:71 -#: src/components/PlaylistMembershipPanel.tsx:47 -#: src/pages/Dump.tsx:282 -#: src/pages/PlaylistDetail.tsx:748 +#: src/components/PlaylistCard.tsx:73 +#: src/components/PlaylistMembershipPanel.tsx:55 +#: src/pages/Dump.tsx:284 +#: src/pages/PlaylistDetail.tsx:759 msgid "private" msgstr "private" -#: src/components/DumpCreateModal.tsx:397 -#: src/components/PlaylistCreateForm.tsx:94 -#: src/pages/DumpEdit.tsx:274 -#: src/pages/PlaylistDetail.tsx:737 +#: src/components/DumpCreateModal.tsx:411 +#: src/components/PlaylistCreateForm.tsx:99 +#: src/pages/DumpEdit.tsx:285 +#: src/pages/PlaylistDetail.tsx:746 msgid "Private" msgstr "Private" -#: src/components/PlaylistCard.tsx:71 -#: src/pages/PlaylistDetail.tsx:748 +#: src/components/PlaylistCard.tsx:72 +#: src/pages/PlaylistDetail.tsx:758 msgid "public" msgstr "public" -#: src/components/DumpCreateModal.tsx:389 -#: src/components/PlaylistCreateForm.tsx:87 -#: src/pages/DumpEdit.tsx:267 -#: src/pages/PlaylistDetail.tsx:730 +#: src/components/DumpCreateModal.tsx:403 +#: src/components/PlaylistCreateForm.tsx:92 +#: src/pages/DumpEdit.tsx:278 +#: src/pages/PlaylistDetail.tsx:739 msgid "Public" msgstr "Public" -#: src/pages/DumpEdit.tsx:206 +#: src/pages/DumpEdit.tsx:214 msgid "Refresh metadata" msgstr "Refresh metadata" -#: src/pages/DumpEdit.tsx:206 +#: src/pages/DumpEdit.tsx:213 msgid "Refreshing…" msgstr "Refreshing…" -#: src/pages/UserRegister.tsx:115 -#: src/pages/UserRegister.tsx:155 +#: src/pages/UserRegister.tsx:118 +#: src/pages/UserRegister.tsx:160 msgid "Register" msgstr "Register" -#: src/pages/UserRegister.tsx:154 +#: src/pages/UserRegister.tsx:159 msgid "Registering…" msgstr "Registering…" -#: src/pages/UserRegister.tsx:118 +#: src/pages/UserRegister.tsx:122 msgid "Registration failed" msgstr "Registration failed" @@ -777,35 +823,35 @@ msgstr "Registration failed" msgid "Remove file" msgstr "Remove file" -#: src/pages/PlaylistDetail.tsx:838 +#: src/pages/PlaylistDetail.tsx:853 msgid "Remove from playlist" msgstr "Remove from playlist" -#: src/pages/DumpEdit.tsx:241 +#: src/pages/DumpEdit.tsx:251 msgid "Replace file" msgstr "Replace file" -#: src/components/CommentThread.tsx:284 +#: src/components/CommentThread.tsx:297 msgid "Reply" msgstr "Reply" -#: src/pages/Dump.tsx:209 -#: src/pages/DumpEdit.tsx:159 +#: src/pages/Dump.tsx:211 +#: src/pages/DumpEdit.tsx:163 msgid "Retry" msgstr "Retry" -#: src/components/CommentThread.tsx:257 -#: src/pages/DumpEdit.tsx:291 -#: src/pages/PlaylistDetail.tsx:665 -#: src/pages/UserPublicProfile.tsx:692 -#: src/pages/UserPublicProfile.tsx:765 +#: src/components/CommentThread.tsx:270 +#: src/pages/DumpEdit.tsx:306 +#: src/pages/PlaylistDetail.tsx:673 +#: src/pages/UserPublicProfile.tsx:816 +#: src/pages/UserPublicProfile.tsx:894 msgid "Save" msgstr "Save" -#: src/components/CommentThread.tsx:257 -#: src/pages/PlaylistDetail.tsx:665 -#: src/pages/UserPublicProfile.tsx:692 -#: src/pages/UserPublicProfile.tsx:765 +#: src/components/CommentThread.tsx:269 +#: src/pages/PlaylistDetail.tsx:673 +#: src/pages/UserPublicProfile.tsx:815 +#: src/pages/UserPublicProfile.tsx:894 msgid "Saving…" msgstr "Saving…" @@ -817,11 +863,11 @@ msgstr "Search" msgid "Search dumps, users, playlists…" msgstr "Search dumps, users, playlists…" -#: src/pages/Search.tsx:214 +#: src/pages/Search.tsx:218 msgid "Search failed" msgstr "Search failed" -#: src/pages/Search.tsx:210 +#: src/pages/Search.tsx:213 msgid "Searching…" msgstr "Searching…" @@ -829,40 +875,48 @@ msgstr "Searching…" msgid "Server unreachable" msgstr "Server unreachable" +#: src/pages/UserPublicProfile.tsx:979 +msgid "Settings" +msgstr "Settings" + #: src/components/PageError.tsx:13 msgid "Something went wrong" msgstr "Something went wrong" +#: src/pages/UserPublicProfile.tsx:1191 +msgid "Style" +msgstr "Style" + #: src/components/SearchBar.tsx:71 msgid "Submit search" msgstr "Submit search" -#: src/pages/UserPublicProfile.tsx:755 +#: src/pages/UserPublicProfile.tsx:882 msgid "Tell people about yourself…" msgstr "Tell people about yourself…" -#: src/components/DumpCreateModal.tsx:377 -#: src/pages/DumpEdit.tsx:256 +#: src/components/DumpCreateModal.tsx:390 +#: src/pages/DumpEdit.tsx:266 msgid "Tell the community what makes this worth their time..." msgstr "Tell the community what makes this worth their time..." -#: src/pages/UserRegister.tsx:105 +#: src/pages/UserRegister.tsx:107 msgid "This invite link is missing, expired, or already used." msgstr "This invite link is missing, expired, or already used." -#: src/pages/UserLogin.tsx:96 +#: src/pages/UserLogin.tsx:98 msgid "This is a mirage." msgstr "This is a mirage." -#: src/components/PlaylistCreateForm.tsx:69 +#: src/components/PlaylistCreateForm.tsx:72 msgid "Title" msgstr "Title" -#: src/pages/Notifications.tsx:341 +#: src/pages/Notifications.tsx:346 msgid "Today" msgstr "Today" -#: src/pages/PlaylistDetail.tsx:850 +#: src/pages/PlaylistDetail.tsx:865 msgid "Undo" msgstr "Undo" @@ -878,30 +932,30 @@ msgstr "Unfollow {targetUsername}" msgid "Unfollow playlist" msgstr "Unfollow playlist" -#: src/pages/UserPublicProfile.tsx:515 +#: src/pages/UserPublicProfile.tsx:632 msgid "Upload failed" msgstr "Upload failed" -#: src/components/DumpCreateModal.tsx:418 +#: src/components/DumpCreateModal.tsx:433 msgid "Uploading…" msgstr "Uploading…" -#: src/pages/UserUpvoted.tsx:150 +#: src/pages/UserUpvoted.tsx:191 msgid "Upvoted" msgstr "Upvoted" #. placeholder {0}: votes.items.length #. placeholder {1}: votes.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:829 +#: src/pages/UserPublicProfile.tsx:998 msgid "Upvoted ({0}{1})" msgstr "Upvoted ({0}{1})" -#: src/components/DumpCreateModal.tsx:322 -#: src/pages/DumpEdit.tsx:221 +#: src/components/DumpCreateModal.tsx:332 +#: src/pages/DumpEdit.tsx:230 msgid "URL" msgstr "URL" -#: src/components/DumpCreateModal.tsx:176 +#: src/components/DumpCreateModal.tsx:185 msgid "URL is required." msgstr "URL is required." @@ -909,8 +963,8 @@ msgstr "URL is required." msgid "User menu" msgstr "User menu" -#: src/pages/UserLogin.tsx:72 -#: src/pages/UserRegister.tsx:125 +#: src/pages/UserLogin.tsx:74 +#: src/pages/UserRegister.tsx:129 msgid "Username" msgstr "Username" @@ -926,36 +980,43 @@ msgstr "Username" msgid "Users" msgstr "Users" -#: src/pages/UserPublicProfile.tsx:878 -#: src/pages/UserPublicProfile.tsx:948 -#: src/pages/UserPublicProfile.tsx:1074 +#: src/pages/UserPublicProfile.tsx:1062 +#: src/pages/UserPublicProfile.tsx:1100 +#: src/pages/UserPublicProfile.tsx:1137 +#: src/pages/UserPublicProfile.tsx:1313 +#: src/pages/UserPublicProfile.tsx:1445 msgid "View all →" msgstr "View all →" -#: src/components/DumpCreateModal.tsx:432 +#: src/components/DumpCreateModal.tsx:447 msgid "View dump →" msgstr "View dump →" -#: src/components/DumpCreateModal.tsx:370 -#: src/pages/DumpEdit.tsx:250 +#: src/components/DumpCreateModal.tsx:383 +#: src/pages/DumpEdit.tsx:260 msgid "Why are you dumping this?" msgstr "Why are you dumping this?" -#: src/components/CommentThread.tsx:329 +#: src/components/CommentThread.tsx:342 msgid "Write a reply…" msgstr "Write a reply…" -#: src/pages/Notifications.tsx:341 +#: src/pages/Notifications.tsx:348 msgid "Yesterday" msgstr "Yesterday" -#: src/pages/Notifications.tsx:329 +#: src/pages/Notifications.tsx:333 msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." -#: src/pages/index/HotFeed.tsx:54 -#: src/pages/index/JournalFeed.tsx:67 -#: src/pages/index/NewFeed.tsx:54 -#: src/pages/Search.tsx:242 +#: src/pages/index/FollowedFeed.tsx:114 +#: src/pages/index/HotFeed.tsx:69 +#: src/pages/index/JournalFeed.tsx:82 +#: src/pages/index/NewFeed.tsx:69 +#: src/pages/Search.tsx:249 +#: src/pages/UserDumps.tsx:98 +#: src/pages/UserPlaylists.tsx:422 +#: src/pages/UserPlaylists.tsx:457 +#: src/pages/UserUpvoted.tsx:182 msgid "You've reached the end." msgstr "You've reached the end." diff --git a/src/locales/fr.js b/src/locales/fr.js new file mode 100644 index 0000000..09dd0bd --- /dev/null +++ b/src/locales/fr.js @@ -0,0 +1,5 @@ +/*eslint-disable*/ module.exports = { + messages: JSON.parse( + '{"-K9EZb":["Ajouter un e-mail…"],"-OxI15":["Collections suivies"],"-Ya-b9":["Enregistrement échoué"],"-siMqD":["Journal"],"0kWhlg":["File too large (max 5 MB)"],"1CalO6":["Style"],"1HfJWf":["Rechercher des recos, utilisateurs, collections…"],"1cbYY_":["Votés (",["0"],["1"],")"],"1njn7W":["Clair"],"1utXA6":["Recos"],"26iNma":["Publier le commentaire"],"2Hlmdt":["Écrire une réponse…"],"2ygf_L":["← Retour"],"3KKSM4":["privé"],"3T1cI4":["Unexpected server error"],"3yfh3D":["<0>",["0"]," a suivi votre collection <1>",["1"],""],"49voTZ":[["label"]," (",["count"],")"],"4B6w_o":["Recommandé !"],"4GKuCs":["Connexion échouée"],"4HH9iB":["Aucun abonnement pour le moment."],"4RtQ1k":["Ne plus suivre ",["targetUsername"]],"4yj9xV":["Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"],"5cC8f2":["Modifié le ",["0"]],"5oD9f_":["Plus tôt"],"6Qly-0":["un commentaire"],"6gRgw8":["Réessayer"],"7JBW66":["Forbidden"],"7PHCIN":["Annuler la suppression"],"7d1a0d":["Public"],"7sNhEz":["Nom d\'utilisateur"],"8ZsakT":["Mot de passe"],"8pxhI8":["Veuillez sélectionner un fichier."],"9l4qcT":["Déposez un fichier de remplacement ici"],"9uI_rE":["Annuler"],"A0y396":["+ Inviter quelqu\'un"],"A1taO8":["Rechercher"],"AQbgNR":["Suivre ",["targetUsername"]],"ATGYL1":["Adresse e-mail"],"AZctoV":[["0","plural",{"one":["#"," commentaire"],"other":["#"," commentaires"]}]],"Ade-6d":["Mises à jour en direct indisponibles."],"CI50ct":["Voté"],"Cj24wt":["Inscription…"],"DPfwMq":["Terminé"],"DdeHXH":["Supprimer cette reco ? Cette action est irréversible."],"Dp1JhP":["<0>",["0"]," a voté pour <1>",["1"],""],"ECiS12":["Voir la reco →"],"ExR0Fr":["L\'URL est obligatoire."],"F5Js1v":["Ne plus suivre la collection"],"FgAxTj":["Se déconnecter"],"Fxf4jq":["Description (facultatif)"],"GNSsCc":["Impossible de créer la collection"],"GbqhrN":[["0"],"–",["1"]," caractères : lettres, chiffres ou tirets bas"],"GsRMX3":["Playlist not found"],"H8pzW-":["Impossible de mettre à jour l\'avatar"],"HTLDA4":["Chargement…"],"IZX7TO":["Impossible de générer une invitation"],"IagCbF":["URL"],"ImOQa9":["Répondre"],"J2eKUI":["Fichier"],"Jd58Fo":["Tendances"],"K1JdNl":["Username already exists"],"KDGWg5":["Retirer de la collection"],"K_F6pa":["Enregistrement…"],"LLyMkV":["Suivies (",["0"],["1"],")"],"LPAv9E":["il y a ",["days"],"j"],"Lld1jm":["Parlez de vous…"],"MHrjPM":["Titre"],"MKEPCY":["Suivre"],"Mq2B8E":["il y a ",["mins"],"min"],"NR0xa9":["Dites à la communauté pourquoi ça vaut le coup…"],"Nn4kr3":["+ Nouvelle reco"],"Oprv1v":["Mot de passe (min. ",["0"]," caractères)"],"Oz0N9s":["nouveau"],"PiH3UR":["Copié !"],"Pwqkdw":["Chargement…"],"Q6n4F4":["Actualiser les métadonnées"],"QKsaQr":["ou <0>parcourir les fichiers"],"QLtPBd":["Aucune reco dans cette collection pour l\'instant."],"R9Khdg":["Auto"],"RCcPrX":["Supprimer cette collection ? Cette action est irréversible."],"RTksSy":["<0>",["0"]," a commencé à vous suivre"],"RaKjrM":["Impossible d\'enregistrer la modification"],"RcUHRT":["Suivi"],"SBTElJ":["Recherche…"],"Sxm8rQ":["Utilisateurs"],"T9bjWt":["<0>",["0"]," a été ajouté à <1>",["1"],""],"TM1ZbA":["Collections (",["0"],["1"],")"],"Tv9vbB":["Suivre la collection"],"Tz0i8g":["Paramètres"],"U7u3q-":["+ Nouveau"],"UOZith":["Publication échouée"],"UTiUFs":["Récupération…"],"VnNJbN":["De collections"],"VyTYmS":["Changer l\'avatar"],"WpXcBJ":["Rien ici pour l\'instant."],"WtkMN8":["Toutes les ",["0","plural",{"one":["#"," reco votée"],"other":["#"," recos votées"]}]," chargées."],"XILg0L":["Invalid email address"],"XJy2oN":["Connexion…"],"Xan6QP":["Nouvelle reco"],"Xi0Mn4":["← Retour au profil"],"XnL-Eu":["Aucun utilisateur ne correspond à « ",["q"]," »."],"YK1Dhc":["une publication"],"YaSA2K":["Comment not found"],"YpkCca":["Pas encore de collections suivies."],"ZCpU0u":["Aucune collection ne correspond à « ",["q"]," »."],"ZmD2o6":["Créer et ajouter"],"_84wxb":["Toutes les ",["0","plural",{"one":["#"," reco"],"other":["#"," recos"]}]," chargées."],"_DwR-n":["Création…"],"_aept4":["Publier la réponse"],"_t4W-i":["De personnes"],"aAIQg2":["Apparence"],"aDvLhk":["Ajouter un commentaire…"],"alBtu4":["Invalid or expired invite"],"b3Thhd":["Envoi échoué"],"b8XMJ8":[["visibleCount","plural",{"one":["#"," commentaire"],"other":["#"," commentaires"]}]],"bQhwn-":["Chargement de la collection…"],"cILfnJ":["Supprimer le fichier"],"cYP9Sb":["+ Collection"],"cnGeoo":["Supprimer"],"d8DZWS":["Ouvrir la recherche"],"dAs22m":["Remplacer le fichier"],"dEgA5A":["Annuler"],"dMizp8":["Nouvelle collection"],"dSKHAa":["Invalid username or password"],"dTU6Wi":["Password must be at most 128 characters"],"dbc28f":["Pourquoi recommandez-vous ça ?"],"eFSqvc":["Impossible de publier la réponse"],"ePK91l":["Modifier"],"ecUA8p":["Aujourd\'hui"],"ef9nPf":["Chargement de la reco…"],"en9o7K":["Impossible de publier le commentaire"],"fGxPOv":["Ajouter une bio…"],"fI-mNw":["Collections"],"f_akpP":["Max 50 Mo"],"fgLNSM":["S\'inscrire"],"gANddk":["Envoi…"],"gGx5tM":["Modification"],"gIQQwD":["Chargement échoué"],"gLfZlz":["Ajouter à la collection"],"gjJ-sb":["Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion."],"hD7w09":["Vous avez tout lu, tout vu, tout bu."],"hJSliC":["<0>",["0"]," a publié <1>",["1"],""],"hYgDIe":["Créer"],"he3ygx":["Copier"],"iDNBZe":["Notifications"],"ijVyoK":["Impossible de contacter le serveur. Veuillez réessayer."],"isRobC":["Nouveau"],"jGrTH0":["Not authenticated"],"jbernk":["Chargement du profil…"],"joEmfT":["Serveur inaccessible"],"jrZTZl":["Pas encore de recos. Soyez le premier !"],"kLttbL":["Inscription échouée"],"kYYCil":["File too large (max 50 MB)"],"l3JaOO":["Cannot edit a deleted comment"],"lUDifl":["Créées (",["0"],["1"],")"],"lUanmi":["Vous serez notifié lorsque quelqu\'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."],"lY5h1V":[["0","plural",{"one":["#"," reco"],"other":["#"," recos"]}]],"lcfvr_":["Supprimer ce commentaire ?"],"mt6O6E":["C\'est un mirage."],"nbm5sI":["Aucune reco ne correspond à « ",["q"]," »."],"nrjqON":["Vérification de l\'invitation…"],"nwtY4N":["Une erreur est survenue"],"pCpd9p":["<0>",["0"]," vous a mentionné dans <1>",["where"],""],"pvnfJD":["Sombre"],"qIMfNQ":["Supprimer la collection"],"qbDAcy":["Recommander"],"qgx_78":["Suivez des collections publiques pour voir leurs recos ici."],"qvFa8r":["public"],"rCbqPX":["Ce lien d\'invitation est manquant, expiré ou déjà utilisé."],"rg9pXu":["Recherche échouée"],"rtpJqV":["Recos (",["0"],["1"],")"],"sBZMWb":["Invalid URL"],"sQia9P":["Se connecter"],"sTiqbm":["invité par"],"sdP5Aa":["[supprimé]"],"shHs8T":["Saisissez une recherche."],"siMTjB":["File content is not a recognised image (JPEG, PNG, GIF, WebP)"],"smeBfS":["Invitation invalide"],"tfDRzk":["Enregistrer"],"tqKwXl":["Username must be 1–32 characters and contain only letters, numbers, or underscores"],"tvmuQ0":["Thème de couleur"],"u1lDX2":["Récupération de l\'aperçu…"],"u4pkXs":["Invite already used"],"uD0qXQ":["Déposez un fichier ici"],"uMGUnV":["Pas encore de collections."],"ub1EEL":["modifié ",["0"]],"vJBF1r":["Publication…"],"vLhLLO":["Notifications (",["unreadNotificationCount"]," non lues)"],"vuosjb":["Menu utilisateur"],"vwGkYB":["Password must be at least 8 characters"],"wXO4Tg":["il y a ",["hrs"],"h"],"wbXKOv":["Fichier trop volumineux (max 50 Mo)."],"wdiqRH":["Admin access required"],"wixIgH":["Vous avez déjà un compte ? <0>Se connecter"],"x4aBfU":["Dump not found"],"xEWkgZ":["← Retour à toutes les recos"],"xOTzt5":["à l\'instant"],"xPHtx0":["Lancer la recherche"],"xVuNgt":["+ Nouvelle collection"],"xc9O_u":["Supprimer la reco"],"y6sq5j":["Abonné"],"yA_6BX":["Tout voir →"],"yBBtRm":["Suivez des utilisateurs pour voir leurs recos ici."],"yQ2kGp":["Charger plus"],"y_0uwd":["Hier"],"yz7wBu":["Fermer"],"z1uNN0":["Aucun emoji trouvé."],"zVuxvN":["Actualisation…"],"zwBp5t":["Privé"]}', + ), +}; diff --git a/src/locales/fr.po b/src/locales/fr.po index b01c891..aa6fa01 100644 --- a/src/locales/fr.po +++ b/src/locales/fr.po @@ -13,7 +13,7 @@ msgstr "" "Last-Translator: \n" "Language-Team: \n" -#: src/components/CommentThread.tsx:170 +#: src/components/CommentThread.tsx:176 msgid "[deleted]" msgstr "[supprimé]" @@ -23,13 +23,13 @@ msgid "{0, plural, one {# comment} other {# comments}}" msgstr "{0, plural, one {# commentaire} other {# commentaires}}" #. placeholder {0}: playlist.dumpCount -#: src/components/PlaylistCard.tsx:84 +#: src/components/PlaylistCard.tsx:86 msgid "{0, plural, one {# dump} other {# dumps}}" msgstr "{0, plural, one {# reco} other {# recos}}" #. placeholder {0}: VALIDATION.USERNAME_MIN #. placeholder {1}: VALIDATION.USERNAME_MAX -#: src/pages/UserRegister.tsx:128 +#: src/pages/UserRegister.tsx:132 msgid "{0}–{1} characters: letters, numbers, or underscores" msgstr "{0}–{1} caractères : lettres, chiffres ou tirets bas" @@ -49,28 +49,28 @@ msgstr "{label} ({count})" msgid "{mins}m ago" msgstr "il y a {mins}min" -#: src/components/CommentThread.tsx:436 +#: src/components/CommentThread.tsx:459 msgid "{visibleCount, plural, one {# comment} other {# comments}}" msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}" -#: src/pages/PlaylistDetail.tsx:605 -#: src/pages/UserPublicProfile.tsx:606 +#: src/pages/PlaylistDetail.tsx:611 +#: src/pages/UserPublicProfile.tsx:728 msgid "← Back" msgstr "← Retour" -#: src/pages/Dump.tsx:216 -#: src/pages/Dump.tsx:318 -#: src/pages/DumpEdit.tsx:166 +#: src/pages/Dump.tsx:218 +#: src/pages/Dump.tsx:321 +#: src/pages/DumpEdit.tsx:170 msgid "← Back to all dumps" msgstr "← Retour à toutes les recos" -#: src/pages/UserDumps.tsx:61 -#: src/pages/UserPlaylists.tsx:352 -#: src/pages/UserUpvoted.tsx:133 +#: src/pages/UserDumps.tsx:63 +#: src/pages/UserPlaylists.tsx:354 +#: src/pages/UserUpvoted.tsx:135 msgid "← Back to profile" msgstr "← Retour au profil" -#: src/pages/UserPublicProfile.tsx:90 +#: src/pages/UserPublicProfile.tsx:93 msgid "+ Invite someone" msgstr "+ Inviter quelqu'un" @@ -78,17 +78,17 @@ msgstr "+ Inviter quelqu'un" msgid "+ New" msgstr "+ Nouveau" -#: src/pages/UserDumps.tsx:82 -#: src/pages/UserPublicProfile.tsx:922 +#: src/pages/UserDumps.tsx:114 +#: src/pages/UserPublicProfile.tsx:1282 msgid "+ New dump" msgstr "+ Nouvelle reco" #: src/components/NewPlaylistForm.tsx:30 -#: src/components/PlaylistMembershipPanel.tsx:72 +#: src/components/PlaylistMembershipPanel.tsx:80 msgid "+ New playlist" msgstr "+ Nouvelle collection" -#: src/pages/Dump.tsx:248 +#: src/pages/Dump.tsx:250 msgid "+ Playlist" msgstr "+ Collection" @@ -134,64 +134,70 @@ msgstr "un commentaire" msgid "a post" msgstr "une publication" -#: src/pages/UserPublicProfile.tsx:802 +#: src/pages/UserPublicProfile.tsx:931 msgid "Add a bio…" msgstr "Ajouter une bio…" -#: src/components/CommentThread.tsx:456 +#: src/components/CommentThread.tsx:479 msgid "Add a comment…" msgstr "Ajouter un commentaire…" -#: src/pages/UserPublicProfile.tsx:718 +#: src/pages/UserPublicProfile.tsx:842 msgid "Add email…" msgstr "Ajouter un e-mail…" #: src/components/AddToPlaylistModal.tsx:64 -#: src/components/DumpCreateModal.tsx:275 +#: src/components/DumpCreateModal.tsx:284 msgid "Add to playlist" msgstr "Ajouter à la collection" -#. placeholder {0}: dumps.length #: src/pages/UserDumps.tsx:114 msgid "All {0, plural, one {# dump} other {# dumps}} loaded." msgstr "Toutes les {0, plural, one {# reco} other {# recos}} chargées." -#. placeholder {0}: votes.length #: src/pages/UserUpvoted.tsx:187 msgid "All {0, plural, one {# upvoted dump} other {# upvoted dumps}} loaded." msgstr "Toutes les {0, plural, one {# reco votée} other {# recos votées}} chargées." -#: src/pages/UserRegister.tsx:160 +#: src/pages/UserRegister.tsx:165 msgid "Already have an account? <0>Log in" msgstr "Vous avez déjà un compte ? <0>Se connecter" +#: src/pages/UserPublicProfile.tsx:1186 +msgid "Appearance" +msgstr "Apparence" + +#: src/pages/UserPublicProfile.tsx:1220 +msgid "Auto" +msgstr "Auto" + #: src/contexts/WSProvider.tsx:168 #: src/contexts/WSProvider.tsx:360 msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects." msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion." -#: src/components/CommentThread.tsx:268 -#: src/components/CommentThread.tsx:353 -#: src/components/CommentThread.tsx:483 +#: src/components/CommentThread.tsx:281 +#: src/components/CommentThread.tsx:373 +#: src/components/CommentThread.tsx:510 #: src/components/ConfirmModal.tsx:32 -#: src/components/DumpCreateModal.tsx:408 -#: src/components/PlaylistCreateForm.tsx:105 -#: src/pages/DumpEdit.tsx:288 -#: src/pages/PlaylistDetail.tsx:672 -#: src/pages/UserPublicProfile.tsx:700 -#: src/pages/UserPublicProfile.tsx:773 +#: src/components/DumpCreateModal.tsx:422 +#: src/components/PlaylistCreateForm.tsx:112 +#: src/pages/DumpEdit.tsx:299 +#: src/pages/PlaylistDetail.tsx:680 +#: src/pages/UserPublicProfile.tsx:824 +#: src/pages/UserPublicProfile.tsx:902 msgid "Cancel" msgstr "Annuler" -#: src/pages/PlaylistDetail.tsx:848 +#: src/pages/PlaylistDetail.tsx:863 msgid "Cancel removal" msgstr "Annuler la suppression" -#: src/pages/UserPublicProfile.tsx:633 +#: src/pages/UserPublicProfile.tsx:755 msgid "Change avatar" msgstr "Changer l'avatar" -#: src/pages/UserRegister.tsx:94 +#: src/pages/UserRegister.tsx:95 msgid "Checking invite…" msgstr "Vérification de l'invitation…" @@ -199,75 +205,83 @@ msgstr "Vérification de l'invitation…" msgid "Close" msgstr "Fermer" -#: src/pages/UserPublicProfile.tsx:81 +#: src/pages/UserPublicProfile.tsx:1212 +msgid "Color scheme" +msgstr "Thème de couleur" + +#: src/pages/UserPublicProfile.tsx:84 msgid "Copied!" msgstr "Copié !" -#: src/pages/UserPublicProfile.tsx:81 +#: src/pages/UserPublicProfile.tsx:84 msgid "Copy" msgstr "Copier" -#: src/components/CommentThread.tsx:108 -#: src/components/CommentThread.tsx:147 -#: src/components/CommentThread.tsx:425 +#: src/components/CommentThread.tsx:111 +#: src/components/CommentThread.tsx:153 +#: src/components/CommentThread.tsx:448 msgid "Could not reach the server. Please try again." msgstr "Impossible de contacter le serveur. Veuillez réessayer." -#: src/components/PlaylistCreateForm.tsx:116 +#: src/components/PlaylistCreateForm.tsx:124 msgid "Create" msgstr "Créer" -#: src/components/PlaylistCreateForm.tsx:115 +#: src/components/PlaylistCreateForm.tsx:123 msgid "Create & Add" msgstr "Créer et ajouter" #. placeholder {0}: created.items.length #. placeholder {1}: created.hasMore ? "+" : "" -#: src/pages/UserPlaylists.tsx:386 +#: src/pages/UserPlaylists.tsx:388 msgid "Created ({0}{1})" msgstr "Créées ({0}{1})" -#: src/components/PlaylistCreateForm.tsx:113 +#: src/components/PlaylistCreateForm.tsx:121 msgid "Creating…" msgstr "Création…" -#: src/components/CommentThread.tsx:306 -#: src/components/CommentThread.tsx:312 +#: src/pages/UserPublicProfile.tsx:1234 +msgid "Dark" +msgstr "Sombre" + +#: src/components/CommentThread.tsx:319 +#: src/components/CommentThread.tsx:325 #: src/components/ConfirmModal.tsx:16 -#: src/pages/PlaylistDetail.tsx:679 +#: src/pages/PlaylistDetail.tsx:687 msgid "Delete" msgstr "Supprimer" -#: src/pages/DumpEdit.tsx:284 -#: src/pages/DumpEdit.tsx:300 +#: src/pages/DumpEdit.tsx:295 +#: src/pages/DumpEdit.tsx:315 msgid "Delete dump" msgstr "Supprimer la reco" -#: src/components/PlaylistCard.tsx:107 -#: src/pages/PlaylistDetail.tsx:861 -#: src/pages/UserPlaylists.tsx:443 +#: src/components/PlaylistCard.tsx:109 +#: src/pages/PlaylistDetail.tsx:876 +#: src/pages/UserPlaylists.tsx:465 msgid "Delete playlist" msgstr "Supprimer la collection" -#: src/components/CommentThread.tsx:311 +#: src/components/CommentThread.tsx:324 msgid "Delete this comment?" msgstr "Supprimer ce commentaire ?" -#: src/pages/DumpEdit.tsx:299 +#: src/pages/DumpEdit.tsx:314 msgid "Delete this dump? This cannot be undone." msgstr "Supprimer cette reco ? Cette action est irréversible." -#: src/pages/PlaylistDetail.tsx:860 -#: src/pages/UserPlaylists.tsx:442 +#: src/pages/PlaylistDetail.tsx:875 +#: src/pages/UserPlaylists.tsx:464 msgid "Delete this playlist? This cannot be undone." msgstr "Supprimer cette collection ? Cette action est irréversible." -#: src/components/PlaylistCreateForm.tsx:76 -#: src/pages/PlaylistDetail.tsx:710 +#: src/components/PlaylistCreateForm.tsx:80 +#: src/pages/PlaylistDetail.tsx:718 msgid "Description (optional)" msgstr "Description (facultatif)" -#: src/components/DumpCreateModal.tsx:453 +#: src/components/DumpCreateModal.tsx:468 msgid "Done" msgstr "Terminé" @@ -275,130 +289,134 @@ msgstr "Terminé" msgid "Drop a file here" msgstr "Déposez un fichier ici" -#: src/pages/DumpEdit.tsx:242 +#: src/pages/DumpEdit.tsx:252 msgid "Drop a replacement here" msgstr "Déposez un fichier de remplacement ici" -#: src/components/DumpCreateModal.tsx:419 +#: src/components/DumpCreateModal.tsx:434 msgid "Dump it" msgstr "Recommander" -#: src/components/DumpCreateModal.tsx:430 +#: src/components/DumpCreateModal.tsx:445 msgid "Dumped!" msgstr "Recommandé !" #: src/pages/Search.tsx:172 -#: src/pages/UserDumps.tsx:75 +#: src/pages/UserDumps.tsx:107 +#: src/pages/UserPublicProfile.tsx:950 msgid "Dumps" msgstr "Recos" #. placeholder {0}: dumps.items.length #. placeholder {1}: dumps.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:817 +#: src/pages/UserPublicProfile.tsx:987 msgid "Dumps ({0}{1})" msgstr "Recos ({0}{1})" -#: src/pages/Notifications.tsx:341 +#: src/pages/Notifications.tsx:349 msgid "Earlier" msgstr "Plus tôt" -#: src/components/CommentThread.tsx:297 -#: src/pages/Dump.tsx:315 -#: src/pages/PlaylistDetail.tsx:698 +#: src/components/CommentThread.tsx:310 +#: src/pages/Dump.tsx:317 +#: src/pages/PlaylistDetail.tsx:706 msgid "Edit" msgstr "Modifier" #. placeholder {0}: relativeTime(comment.updatedAt) #. placeholder {0}: relativeTime(dump.updatedAt) #. placeholder {0}: relativeTime(playlist.updatedAt) -#: src/components/CommentThread.tsx:231 -#: src/pages/Dump.tsx:276 -#: src/pages/PlaylistDetail.tsx:768 +#: src/components/CommentThread.tsx:237 +#: src/pages/Dump.tsx:278 +#: src/pages/PlaylistDetail.tsx:779 msgid "edited {0}" msgstr "modifié {0}" #. placeholder {0}: comment.updatedAt.toLocaleString() #. placeholder {0}: dump.updatedAt.toLocaleString() #. placeholder {0}: playlist.updatedAt.toLocaleString() -#: src/components/CommentThread.tsx:229 -#: src/pages/Dump.tsx:274 -#: src/pages/PlaylistDetail.tsx:765 +#: src/components/CommentThread.tsx:235 +#: src/pages/Dump.tsx:276 +#: src/pages/PlaylistDetail.tsx:776 msgid "Edited {0}" msgstr "Modifié le {0}" -#: src/pages/DumpEdit.tsx:180 +#: src/pages/DumpEdit.tsx:185 msgid "Editing" msgstr "Modification" -#: src/pages/UserRegister.tsx:135 +#: src/pages/UserRegister.tsx:140 msgid "Email address" msgstr "Adresse e-mail" -#: src/pages/Search.tsx:206 +#: src/pages/Search.tsx:207 msgid "Enter a query to search." msgstr "Saisissez une recherche." -#: src/components/PlaylistCreateForm.tsx:59 -#: src/components/PlaylistCreateForm.tsx:97 +#: src/components/PlaylistCreateForm.tsx:62 +#: src/components/PlaylistCreateForm.tsx:103 msgid "Failed to create playlist" msgstr "Impossible de créer la collection" -#: src/pages/UserPublicProfile.tsx:62 #: src/pages/UserPublicProfile.tsx:65 -#: src/pages/UserPublicProfile.tsx:92 +#: src/pages/UserPublicProfile.tsx:68 +#: src/pages/UserPublicProfile.tsx:96 msgid "Failed to generate invite" msgstr "Impossible de générer une invitation" -#: src/pages/index/FollowedFeed.tsx:77 -#: src/pages/index/HotFeed.tsx:30 -#: src/pages/index/JournalFeed.tsx:42 -#: src/pages/index/NewFeed.tsx:30 -#: src/pages/Notifications.tsx:321 +#: src/pages/index/FollowedFeed.tsx:81 +#: src/pages/index/HotFeed.tsx:36 +#: src/pages/index/JournalFeed.tsx:48 +#: src/pages/index/NewFeed.tsx:36 +#: src/pages/Notifications.tsx:323 +#: src/pages/UserPublicProfile.tsx:1081 +#: src/pages/UserPublicProfile.tsx:1118 +#: src/pages/UserPublicProfile.tsx:1160 msgid "Failed to load" msgstr "Chargement échoué" -#: src/components/DumpCreateModal.tsx:313 +#: src/components/DumpCreateModal.tsx:322 msgid "Failed to post" msgstr "Publication échouée" -#: src/components/CommentThread.tsx:462 +#: src/components/CommentThread.tsx:486 msgid "Failed to post comment" msgstr "Impossible de publier le commentaire" -#: src/components/CommentThread.tsx:334 +#: src/components/CommentThread.tsx:349 msgid "Failed to post reply" msgstr "Impossible de publier la réponse" -#: src/pages/PlaylistDetail.tsx:776 -#: src/pages/UserPublicProfile.tsx:546 -#: src/pages/UserPublicProfile.tsx:581 -#: src/pages/UserPublicProfile.tsx:704 -#: src/pages/UserPublicProfile.tsx:776 +#: src/pages/PlaylistDetail.tsx:789 +#: src/pages/UserPublicProfile.tsx:663 +#: src/pages/UserPublicProfile.tsx:701 +#: src/pages/UserPublicProfile.tsx:828 +#: src/pages/UserPublicProfile.tsx:905 msgid "Failed to save" msgstr "Enregistrement échoué" -#: src/components/CommentThread.tsx:249 +#: src/components/CommentThread.tsx:257 msgid "Failed to save edit" msgstr "Impossible d'enregistrer la modification" -#: src/pages/UserPublicProfile.tsx:726 +#: src/pages/UserPublicProfile.tsx:851 msgid "Failed to update avatar" msgstr "Impossible de mettre à jour l'avatar" -#: src/components/DumpCreateModal.tsx:347 +#: src/components/DumpCreateModal.tsx:359 msgid "Fetching preview…" msgstr "Récupération de l'aperçu…" -#: src/components/DumpCreateModal.tsx:417 +#: src/components/DumpCreateModal.tsx:432 msgid "Fetching…" msgstr "Récupération…" -#: src/components/DumpCreateModal.tsx:306 +#: src/components/DumpCreateModal.tsx:315 #: src/components/FileDropZone.tsx:31 msgid "File" msgstr "Fichier" -#: src/components/DumpCreateModal.tsx:200 +#: src/components/DumpCreateModal.tsx:209 msgid "File too large (max 50 MB)." msgstr "Fichier trop volumineux (max 50 Mo)." @@ -415,34 +433,40 @@ msgstr "Suivre {targetUsername}" msgid "Follow playlist" msgstr "Suivre la collection" -#: src/pages/index/FollowedFeed.tsx:358 +#: src/pages/index/FollowedFeed.tsx:371 msgid "Follow some public playlists to see their dumps here." msgstr "Suivez des collections publiques pour voir leurs recos ici." -#: src/pages/index/FollowedFeed.tsx:344 +#: src/pages/index/FollowedFeed.tsx:357 msgid "Follow some users to see their dumps here." msgstr "Suivez des utilisateurs pour voir leurs recos ici." #: src/components/FeedTabBar.tsx:47 +#: src/pages/UserPublicProfile.tsx:964 msgid "Followed" msgstr "Suivi" #. placeholder {0}: followed.items.length #. placeholder {1}: followed.hasMore ? "+" : "" -#: src/pages/UserPlaylists.tsx:416 +#: src/pages/UserPlaylists.tsx:430 msgid "Followed ({0}{1})" msgstr "Suivies ({0}{1})" +#: src/pages/UserPublicProfile.tsx:1109 +msgid "Followed playlists" +msgstr "Collections suivies" + #: src/components/FollowButton.tsx:37 #: src/components/FollowButton.tsx:64 +#: src/pages/UserPublicProfile.tsx:1072 msgid "Following" msgstr "Abonné" -#: src/pages/index/FollowedFeed.tsx:324 +#: src/pages/index/FollowedFeed.tsx:337 msgid "From people" msgstr "De personnes" -#: src/pages/index/FollowedFeed.tsx:331 +#: src/pages/index/FollowedFeed.tsx:344 msgid "From playlists" msgstr "De collections" @@ -450,14 +474,19 @@ msgstr "De collections" msgid "Hot" msgstr "Tendances" -#: src/pages/UserRegister.tsx:104 +#: src/pages/UserRegister.tsx:106 msgid "Invalid invite" msgstr "Invitation invalide" -#: src/pages/UserPublicProfile.tsx:651 +#: src/pages/UserPublicProfile.tsx:773 msgid "invited by" msgstr "invité par" +#: src/pages/UserPublicProfile.tsx:971 +#: src/pages/UserPublicProfile.tsx:1149 +msgid "Invitees" +msgstr "Invités" + #: src/components/FeedTabBar.tsx:39 msgid "Journal" msgstr "Journal" @@ -466,77 +495,84 @@ msgstr "Journal" msgid "just now" msgstr "à l'instant" +#: src/pages/UserPublicProfile.tsx:1227 +msgid "Light" +msgstr "Clair" + #: src/contexts/WSProvider.tsx:359 msgid "Live updates are temporarily disconnected. Trying to reconnect…" msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…" -#: src/components/AppHeader.tsx:83 +#: src/components/AppHeader.tsx:84 msgid "Live updates unavailable." msgstr "Mises à jour en direct indisponibles." -#: src/pages/Notifications.tsx:386 +#: src/pages/Notifications.tsx:396 msgid "Load more" msgstr "Charger plus" -#: src/pages/Dump.tsx:193 -#: src/pages/DumpEdit.tsx:143 +#: src/pages/Dump.tsx:194 +#: src/pages/DumpEdit.tsx:146 msgid "Loading dump…" msgstr "Chargement de la reco…" -#: src/pages/index/FollowedFeed.tsx:103 -#: src/pages/index/HotFeed.tsx:52 -#: src/pages/index/JournalFeed.tsx:65 -#: src/pages/index/NewFeed.tsx:52 -#: src/pages/Search.tsx:239 -#: src/pages/UserDumps.tsx:111 -#: src/pages/UserPlaylists.tsx:409 -#: src/pages/UserPlaylists.tsx:436 -#: src/pages/UserUpvoted.tsx:183 +#: src/pages/index/FollowedFeed.tsx:109 +#: src/pages/index/HotFeed.tsx:64 +#: src/pages/index/JournalFeed.tsx:77 +#: src/pages/index/NewFeed.tsx:64 +#: src/pages/Search.tsx:244 +#: src/pages/UserDumps.tsx:93 +#: src/pages/UserPlaylists.tsx:417 +#: src/pages/UserPlaylists.tsx:452 +#: src/pages/UserUpvoted.tsx:177 msgid "Loading more…" msgstr "Chargement…" -#: src/pages/PlaylistDetail.tsx:590 +#: src/pages/PlaylistDetail.tsx:595 msgid "Loading playlist…" msgstr "Chargement de la collection…" -#: src/pages/UserPublicProfile.tsx:590 +#: src/pages/UserPublicProfile.tsx:711 msgid "Loading profile…" msgstr "Chargement du profil…" -#: src/components/PlaylistMembershipPanel.tsx:26 -#: src/components/TextEditor.tsx:273 -#: src/pages/index/FollowedFeed.tsx:74 -#: src/pages/index/HotFeed.tsx:29 -#: src/pages/index/JournalFeed.tsx:41 -#: src/pages/index/NewFeed.tsx:29 -#: src/pages/Notifications.tsx:318 -#: src/pages/Notifications.tsx:386 -#: src/pages/UserDumps.tsx:50 -#: src/pages/UserPlaylists.tsx:341 -#: src/pages/UserUpvoted.tsx:122 +#: src/components/PlaylistMembershipPanel.tsx:28 +#: src/components/TextEditor.tsx:289 +#: src/pages/index/FollowedFeed.tsx:76 +#: src/pages/index/HotFeed.tsx:32 +#: src/pages/index/JournalFeed.tsx:44 +#: src/pages/index/NewFeed.tsx:32 +#: src/pages/Notifications.tsx:319 +#: src/pages/Notifications.tsx:395 +#: src/pages/UserDumps.tsx:51 +#: src/pages/UserPlaylists.tsx:342 +#: src/pages/UserPublicProfile.tsx:1077 +#: src/pages/UserPublicProfile.tsx:1114 +#: src/pages/UserPublicProfile.tsx:1154 +#: src/pages/UserUpvoted.tsx:123 msgid "Loading…" msgstr "Chargement…" #: src/components/AppHeader.tsx:74 -#: src/pages/UserLogin.tsx:62 -#: src/pages/UserLogin.tsx:91 +#: src/pages/UserLogin.tsx:63 +#: src/pages/UserLogin.tsx:93 msgid "Log in" msgstr "Se connecter" -#: src/pages/UserPublicProfile.tsx:610 -#: src/pages/UserPublicProfile.tsx:738 +#: src/pages/UserPublicProfile.tsx:732 +#: src/pages/UserPublicProfile.tsx:865 msgid "Log out" msgstr "Se déconnecter" -#: src/pages/UserLogin.tsx:90 +#: src/pages/UserLogin.tsx:92 msgid "Logging in…" msgstr "Connexion…" -#: src/pages/UserLogin.tsx:65 +#: src/pages/UserLogin.tsx:67 msgid "Login failed" msgstr "Connexion échouée" -#: src/components/FileDropZone.tsx:141 +#: src/components/FileDropZone.tsx:145 msgid "Max 50 MB" msgstr "Max 50 Mo" @@ -548,7 +584,7 @@ msgstr "nouveau" msgid "New" msgstr "Nouveau" -#: src/components/DumpCreateModal.tsx:275 +#: src/components/DumpCreateModal.tsx:284 msgid "New dump" msgstr "Nouvelle reco" @@ -556,47 +592,56 @@ msgstr "Nouvelle reco" msgid "New playlist" msgstr "Nouvelle collection" -#: src/pages/PlaylistDetail.tsx:783 +#: src/pages/PlaylistDetail.tsx:798 msgid "No dumps in this playlist yet." msgstr "Aucune reco dans cette collection pour l'instant." -#: src/pages/Search.tsx:220 +#: src/pages/Search.tsx:224 msgid "No dumps match \"{q}\"." msgstr "Aucune reco ne correspond à « {q} »." -#: src/pages/index/HotFeed.tsx:32 -#: src/pages/index/JournalFeed.tsx:44 -#: src/pages/index/NewFeed.tsx:32 +#: src/pages/index/HotFeed.tsx:40 +#: src/pages/index/JournalFeed.tsx:52 +#: src/pages/index/NewFeed.tsx:40 msgid "No dumps yet. Be the first!" msgstr "Pas encore de recos. Soyez le premier !" -#: src/components/TextEditor.tsx:274 +#: src/components/TextEditor.tsx:292 msgid "No emoji found." msgstr "Aucun emoji trouvé." -#: src/pages/UserPlaylists.tsx:424 +#: src/pages/UserPlaylists.tsx:439 +#: src/pages/UserPublicProfile.tsx:1122 msgid "No followed playlists yet." msgstr "Pas encore de collections suivies." -#: src/pages/Search.tsx:273 +#: src/pages/UserPublicProfile.tsx:1167 +msgid "No invitees yet." +msgstr "Aucun invité pour le moment." + +#: src/pages/Search.tsx:283 msgid "No playlists match \"{q}\"." msgstr "Aucune collection ne correspond à « {q} »." -#: src/components/PlaylistMembershipPanel.tsx:28 -#: src/pages/UserPlaylists.tsx:392 -#: src/pages/UserPublicProfile.tsx:865 +#: src/components/PlaylistMembershipPanel.tsx:34 +#: src/pages/UserPlaylists.tsx:397 +#: src/pages/UserPublicProfile.tsx:1043 msgid "No playlists yet." msgstr "Pas encore de collections." -#: src/pages/Search.tsx:249 +#: src/pages/Search.tsx:257 msgid "No users match \"{q}\"." msgstr "Aucun utilisateur ne correspond à « {q} »." -#: src/pages/Notifications.tsx:327 -#: src/pages/UserDumps.tsx:92 -#: src/pages/UserPublicProfile.tsx:930 -#: src/pages/UserPublicProfile.tsx:1047 -#: src/pages/UserUpvoted.tsx:154 +#: src/pages/UserPublicProfile.tsx:1085 +msgid "Not following anyone yet." +msgstr "Aucun abonnement pour le moment." + +#: src/pages/Notifications.tsx:330 +#: src/pages/UserDumps.tsx:123 +#: src/pages/UserPublicProfile.tsx:1292 +#: src/pages/UserPublicProfile.tsx:1415 +#: src/pages/UserUpvoted.tsx:195 msgid "Nothing here yet." msgstr "Rien ici pour l'instant." @@ -617,90 +662,91 @@ msgstr "Ouvrir la recherche" msgid "or <0>browse files" msgstr "ou <0>parcourir les fichiers" -#: src/pages/UserLogin.tsx:80 +#: src/pages/UserLogin.tsx:82 msgid "Password" msgstr "Mot de passe" #. placeholder {0}: VALIDATION.PASSWORD_MIN -#: src/pages/UserRegister.tsx:142 +#: src/pages/UserRegister.tsx:147 msgid "Password (min. {0} characters)" msgstr "Mot de passe (min. {0} caractères)" #: src/components/AppHeader.tsx:50 #: src/components/UserMenu.tsx:62 #: src/pages/Search.tsx:175 -#: src/pages/UserPlaylists.tsx:366 +#: src/pages/UserPlaylists.tsx:368 +#: src/pages/UserPublicProfile.tsx:957 msgid "Playlists" msgstr "Collections" #. placeholder {0}: playlists.items.length #. placeholder {1}: playlists.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:845 +#: src/pages/UserPublicProfile.tsx:1016 msgid "Playlists ({0}{1})" msgstr "Collections ({0}{1})" -#: src/components/DumpCreateModal.tsx:193 +#: src/components/DumpCreateModal.tsx:202 msgid "Please select a file." msgstr "Veuillez sélectionner un fichier." -#: src/components/CommentThread.tsx:472 +#: src/components/CommentThread.tsx:499 msgid "Post comment" msgstr "Publier le commentaire" -#: src/components/CommentThread.tsx:342 +#: src/components/CommentThread.tsx:362 msgid "Post reply" msgstr "Publier la réponse" -#: src/components/CommentThread.tsx:342 -#: src/components/CommentThread.tsx:472 +#: src/components/CommentThread.tsx:361 +#: src/components/CommentThread.tsx:498 msgid "Posting…" msgstr "Publication…" #: src/components/DumpCard.tsx:91 -#: src/components/PlaylistCard.tsx:71 -#: src/components/PlaylistMembershipPanel.tsx:47 -#: src/pages/Dump.tsx:282 -#: src/pages/PlaylistDetail.tsx:748 +#: src/components/PlaylistCard.tsx:73 +#: src/components/PlaylistMembershipPanel.tsx:55 +#: src/pages/Dump.tsx:284 +#: src/pages/PlaylistDetail.tsx:759 msgid "private" msgstr "privé" -#: src/components/DumpCreateModal.tsx:397 -#: src/components/PlaylistCreateForm.tsx:94 -#: src/pages/DumpEdit.tsx:274 -#: src/pages/PlaylistDetail.tsx:737 +#: src/components/DumpCreateModal.tsx:411 +#: src/components/PlaylistCreateForm.tsx:99 +#: src/pages/DumpEdit.tsx:285 +#: src/pages/PlaylistDetail.tsx:746 msgid "Private" msgstr "Privé" -#: src/components/PlaylistCard.tsx:71 -#: src/pages/PlaylistDetail.tsx:748 +#: src/components/PlaylistCard.tsx:72 +#: src/pages/PlaylistDetail.tsx:758 msgid "public" msgstr "public" -#: src/components/DumpCreateModal.tsx:389 -#: src/components/PlaylistCreateForm.tsx:87 -#: src/pages/DumpEdit.tsx:267 -#: src/pages/PlaylistDetail.tsx:730 +#: src/components/DumpCreateModal.tsx:403 +#: src/components/PlaylistCreateForm.tsx:92 +#: src/pages/DumpEdit.tsx:278 +#: src/pages/PlaylistDetail.tsx:739 msgid "Public" msgstr "Public" -#: src/pages/DumpEdit.tsx:206 +#: src/pages/DumpEdit.tsx:214 msgid "Refresh metadata" msgstr "Actualiser les métadonnées" -#: src/pages/DumpEdit.tsx:206 +#: src/pages/DumpEdit.tsx:213 msgid "Refreshing…" msgstr "Actualisation…" -#: src/pages/UserRegister.tsx:115 -#: src/pages/UserRegister.tsx:155 +#: src/pages/UserRegister.tsx:118 +#: src/pages/UserRegister.tsx:160 msgid "Register" msgstr "S'inscrire" -#: src/pages/UserRegister.tsx:154 +#: src/pages/UserRegister.tsx:159 msgid "Registering…" msgstr "Inscription…" -#: src/pages/UserRegister.tsx:118 +#: src/pages/UserRegister.tsx:122 msgid "Registration failed" msgstr "Inscription échouée" @@ -708,35 +754,35 @@ msgstr "Inscription échouée" msgid "Remove file" msgstr "Supprimer le fichier" -#: src/pages/PlaylistDetail.tsx:838 +#: src/pages/PlaylistDetail.tsx:853 msgid "Remove from playlist" msgstr "Retirer de la collection" -#: src/pages/DumpEdit.tsx:241 +#: src/pages/DumpEdit.tsx:251 msgid "Replace file" msgstr "Remplacer le fichier" -#: src/components/CommentThread.tsx:284 +#: src/components/CommentThread.tsx:297 msgid "Reply" msgstr "Répondre" -#: src/pages/Dump.tsx:209 -#: src/pages/DumpEdit.tsx:159 +#: src/pages/Dump.tsx:211 +#: src/pages/DumpEdit.tsx:163 msgid "Retry" msgstr "Réessayer" -#: src/components/CommentThread.tsx:257 -#: src/pages/DumpEdit.tsx:291 -#: src/pages/PlaylistDetail.tsx:665 -#: src/pages/UserPublicProfile.tsx:692 -#: src/pages/UserPublicProfile.tsx:765 +#: src/components/CommentThread.tsx:270 +#: src/pages/DumpEdit.tsx:306 +#: src/pages/PlaylistDetail.tsx:673 +#: src/pages/UserPublicProfile.tsx:816 +#: src/pages/UserPublicProfile.tsx:894 msgid "Save" msgstr "Enregistrer" -#: src/components/CommentThread.tsx:257 -#: src/pages/PlaylistDetail.tsx:665 -#: src/pages/UserPublicProfile.tsx:692 -#: src/pages/UserPublicProfile.tsx:765 +#: src/components/CommentThread.tsx:269 +#: src/pages/PlaylistDetail.tsx:673 +#: src/pages/UserPublicProfile.tsx:815 +#: src/pages/UserPublicProfile.tsx:894 msgid "Saving…" msgstr "Enregistrement…" @@ -748,11 +794,11 @@ msgstr "Rechercher" msgid "Search dumps, users, playlists…" msgstr "Rechercher des recos, utilisateurs, collections…" -#: src/pages/Search.tsx:214 +#: src/pages/Search.tsx:218 msgid "Search failed" msgstr "Recherche échouée" -#: src/pages/Search.tsx:210 +#: src/pages/Search.tsx:213 msgid "Searching…" msgstr "Recherche…" @@ -760,40 +806,48 @@ msgstr "Recherche…" msgid "Server unreachable" msgstr "Serveur inaccessible" +#: src/pages/UserPublicProfile.tsx:979 +msgid "Settings" +msgstr "Paramètres" + #: src/components/PageError.tsx:13 msgid "Something went wrong" msgstr "Une erreur est survenue" +#: src/pages/UserPublicProfile.tsx:1191 +msgid "Style" +msgstr "Style" + #: src/components/SearchBar.tsx:71 msgid "Submit search" msgstr "Lancer la recherche" -#: src/pages/UserPublicProfile.tsx:755 +#: src/pages/UserPublicProfile.tsx:882 msgid "Tell people about yourself…" msgstr "Parlez de vous…" -#: src/components/DumpCreateModal.tsx:377 -#: src/pages/DumpEdit.tsx:256 +#: src/components/DumpCreateModal.tsx:390 +#: src/pages/DumpEdit.tsx:266 msgid "Tell the community what makes this worth their time..." msgstr "Dites à la communauté pourquoi ça vaut le coup…" -#: src/pages/UserRegister.tsx:105 +#: src/pages/UserRegister.tsx:107 msgid "This invite link is missing, expired, or already used." msgstr "Ce lien d'invitation est manquant, expiré ou déjà utilisé." -#: src/pages/UserLogin.tsx:96 +#: src/pages/UserLogin.tsx:98 msgid "This is a mirage." msgstr "C'est un mirage." -#: src/components/PlaylistCreateForm.tsx:69 +#: src/components/PlaylistCreateForm.tsx:72 msgid "Title" msgstr "Titre" -#: src/pages/Notifications.tsx:341 +#: src/pages/Notifications.tsx:346 msgid "Today" msgstr "Aujourd'hui" -#: src/pages/PlaylistDetail.tsx:850 +#: src/pages/PlaylistDetail.tsx:865 msgid "Undo" msgstr "Annuler" @@ -805,30 +859,30 @@ msgstr "Ne plus suivre {targetUsername}" msgid "Unfollow playlist" msgstr "Ne plus suivre la collection" -#: src/pages/UserPublicProfile.tsx:515 +#: src/pages/UserPublicProfile.tsx:632 msgid "Upload failed" msgstr "Envoi échoué" -#: src/components/DumpCreateModal.tsx:418 +#: src/components/DumpCreateModal.tsx:433 msgid "Uploading…" msgstr "Envoi…" -#: src/pages/UserUpvoted.tsx:150 +#: src/pages/UserUpvoted.tsx:191 msgid "Upvoted" msgstr "Voté" #. placeholder {0}: votes.items.length #. placeholder {1}: votes.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:829 +#: src/pages/UserPublicProfile.tsx:998 msgid "Upvoted ({0}{1})" msgstr "Votés ({0}{1})" -#: src/components/DumpCreateModal.tsx:322 -#: src/pages/DumpEdit.tsx:221 +#: src/components/DumpCreateModal.tsx:332 +#: src/pages/DumpEdit.tsx:230 msgid "URL" msgstr "URL" -#: src/components/DumpCreateModal.tsx:176 +#: src/components/DumpCreateModal.tsx:185 msgid "URL is required." msgstr "L'URL est obligatoire." @@ -836,8 +890,8 @@ msgstr "L'URL est obligatoire." msgid "User menu" msgstr "Menu utilisateur" -#: src/pages/UserLogin.tsx:72 -#: src/pages/UserRegister.tsx:125 +#: src/pages/UserLogin.tsx:74 +#: src/pages/UserRegister.tsx:129 msgid "Username" msgstr "Nom d'utilisateur" @@ -845,36 +899,43 @@ msgstr "Nom d'utilisateur" msgid "Users" msgstr "Utilisateurs" -#: src/pages/UserPublicProfile.tsx:878 -#: src/pages/UserPublicProfile.tsx:948 -#: src/pages/UserPublicProfile.tsx:1074 +#: src/pages/UserPublicProfile.tsx:1062 +#: src/pages/UserPublicProfile.tsx:1100 +#: src/pages/UserPublicProfile.tsx:1137 +#: src/pages/UserPublicProfile.tsx:1313 +#: src/pages/UserPublicProfile.tsx:1445 msgid "View all →" msgstr "Tout voir →" -#: src/components/DumpCreateModal.tsx:432 +#: src/components/DumpCreateModal.tsx:447 msgid "View dump →" msgstr "Voir la reco →" -#: src/components/DumpCreateModal.tsx:370 -#: src/pages/DumpEdit.tsx:250 +#: src/components/DumpCreateModal.tsx:383 +#: src/pages/DumpEdit.tsx:260 msgid "Why are you dumping this?" msgstr "Pourquoi recommandez-vous ça ?" -#: src/components/CommentThread.tsx:329 +#: src/components/CommentThread.tsx:342 msgid "Write a reply…" msgstr "Écrire une réponse…" -#: src/pages/Notifications.tsx:341 +#: src/pages/Notifications.tsx:348 msgid "Yesterday" msgstr "Hier" -#: src/pages/Notifications.tsx:329 +#: src/pages/Notifications.tsx:333 msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content." msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu." -#: src/pages/index/HotFeed.tsx:54 -#: src/pages/index/JournalFeed.tsx:67 -#: src/pages/index/NewFeed.tsx:54 -#: src/pages/Search.tsx:242 +#: src/pages/index/FollowedFeed.tsx:114 +#: src/pages/index/HotFeed.tsx:69 +#: src/pages/index/JournalFeed.tsx:82 +#: src/pages/index/NewFeed.tsx:69 +#: src/pages/Search.tsx:249 +#: src/pages/UserDumps.tsx:98 +#: src/pages/UserPlaylists.tsx:422 +#: src/pages/UserPlaylists.tsx:457 +#: src/pages/UserUpvoted.tsx:182 msgid "You've reached the end." msgstr "Vous avez tout lu, tout vu, tout bu." diff --git a/src/main.tsx b/src/main.tsx index 342a5fa..6befde4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,8 @@ import App from "./App.tsx"; import { i18n, loadCatalog } from "./i18n.ts"; import "./index.css"; +import "./themes/smooth.css"; +import "./themes/brutalist.css"; await loadCatalog(); diff --git a/src/model.ts b/src/model.ts index 31b6cf1..b8d2d44 100644 --- a/src/model.ts +++ b/src/model.ts @@ -114,6 +114,24 @@ export function deserializePublicUser(raw: RawPublicUser): PublicUser { // Alias so call sites using deserializeUser continue to work. export const deserializeUser = deserializePublicUser; +export interface InviteTreeEntry { + id: string; + username: string; + avatarMime?: string; + invitedById: string; + createdAt: Date; +} + +export type RawInviteTreeEntry = Omit & { + createdAt: string; +}; + +export function deserializeInviteTreeEntry( + raw: RawInviteTreeEntry, +): InviteTreeEntry { + return { ...raw, createdAt: new Date(raw.createdAt) }; +} + /** * Authentication */ diff --git a/src/pages/UserDumps.tsx b/src/pages/UserDumps.tsx index 39d34e9..388c07f 100644 --- a/src/pages/UserDumps.tsx +++ b/src/pages/UserDumps.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { t } from "@lingui/core/macro"; -import { Plural, Trans } from "@lingui/react/macro"; +import { Trans } from "@lingui/react/macro"; import { Link, useParams } from "react-router"; import { useAuth } from "../hooks/useAuth.ts"; @@ -70,33 +70,9 @@ export function UserDumps() { const { profileUser, items: dumps, hasMore, loadingMore } = state; return ( - - setCreateModalOpen(true)} - > - + New dump - - )} - /> - - {createModalOpen && ( - setCreateModalOpen(false)} /> - )} - - {dumps.length === 0 - ? ( -

- Nothing here yet. -

- ) - : ( + 0 && ( + <>
    {dumps.map((dump) => ( ))}
- )} - -
- {loadingMore && ( -

- Loading more… -

+ {hasMore &&
} + {loadingMore && ( +

+ Loading more… +

+ )} + {!hasMore && ( +

+ You've reached the end. +

+ )} + )} - {!hasMore && dumps.length > 0 && ( -

- - All - {" "} - loaded. - + > + setCreateModalOpen(true)} + > + + New dump + + )} + /> + {createModalOpen && ( + setCreateModalOpen(false)} /> + )} + {dumps.length === 0 && ( +

+ Nothing here yet.

)} diff --git a/src/pages/UserPlaylists.tsx b/src/pages/UserPlaylists.tsx index eb29a57..59d38b0 100644 --- a/src/pages/UserPlaylists.tsx +++ b/src/pages/UserPlaylists.tsx @@ -411,12 +411,17 @@ export function UserPlaylists() { ))} )} -
+ {created.hasMore &&
} {created.loadingMore && (

Loading more…

)} + {!created.hasMore && created.items.length > 0 && ( +

+ You've reached the end. +

+ )}
@@ -441,12 +446,17 @@ export function UserPlaylists() { ))} )} -
+ {followed.hasMore &&
} {followed.loadingMore && (

Loading more…

)} + {!followed.hasMore && followed.items.length > 0 && ( +

+ You've reached the end. +

+ )}
{confirmDeleteId && ( diff --git a/src/pages/UserPublicProfile.tsx b/src/pages/UserPublicProfile.tsx index 6f259c8..b4a4878 100644 --- a/src/pages/UserPublicProfile.tsx +++ b/src/pages/UserPublicProfile.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useLayoutEffect, + useMemo, useRef, useState, } from "react"; @@ -10,15 +11,22 @@ import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL, DEFAULT_PAGE_SIZE, VALIDATION } from "../config/api.ts"; -import type { Dump, PaginatedData, PublicUser } from "../model.ts"; +import type { + Dump, + InviteTreeEntry, + PaginatedData, + PublicUser, +} from "../model.ts"; import { deserializeAuthResponse, deserializeDump, + deserializeInviteTreeEntry, deserializePublicUser, hydrateDump, hydratePlaylist, parseAPIResponse, type RawDump, + type RawInviteTreeEntry, type RawPublicUser, type UpdateUserRequest, } from "../model.ts"; @@ -29,6 +37,7 @@ import { NewPlaylistForm } from "../components/NewPlaylistForm.tsx"; import { PageShell } from "../components/PageShell.tsx"; import { PageError } from "../components/PageError.tsx"; import { useAuth } from "../hooks/useAuth.ts"; +import { useTheme } from "../hooks/useTheme.ts"; import { useWS } from "../hooks/useWS.ts"; import { useDumpListSync } from "../hooks/useDumpListSync.ts"; import { useFading } from "../hooks/useFading.ts"; @@ -118,10 +127,38 @@ type ProfileState = playlists: PaginatedList; }; +type FollowedState = + | null + | { status: "loading" } + | { status: "error"; error: string } + | { + status: "loaded"; + users: PaginatedList; + playlists: PaginatedList; + }; + +type InviteTreeState = + | null + | { status: "loading" } + | { status: "error"; error: string } + | { status: "loaded"; entries: InviteTreeEntry[] }; + +type InviteTreeNode = InviteTreeEntry & { children: InviteTreeNode[] }; + +function buildInviteTree( + entries: InviteTreeEntry[], + parentId: string, +): InviteTreeNode[] { + return entries + .filter((e) => e.invitedById === parentId) + .map((e) => ({ ...e, children: buildInviteTree(entries, e.id) })); +} + export function UserPublicProfile() { const { username } = useParams(); const navigate = useNavigate(); const { user: me, authFetch, login, logout, token } = useAuth(); + const { style, colorScheme, setStyle, setColorScheme } = useTheme(); const { voteCounts, myVotes, @@ -244,11 +281,25 @@ export function UserPublicProfile() { const [emailError, setEmailError] = useState(null); const prevMyVotesRef = useRef | null>(null); + const [tab, setTab] = useState< + "dumps" | "playlists" | "followed" | "invitees" | "settings" + >("dumps"); + const [followedState, setFollowedState] = useState(null); + const [inviteTreeState, setInviteTreeState] = useState(null); + + const inviteTreeNodes = useMemo(() => { + if (!profileUserId || inviteTreeState?.status !== "loaded") return []; + return buildInviteTree(inviteTreeState.entries, profileUserId); + }, [inviteTreeState, profileUserId]); + const [prevUsername, setPrevUsername] = useState(username); if (prevUsername !== username) { setPrevUsername(username); setState({ status: "loading" }); prevMyVotesRef.current = null; + setTab("dumps"); + setFollowedState(null); + setInviteTreeState(null); } useEffect(() => { @@ -468,6 +519,85 @@ export function UserPublicProfile() { ); }, [playlistFeed, savePlaylists]); + // Lazy-load followed users + playlists when the Followed tab is first opened + useEffect(() => { + if ( + tab !== "followed" || followedState !== null || state.status !== "loaded" + ) { + return; + } + setFollowedState({ status: "loading" }); + const controller = new AbortController(); + Promise.all([ + fetch( + `${API_URL}/api/users/${username}/followed-users?page=1&limit=${DEFAULT_PAGE_SIZE}`, + { signal: controller.signal }, + ), + fetch( + `${API_URL}/api/users/${username}/followed-playlists?page=1&limit=${DEFAULT_PAGE_SIZE}`, + { signal: controller.signal }, + ), + ]) + .then(([ur, pr]) => Promise.all([ur.json(), pr.json()])) + .then(([ub, pb]) => { + const usersData = ub.success + ? ub.data as PaginatedData + : { items: [], hasMore: false }; + const playlistsData = pb.success + ? pb.data as PaginatedData + : { items: [], hasMore: false }; + setFollowedState({ + status: "loaded", + users: initialList( + usersData.items.map(deserializePublicUser), + usersData.hasMore, + ), + playlists: initialList( + playlistsData.items.map(deserializePlaylist), + playlistsData.hasMore, + ), + }); + }) + .catch((err) => { + if (err.name === "AbortError") return; + setFollowedState({ status: "error", error: friendlyFetchError(err) }); + }); + return () => controller.abort(); + // followedState intentionally omitted: it's a read-once guard, not a trigger. + // Including it would abort the in-flight fetch when state changes to "loading". + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tab, state.status, username]); + + // Lazy-load invite tree when the Invitees tab is first opened + useEffect(() => { + if ( + tab !== "invitees" || inviteTreeState !== null || + state.status !== "loaded" + ) { + return; + } + setInviteTreeState({ status: "loading" }); + const controller = new AbortController(); + fetch( + `${API_URL}/api/users/${username}/invitees`, + { signal: controller.signal }, + ) + .then((r) => r.json()) + .then((body) => { + const entries: InviteTreeEntry[] = body.success + ? (body.data as RawInviteTreeEntry[]).map(deserializeInviteTreeEntry) + : []; + setInviteTreeState({ status: "loaded", entries }); + }) + .catch((err) => { + if (err.name === "AbortError") return; + setInviteTreeState({ status: "error", error: friendlyFetchError(err) }); + }); + return () => controller.abort(); + // inviteTreeState intentionally omitted — same read-once guard pattern. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tab, state.status, username]); + // Restore scroll position after cache restoration const scrollRestored = useRef(false); useLayoutEffect(() => { @@ -826,80 +956,312 @@ export function UserPublicProfile() {
)} -
- - - +
+ + + + + {isOwnProfile && ( + + )}
-
-
-

- - Playlists ({playlists.items.length} - {playlists.hasMore ? "+" : ""}) - -

- {isOwnProfile && ( - - setState((s) => { - if (s.status !== "loaded") return s; - if (s.playlists.items.some((pl) => pl.id === p.id)) return s; - return { - ...s, - playlists: { - ...s.playlists, - items: [p, ...s.playlists.items], - }, - }; - })} - /> - )} + {tab === "dumps" && ( +
+ +
- {playlists.items.length === 0 - ? ( -

- No playlists yet. -

- ) - : ( -
    - {playlists.items.map((p) => ( - - ))} -
+ )} + + {tab === "playlists" && ( +
+
+

+ + Playlists ({playlists.items.length} + {playlists.hasMore ? "+" : ""}) + +

+ {isOwnProfile && ( + + setState((s) => { + if (s.status !== "loaded") return s; + if ( + s.playlists.items.some((pl) => + pl.id === p.id + ) + ) { + return s; + } + return { + ...s, + playlists: { + ...s.playlists, + items: [p, ...s.playlists.items], + }, + }; + })} + /> + )} +
+ {playlists.items.length === 0 + ? ( +

+ No playlists yet. +

+ ) + : ( +
    + {playlists.items.map((p) => ( + + ))} +
+ )} + {playlists.items.length > 0 && ( + + View all → + )} - {playlists.items.length > 0 && ( - - View all → - - )} -
+
+ )} + + {tab === "followed" && ( +
+
+

+ Following +

+ {followedState === null || followedState.status === "loading" + ? ( +

+ Loading… +

+ ) + : followedState.status === "error" + ? ( + + ) + : followedState.users.items.length === 0 + ? ( +

+ Not following anyone yet. +

+ ) + : ( + <> +
    + {followedState.users.items.map((u) => ( + + ))} +
+ {followedState.users.hasMore && ( + + View all → + + )} + + )} +
+ +
+

+ Followed playlists +

+ {followedState === null || followedState.status === "loading" + ? ( +

+ Loading… +

+ ) + : followedState.status === "error" + ? ( + + ) + : followedState.playlists.items.length === 0 + ? ( +

+ No followed playlists yet. +

+ ) + : ( + <> +
    + {followedState.playlists.items.map((p) => ( + + ))} +
+ {followedState.playlists.hasMore && ( + + View all → + + )} + + )} +
+
+ )} + + {tab === "invitees" && ( +
+

+ Invitees +

+ {inviteTreeState === null || inviteTreeState.status === "loading" + ? ( +

+ Loading… +

+ ) + : inviteTreeState.status === "error" + ? ( + + ) + : inviteTreeState.entries.length === 0 + ? ( +

+ No invitees yet. +

+ ) + : } +
+ )} + + {tab === "settings" && isOwnProfile && ( + <> +
+

+ Appearance +

+
+
+ + Style + +
+ + +
+
+
+ + Color scheme + +
+ + + +
+
+
+
+ + )} ); } @@ -1109,3 +1471,52 @@ function UpvotedDumpList( ); } + +// ── Invite tree ────────────────────────────────────────────────────────────── + +function InviteTreeList({ nodes }: { nodes: InviteTreeNode[] }) { + return ( +
    + {nodes.map((node) => ( +
  • + + + @{node.username} + + {node.children.length > 0 && } +
  • + ))} +
+ ); +} + +// ── Followed user card ─────────────────────────────────────────────────────── + +function FollowedUserCard({ user }: { user: PublicUser }) { + return ( +
  • + + + @{user.username} + + +
  • + ); +} diff --git a/src/pages/UserUpvoted.tsx b/src/pages/UserUpvoted.tsx index d060d97..8de17d0 100644 --- a/src/pages/UserUpvoted.tsx +++ b/src/pages/UserUpvoted.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router"; import { t } from "@lingui/core/macro"; -import { Plural, Trans } from "@lingui/react/macro"; +import { Trans } from "@lingui/react/macro"; import { API_URL } from "../config/api.ts"; import type { Dump } from "../model.ts"; @@ -145,20 +145,9 @@ export function UserUpvoted() { ); return ( - - - - {visibleDumps.length === 0 - ? ( -

    - Nothing here yet. -

    - ) - : ( + 0 && ( + <>
      {visibleDumps.map((dump) => { const phase = fading[dump.id]; @@ -182,25 +171,28 @@ export function UserUpvoted() { ); })}
    - )} - -
    - {loadingMore && ( -

    - Loading more… -

    + {hasMore &&
    } + {loadingMore && ( +

    + Loading more… +

    + )} + {!hasMore && ( +

    + You've reached the end. +

    + )} + )} - {!hasMore && visibleDumps.length > 0 && ( -

    - - All{" "} - {" "} - loaded. - + > + + {visibleDumps.length === 0 && ( +

    + Nothing here yet.

    )} diff --git a/src/pages/index/FollowedFeed.tsx b/src/pages/index/FollowedFeed.tsx index ad243e5..d4ca43c 100644 --- a/src/pages/index/FollowedFeed.tsx +++ b/src/pages/index/FollowedFeed.tsx @@ -103,12 +103,17 @@ function FollowedSubFeed({ /> ))} -
    + {state.hasMore &&
    } {state.loadingMore && (

    Loading more…

    )} + {!state.hasMore && state.dumps.length > 0 && ( +

    + You've reached the end. +

    + )} ); } diff --git a/src/themes/brutalist.css b/src/themes/brutalist.css new file mode 100644 index 0000000..f07d139 --- /dev/null +++ b/src/themes/brutalist.css @@ -0,0 +1,479 @@ +/* ══════════════════════════════════════════════════════════════════════ + Brutalist theme — pastel palette, hard edges, bold type + Palette: Eggshell · Burnt Peach · Twilight Indigo · Muted Teal · Apricot Cream + ══════════════════════════════════════════════════════════════════════ */ + +/* ── Dark variant (default for this theme) ────────────────────────── */ +:root[data-style="brutalist"] { + --color-text: #f4f1de; /* Eggshell */ + --color-text-secondary: #c8c5b2; + --color-text-muted: #8e8c80; + --color-on-accent: #3d405b; /* Twilight Indigo on Apricot Cream */ + + --color-bg: #252838; /* darkened Twilight Indigo */ + --color-surface: #2e3145; + + --color-border: #4a4e6a; + --color-border-subtle: rgba(74, 78, 106, 0.4); + + --color-accent: #f2cc8f; /* Apricot Cream */ + --color-accent-hover: #d4a860; + + --color-link: #f2cc8f; + --color-link-hover: #d4a860; + + --color-danger: #e07a5f; /* Burnt Peach */ + --color-danger-bg: #3a1e18; + --color-danger-hover: #c85a40; + + --color-success: #81b29a; /* Muted Teal */ + --color-success-bg: #1a2e26; + --color-success-hover: #5a9878; + + --color-overlay: rgba(0, 0, 0, 0.6); + --color-header-user-bg: rgba(242, 204, 143, 0.07); + --color-header-user-bg-hover: rgba(242, 204, 143, 0.14); + + --color-option-bg: #222436; + --color-option-border: #252838; +} + +/* ── Light via system preference (auto color-scheme) ──────────────── */ +@media (prefers-color-scheme: light) { + :root[data-style="brutalist"]:not([data-color-scheme]) { + --color-text: #3d405b; /* Twilight Indigo */ + --color-text-secondary: #5a5e80; + --color-text-muted: #7a7e9a; + --color-on-accent: #f4f1de; /* Eggshell on Burnt Peach */ + + --color-bg: #f4f1de; /* Eggshell */ + --color-surface: #f8f5e8; + + --color-border: #8a8da8; + --color-border-subtle: rgba(61, 64, 91, 0.25); + + --color-accent: #e07a5f; /* Burnt Peach — visible against Eggshell */ + --color-accent-hover: #c05040; + + --color-link: #b85a3a; /* darkened Burnt Peach for legibility */ + --color-link-hover: #943020; + + --color-danger: #c04030; + --color-danger-bg: #fae4e0; + --color-danger-hover: #a02820; + + --color-success: #3a7860; + --color-success-bg: #e0f0ea; + --color-success-hover: #286048; + + --color-overlay: rgba(0, 0, 0, 0.5); + --color-header-user-bg: rgba(224, 122, 95, 0.08); + --color-header-user-bg-hover: rgba(224, 122, 95, 0.16); + + --color-option-bg: #ede9d4; + --color-option-border: #e0dcc8; + } +} + +/* ── Manual dark override ─────────────────────────────────────────── */ +:root[data-style="brutalist"][data-color-scheme="dark"] { + color-scheme: dark; + --color-text: #f4f1de; + --color-text-secondary: #c8c5b2; + --color-text-muted: #8e8c80; + --color-on-accent: #3d405b; + --color-bg: #252838; + --color-surface: #2e3145; + --color-border: #4a4e6a; + --color-border-subtle: rgba(74, 78, 106, 0.4); + --color-accent: #f2cc8f; + --color-accent-hover: #d4a860; + --color-link: #f2cc8f; + --color-link-hover: #d4a860; + --color-danger: #e07a5f; + --color-danger-bg: #3a1e18; + --color-danger-hover: #c85a40; + --color-success: #81b29a; + --color-success-bg: #1a2e26; + --color-success-hover: #5a9878; + --color-overlay: rgba(0, 0, 0, 0.6); + --color-header-user-bg: rgba(242, 204, 143, 0.07); + --color-header-user-bg-hover: rgba(242, 204, 143, 0.14); + --color-option-bg: #222436; + --color-option-border: #252838; +} + +/* ── Manual light override ────────────────────────────────────────── */ +:root[data-style="brutalist"][data-color-scheme="light"] { + color-scheme: light; + --color-text: #3d405b; + --color-text-secondary: #5a5e80; + --color-text-muted: #7a7e9a; + --color-on-accent: #f4f1de; + --color-bg: #f4f1de; + --color-surface: #f8f5e8; + --color-border: #8a8da8; + --color-border-subtle: rgba(61, 64, 91, 0.25); + --color-accent: #e07a5f; + --color-accent-hover: #c05040; + --color-link: #b85a3a; + --color-link-hover: #943020; + --color-danger: #c04030; + --color-danger-bg: #fae4e0; + --color-danger-hover: #a02820; + --color-success: #3a7860; + --color-success-bg: #e0f0ea; + --color-success-hover: #286048; + --color-overlay: rgba(0, 0, 0, 0.5); + --color-header-user-bg: rgba(224, 122, 95, 0.08); + --color-header-user-bg-hover: rgba(224, 122, 95, 0.16); + --color-option-bg: #ede9d4; + --color-option-border: #e0dcc8; +} + +/* ══════════════════════════════════════════════════════════════════════ + Structural overrides + ══════════════════════════════════════════════════════════════════════ */ + +/* ── Typography ──────────────────────────────────────────────────── */ +[data-style="brutalist"] { + font-family: "Space Grotesk", "Saira", sans-serif; + font-weight: 500; +} + +/* Only section-level headings get the uppercase treatment */ +[data-style="brutalist"] h1 { + font-weight: 900; + letter-spacing: 0.03em; +} + +/* ── Buttons — square corners, modest hard shadow ────────────────── */ +[data-style="brutalist"] button { + border-radius: 0; + border: 2px solid var(--color-border); + font-weight: 700; + box-shadow: 2px 2px 0 var(--color-border); + transition: box-shadow 0.08s ease, transform 0.08s ease, border-color 0.1s; +} + +[data-style="brutalist"] button:hover:not(:disabled) { + border-color: var(--color-border); + box-shadow: none; + transform: translate(2px, 2px); +} + +/* Ghost / icon-only buttons — keep borderless, no press effect */ +[data-style="brutalist"] .modal-close-btn, +[data-style="brutalist"] .playlist-remove-btn, +[data-style="brutalist"] .playlist-card-delete-btn, +[data-style="brutalist"] .vote-btn, +[data-style="brutalist"] .emoji-picker-float [frimousse-emoji], +[data-style="brutalist"] .emoji-picker-close-btn, +[data-style="brutalist"] .new-playlist-toggle, +[data-style="brutalist"] .btn--ghost, +[data-style="brutalist"] .audio-player-btn, +[data-style="brutalist"] .audio-player-vol-btn, +[data-style="brutalist"] .rich-content-thumbnail-btn { + border: none; + box-shadow: none; +} + +[data-style="brutalist"] .modal-close-btn:hover, +[data-style="brutalist"] .playlist-remove-btn:hover, +[data-style="brutalist"] .playlist-card-delete-btn:hover, +[data-style="brutalist"] .vote-btn:hover:not(:disabled), +[data-style="brutalist"] .emoji-picker-float [frimousse-emoji]:hover, +[data-style="brutalist"] .emoji-picker-close-btn:hover, +[data-style="brutalist"] .new-playlist-toggle:hover:not(:disabled), +[data-style="brutalist"] .btn--ghost:hover:not(:disabled), +[data-style="brutalist"] .audio-player-btn:hover:not(:disabled), +[data-style="brutalist"] .audio-player-vol-btn:hover:not(:disabled), +[data-style="brutalist"] .rich-content-thumbnail-btn:hover:not(:disabled) { + transform: none; + box-shadow: none; +} + +/* ── Segmented controls / visibility-toggle ──────────────────────── */ +[data-style="brutalist"] .visibility-toggle { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 2px 2px 0 var(--color-border); +} + +[data-style="brutalist"] .visibility-toggle button { + border-radius: 0; + border: none; + box-shadow: none; +} + +[data-style="brutalist"] .visibility-toggle button:hover:not(:disabled) { + transform: none; + box-shadow: none; +} + +/* ── Cards ───────────────────────────────────────────────────────── */ +[data-style="brutalist"] .dump-card, +[data-style="brutalist"] .playlist-card { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 3px 3px 0 var(--color-border); + transition: + box-shadow 0.08s ease, + transform 0.08s ease, + border-color 0.1s, + grid-template-rows 0.32s ease, + opacity 0.25s ease, + filter 0.3s ease; +} + +[data-style="brutalist"] .dump-card:hover, +[data-style="brutalist"] .playlist-card:hover { + border-color: var(--color-accent); + box-shadow: 3px 3px 0 var(--color-accent); + transform: translate(-2px, -2px); +} + +/* Don't shift cards that are animating out */ +[data-style="brutalist"] .dump-card--fading, +[data-style="brutalist"] .dump-card--dismissing { + transform: none; +} + +/* ── Dump post detail header ─────────────────────────────────────── */ +[data-style="brutalist"] .dump-post-header { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 3px 3px 0 var(--color-border); +} + +/* ── App header ──────────────────────────────────────────────────── */ +[data-style="brutalist"] .app-header { + border-bottom: 2px solid var(--color-border); +} + +[data-style="brutalist"] .app-header-user { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 2px 2px 0 var(--color-border); + transition: box-shadow 0.08s ease, transform 0.08s ease, background 0.1s; +} + +[data-style="brutalist"] .app-header-user:hover { + box-shadow: none; + transform: translate(2px, 2px); +} + +/* ── Inputs / textareas ──────────────────────────────────────────── */ +[data-style="brutalist"] + input:not([type="range"]):not([type="file"]):not([type="checkbox"]):not( + [type="radio"] + ), +[data-style="brutalist"] textarea { + border-radius: 0 !important; + border-width: 2px !important; + border-color: var(--color-border) !important; +} + +[data-style="brutalist"] .dump-form input:focus, +[data-style="brutalist"] .dump-form textarea:focus, +[data-style="brutalist"] .auth-form input:focus, +[data-style="brutalist"] .auth-form textarea:focus, +[data-style="brutalist"] .mention-textarea-wrap textarea:focus { + box-shadow: 2px 2px 0 var(--color-accent); +} + +/* ── Search bar ──────────────────────────────────────────────────── */ +[data-style="brutalist"] .search-bar-input, +[data-style="brutalist"] .search-bar-btn { + border-radius: 0; +} + +/* ── Modals ──────────────────────────────────────────────────────── */ +[data-style="brutalist"] .modal-card, +[data-style="brutalist"] .confirm-modal { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 4px 4px 0 var(--color-border); +} + +[data-style="brutalist"] .modal-header { + border-bottom: 2px solid var(--color-border); +} + +/* ── Section headings ────────────────────────────────────────────── */ +[data-style="brutalist"] .profile-section > h2, +[data-style="brutalist"] .profile-section-header, +[data-style="brutalist"] .comment-section-title { + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 700; +} + +[data-style="brutalist"] .profile-section > h2, +[data-style="brutalist"] .profile-section-header { + border-bottom-width: 2px; +} + +/* ── Avatars — square ────────────────────────────────────────────── */ +[data-style="brutalist"] .avatar-img, +[data-style="brutalist"] .avatar-initials { + border-radius: 0; +} + +[data-style="brutalist"] .avatar-change-overlay { + border-radius: 0; +} + +/* ── Rich content cards ──────────────────────────────────────────── */ +[data-style="brutalist"] .rich-content-card { + border-radius: 0; + box-shadow: 3px 3px 0 var(--color-border); +} + +/* Neutralise provider brand border-colors so they don't fight the hard shadow */ +[data-style="brutalist"] .rich-content-card--youtube, +[data-style="brutalist"] .rich-content-card--bandcamp, +[data-style="brutalist"] .rich-content-card--soundcloud { + border-color: var(--color-border); +} + +[data-style="brutalist"] + .rich-content-card--youtube:has(.rich-content-body:hover), +[data-style="brutalist"] + .rich-content-card--bandcamp:has(.rich-content-body:hover), +[data-style="brutalist"] + .rich-content-card--soundcloud:has(.rich-content-body:hover) { + border-color: var(--color-accent); + box-shadow: 3px 3px 0 var(--color-accent); +} + +/* ── File drop zone ──────────────────────────────────────────────── */ +[data-style="brutalist"] .fdz { + border-radius: 0; +} + +/* ── Global persistent player ────────────────────────────────────── */ +[data-style="brutalist"] .global-player { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 0 0 0 2px var(--color-border); +} + +[data-style="brutalist"] .global-player-iframe-wrap, +[data-style="brutalist"] .global-player-media-wrap, +[data-style="brutalist"] .global-player .audio-player, +[data-style="brutalist"] .global-player .video-player { + border-radius: 0; +} + +[data-style="brutalist"] .global-player .audio-player-btn { + background: transparent; + color: var(--color-text); +} + +[data-style="brutalist"] .global-player .audio-player-btn:hover { + background: var(--color-border-subtle); + color: var(--color-text); +} + +[data-style="brutalist"] .global-player .audio-player-vol-btn { + color: var(--color-text); +} + +[data-style="brutalist"] .global-player .btn--ghost { + color: var(--color-text); +} + +/* ── Notification items ──────────────────────────────────────────── */ +[data-style="brutalist"] .notification-item { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 2px 2px 0 var(--color-border); +} + +[data-style="brutalist"] .notifications-unread-pill { + border-radius: 0; +} + +[data-style="brutalist"] .notif-dot { + border-radius: 0; + box-shadow: none; +} + +[data-style="brutalist"] .notif-icon { + border-radius: 0; +} + +/* ── Notification badge ──────────────────────────────────────────── */ +[data-style="brutalist"] .notification-badge { + border-radius: 0; +} + +/* ── Markdown code ───────────────────────────────────────────────── */ +[data-style="brutalist"] .md code, +[data-style="brutalist"] .md pre { + border-radius: 0; +} + +/* ── User result items (search) ──────────────────────────────────── */ +[data-style="brutalist"] .user-result-item { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 2px 2px 0 var(--color-border); + transition: box-shadow 0.08s ease, transform 0.08s ease, border-color 0.1s; +} + +[data-style="brutalist"] .user-result-item:hover { + border-color: var(--color-accent); + box-shadow: 2px 2px 0 var(--color-accent); + transform: translate(-2px, -2px); +} + +/* ── Feed tab bar ────────────────────────────────────────────────── */ +[data-style="brutalist"] .feed-tab { + border-radius: 0; +} + +/* ── btn-primary ─────────────────────────────────────────────────── */ +[data-style="brutalist"] .btn-primary { + border-radius: 0; + border: 2px solid var(--color-border); + box-shadow: 2px 2px 0 var(--color-border); + transition: box-shadow 0.08s ease, transform 0.08s ease; +} + +[data-style="brutalist"] .btn-primary:hover:not(:disabled) { + box-shadow: none; + transform: translate(2px, 2px); +} + +[data-style="brutalist"] .btn-primary:active { + box-shadow: none; + transform: translate(2px, 2px); +} + +/* ── btn-border ──────────────────────────────────────────────────── */ +[data-style="brutalist"] .btn-border, +[data-style="brutalist"] .btn-border-danger, +[data-style="brutalist"] .btn-border-success { + border-radius: 0; + box-shadow: 2px 2px 0 var(--color-border); +} + +[data-style="brutalist"] .btn-border:hover, +[data-style="brutalist"] .btn-border-danger:hover, +[data-style="brutalist"] .btn-border-success:hover { + box-shadow: none; + transform: translate(2px, 2px); +} + +/* ── Comment actions ─────────────────────────────────────────────── */ +[data-style="brutalist"] .comment-submit-btn, +[data-style="brutalist"] .comment-action-btn { + border-radius: 0; +} + +/* ── Error card ──────────────────────────────────────────────────── */ +[data-style="brutalist"] .error-card { + border-radius: 0; +} diff --git a/src/themes/smooth.css b/src/themes/smooth.css new file mode 100644 index 0000000..e9b2af9 --- /dev/null +++ b/src/themes/smooth.css @@ -0,0 +1,162 @@ +/* ══════════════════════════════════════════════════════════════════════ + Smooth theme — clean, modern, Saira typeface + Palette: Prussian Blue · Deep Teal · Jasmine · Brick Ember · Blood Red + Applied when data-style is absent or "smooth". + ══════════════════════════════════════════════════════════════════════ */ + +/* ── Dark variant (default) ───────────────────────────────────────── */ +:root, +:root[data-style="smooth"] { + font-family: "Saira", sans-serif; + font-optical-sizing: auto; + font-weight: 400; + font-style: normal; + line-height: 1.5; + + color-scheme: light dark; + + /* Text */ + --color-text: #eef0f3; + --color-text-secondary: #8aaabb; + --color-text-muted: #506878; + --color-on-accent: #001427; /* Prussian Blue on Jasmine */ + + /* Surfaces */ + --color-bg: #001427; /* Prussian Blue */ + --color-surface: #0a1f35; + + /* Borders */ + --color-border: transparent; + --color-border-subtle: rgba(112, 141, 129, 0.2); /* Deep Teal tint */ + + /* Accent */ + --color-accent: #f4d58d; /* Jasmine */ + --color-accent-hover: #d4aa5a; + + /* Links */ + --color-link: #f4d58d; + --color-link-hover: #d4aa5a; + + /* Danger */ + --color-danger: #bf0603; /* Brick Ember */ + --color-danger-bg: #220404; + --color-danger-hover: #8d0801; /* Blood Red */ + + /* Success */ + --color-success: #708d81; /* Deep Teal */ + --color-success-bg: #0d2820; + --color-success-hover: #8aaa9a; + + /* Overlays */ + --color-overlay: rgba(0, 10, 20, 0.55); + --color-header-user-bg: rgba(244, 213, 141, 0.06); + --color-header-user-bg-hover: rgba(244, 213, 141, 0.12); + + /* Misc */ + --color-option-bg: #0d2340; + --color-option-border: #001427; + + /* Service brand colors (fixed, not theme-dependent) */ + --color-youtube: #c00; + --color-youtube-hover: #f00; + --color-bandcamp: #1da0c3; + --color-bandcamp-hover: #25c8f0; + --color-soundcloud: #f50; + --color-soundcloud-hover: #f73; +} + +/* ── Light via system preference (auto color-scheme) ──────────────── */ +@media (prefers-color-scheme: light) { + :root:not([data-style]), + :root[data-style="smooth"]:not([data-color-scheme]) { + --color-text: #001427; /* Prussian Blue */ + --color-text-secondary: #3a5a70; + --color-text-muted: #6a8898; + --color-on-accent: #ffffff; + + --color-bg: #f0f4f8; + --color-surface: #ffffff; + + --color-border: transparent; + --color-border-subtle: rgba(0, 20, 39, 0.12); + + --color-accent: #bf0603; /* Brick Ember — pops on light bg */ + --color-accent-hover: #8d0801; + + --color-link: #bf0603; + --color-link-hover: #8d0801; + + --color-danger: #bf0603; + --color-danger-bg: #fde8e8; + --color-danger-hover: #8d0801; + + --color-success: #4a6e60; /* darkened Deep Teal */ + --color-success-bg: #e0ede8; + --color-success-hover: #2e5448; + + --color-overlay: rgba(0, 10, 20, 0.4); + --color-header-user-bg: rgba(0, 20, 39, 0.07); + --color-header-user-bg-hover: rgba(0, 20, 39, 0.13); + + --color-option-bg: #f5f7fa; + --color-option-border: #dde3ea; + } +} + +/* ── Manual dark override ─────────────────────────────────────────── */ +:root:not([data-style])[data-color-scheme="dark"], +:root[data-style="smooth"][data-color-scheme="dark"] { + color-scheme: dark; + --color-text: #eef0f3; + --color-text-secondary: #8aaabb; + --color-text-muted: #506878; + --color-on-accent: #001427; + --color-bg: #001427; + --color-surface: #0a1f35; + --color-border: transparent; + --color-border-subtle: rgba(112, 141, 129, 0.2); + --color-accent: #f4d58d; + --color-accent-hover: #d4aa5a; + --color-link: #f4d58d; + --color-link-hover: #d4aa5a; + --color-danger: #bf0603; + --color-danger-bg: #220404; + --color-danger-hover: #8d0801; + --color-success: #708d81; + --color-success-bg: #0d2820; + --color-success-hover: #8aaa9a; + --color-overlay: rgba(0, 10, 20, 0.55); + --color-header-user-bg: rgba(244, 213, 141, 0.06); + --color-header-user-bg-hover: rgba(244, 213, 141, 0.12); + --color-option-bg: #0d2340; + --color-option-border: #001427; +} + +/* ── Manual light override ────────────────────────────────────────── */ +:root:not([data-style])[data-color-scheme="light"], +:root[data-style="smooth"][data-color-scheme="light"] { + color-scheme: light; + --color-text: #001427; + --color-text-secondary: #3a5a70; + --color-text-muted: #6a8898; + --color-on-accent: #ffffff; + --color-bg: #f0f4f8; + --color-surface: #ffffff; + --color-border: transparent; + --color-border-subtle: rgba(0, 20, 39, 0.12); + --color-accent: #bf0603; + --color-accent-hover: #8d0801; + --color-link: #bf0603; + --color-link-hover: #8d0801; + --color-danger: #bf0603; + --color-danger-bg: #fde8e8; + --color-danger-hover: #8d0801; + --color-success: #4a6e60; + --color-success-bg: #e0ede8; + --color-success-hover: #2e5448; + --color-overlay: rgba(0, 10, 20, 0.4); + --color-header-user-bg: rgba(0, 20, 39, 0.07); + --color-header-user-bg-hover: rgba(0, 20, 39, 0.13); + --color-option-bg: #f5f7fa; + --color-option-border: #dde3ea; +}