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"],"0> followed your playlist <1>",["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"],"0> upvoted <1>",["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 files0>"],"QLtPBd":["No dumps in this playlist yet."],"RCcPrX":["Delete this playlist? This cannot be undone."],"RTksSy":["<0>",["0"],"0> started following you"],"RaKjrM":["Failed to save edit"],"RcUHRT":["Followed"],"SBTElJ":["Searching…"],"Sxm8rQ":["Users"],"T9bjWt":["<0>",["0"],"0> was added to <1>",["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"],"0> posted <1>",["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"],"0> mentioned you in <1>",["where"],"1>"],"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 in0>"],"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"],"0> followed your playlist <1>",["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"],"0> upvoted <1>",["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 files0>"],"QLtPBd":["No dumps in this playlist yet."],"R9Khdg":["Auto"],"RCcPrX":["Delete this playlist? This cannot be undone."],"RTksSy":["<0>",["0"],"0> started following you"],"RaKjrM":["Failed to save edit"],"RcUHRT":["Followed"],"SBTElJ":["Searching…"],"Sxm8rQ":["Users"],"T9bjWt":["<0>",["0"],"0> was added to <1>",["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"],"0> posted <1>",["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"],"0> mentioned you in <1>",["where"],"1>"],"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 in0>"],"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 in0>"
msgstr "Already have an account? <0>Log in0>"
+#: 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 files0>"
msgstr "or <0>browse files0>"
-#: 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"],"0> a suivi votre collection <1>",["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"],"0> a voté pour <1>",["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 fichiers0>"],"QLtPBd":["Aucune reco dans cette collection pour l\'instant."],"R9Khdg":["Auto"],"RCcPrX":["Supprimer cette collection ? Cette action est irréversible."],"RTksSy":["<0>",["0"],"0> a commencé à vous suivre"],"RaKjrM":["Impossible d\'enregistrer la modification"],"RcUHRT":["Suivi"],"SBTElJ":["Recherche…"],"Sxm8rQ":["Utilisateurs"],"T9bjWt":["<0>",["0"],"0> a été ajouté à <1>",["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"],"0> a publié <1>",["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"],"0> vous a mentionné dans <1>",["where"],"1>"],"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 connecter0>"],"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 in0>"
msgstr "Vous avez déjà un compte ? <0>Se connecter0>"
+#: 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 files0>"
msgstr "ou <0>parcourir les fichiers0>"
-#: 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;
+}