feat(feed): reshuffle button for the "surprise me" sort

The backend shuffle sort already accepts a seed param. Add a circular
reshuffle button next to the sort control (shown only when shuffle is
active) that re-rolls the seed and re-queries the feed; selecting shuffle
also seeds a fresh order instead of the deterministic seed-0 one. The seed
lives in FeedFilters but is intentionally kept out of the URL state.
This commit is contained in:
npeter83 2026-06-15 03:08:10 +02:00
parent b325893b22
commit 5a96607557
5 changed files with 43 additions and 11 deletions

View file

@ -8,6 +8,7 @@ import {
EyeOff, EyeOff,
GripVertical, GripVertical,
Pencil, Pencil,
RefreshCw,
RotateCcw, RotateCcw,
X, X,
} from "lucide-react"; } from "lucide-react";
@ -48,6 +49,9 @@ const SORT_IDS = [
const SHOW_IDS = ["unwatched", "in_progress", "all", "watched", "saved", "hidden"]; const SHOW_IDS = ["unwatched", "in_progress", "all", "watched", "saved", "hidden"];
// Fresh shuffle token for the "surprise me" sort.
const rollSeed = () => Math.floor(Math.random() * 1_000_000_000);
const DATE_PRESETS: { days: number; key: string }[] = [ const DATE_PRESETS: { days: number; key: string }[] = [
{ days: 1, key: "24h" }, { days: 1, key: "24h" },
{ days: 7, key: "1week" }, { days: 7, key: "1week" },
@ -215,17 +219,38 @@ export default function Sidebar({
); );
case "sort": case "sort":
return ( return (
<select <div className="flex items-center gap-1.5">
value={filters.sort} <select
onChange={(e) => setFilters({ ...filters, sort: e.target.value })} value={filters.sort}
className="w-full bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent" onChange={(e) => {
> const sort = e.target.value;
{SORT_IDS.map((id) => ( // Seed shuffle on selection so it lands on a fresh order, not the
<option key={id} value={id}> // deterministic seed-0 one every time.
{t("sidebar.sort." + id)} setFilters({
</option> ...filters,
))} sort,
</select> seed: sort === "shuffle" ? rollSeed() : undefined,
});
}}
className="w-full bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent"
>
{SORT_IDS.map((id) => (
<option key={id} value={id}>
{t("sidebar.sort." + id)}
</option>
))}
</select>
{filters.sort === "shuffle" && (
<button
onClick={() => setFilters({ ...filters, seed: rollSeed() })}
title={t("sidebar.reshuffle")}
aria-label={t("sidebar.reshuffle")}
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg shadow-sm hover:border-accent hover:text-accent active:translate-y-px transition"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
</div>
); );
case "date": case "date":
return ( return (

View file

@ -23,6 +23,7 @@
"from": "Von", "from": "Von",
"to": "Bis", "to": "Bis",
"clearDates": "Daten löschen", "clearDates": "Daten löschen",
"reshuffle": "Neu mischen",
"widget": { "widget": {
"show": "Anzeigen", "show": "Anzeigen",
"sort": "Sortierung", "sort": "Sortierung",

View file

@ -23,6 +23,7 @@
"from": "From", "from": "From",
"to": "To", "to": "To",
"clearDates": "clear dates", "clearDates": "clear dates",
"reshuffle": "Reshuffle",
"widget": { "widget": {
"show": "Show", "show": "Show",
"sort": "Sort", "sort": "Sort",

View file

@ -23,6 +23,7 @@
"from": "Ettől", "from": "Ettől",
"to": "Eddig", "to": "Eddig",
"clearDates": "dátumok törlése", "clearDates": "dátumok törlése",
"reshuffle": "Újrakeverés",
"widget": { "widget": {
"show": "Megjelenítés", "show": "Megjelenítés",
"sort": "Rendezés", "sort": "Rendezés",

View file

@ -73,6 +73,9 @@ export interface FeedFilters {
tagMode: "or" | "and"; tagMode: "or" | "and";
q: string; q: string;
sort: string; sort: string;
// Re-roll token for the "shuffle" sort; bumped by the reshuffle button so the
// feed re-queries with a fresh order. Ignored by every other sort.
seed?: number;
includeNormal: boolean; includeNormal: boolean;
includeShorts: boolean; includeShorts: boolean;
includeLive: boolean; includeLive: boolean;
@ -153,6 +156,7 @@ function feedQuery(f: FeedFilters, offset: number, limit: number): string {
p.set("tag_mode", f.tagMode); p.set("tag_mode", f.tagMode);
if (f.q) p.set("q", f.q); if (f.q) p.set("q", f.q);
p.set("sort", f.sort); p.set("sort", f.sort);
if (f.sort === "shuffle" && f.seed) p.set("seed", String(f.seed));
p.set("show_normal", String(f.includeNormal)); p.set("show_normal", String(f.includeNormal));
p.set("include_shorts", String(f.includeShorts)); p.set("include_shorts", String(f.includeShorts));
p.set("include_live", String(f.includeLive)); p.set("include_live", String(f.includeLive));