mirror of
https://github.com/narrrl/pl8list-rewind.git
synced 2026-11-04 16:47:00 +01:00
feat(ui): refine listening controls and styling
This commit is contained in:
1 parent
b01403af6b
commit
cc53fbe911
5 files changed
+71
-44
No files matched your search
@@ -30,7 +30,7 @@ Not affiliated with pl8list.
|
||||
|
||||
- **One track per screen.** Swipe, scroll or press ↓. The next track is already buffered and
|
||||
crossfades in.
|
||||
- **A feed that learns.** Likes, finished plays, skips, "not for me" and follows shape what
|
||||
- **A feed that learns.** Likes, finished plays, skips and follows shape what
|
||||
comes next. Four feeds: for you, riddim, fresh and deep cuts.
|
||||
- **Artist radio.** Start a radio from any artist: them and their neighbours in the artist
|
||||
graph, with a reason on each track.
|
||||
@@ -168,7 +168,6 @@ visitor address reaches pl8list or a CDN.
|
||||
| drop ↓ button | d | jump to the next drop |
|
||||
| follow button | f | follow the artist |
|
||||
| radio button | r | artist radio (more like them) |
|
||||
| ✕ button | x | not for me (skip, show less) |
|
||||
| share button | s | share / copy link |
|
||||
| | v | liquid / simple visuals |
|
||||
| | z | zen: just the drop |
|
||||
@@ -253,7 +252,7 @@ one request every 20 seconds. When a user likes or finishes a track, its related
|
||||
its artist's catalogue are crawled first.
|
||||
|
||||
**Ranking** (`backend/src/ranker.rs`). Each track the user has not seen yet gets a score from popularity,
|
||||
freshness, artist affinity (from their likes, completions, skips, "not for me" and follows), closeness to the BPM and
|
||||
freshness, artist affinity (from their likes, completions, skips and follows), closeness to the BPM and
|
||||
key of tracks they liked, whether it is related to something they liked, and some random
|
||||
exploration. Artists are then spread out so you don't get five tracks from one person in a
|
||||
row. Weights live in a `Ranker`, which can also narrow the candidates and add a bonus;
|
||||
|
||||
+6
-6
@@ -105,7 +105,8 @@
|
||||
<form id="filters" aria-labelledby="filters-title">
|
||||
<h2 id="filters-title">filters and settings</h2>
|
||||
<fieldset>
|
||||
<legend>filters</legend>
|
||||
<legend>feed filters</legend>
|
||||
<p class="setting-note">Apply these changes to refresh your feed.</p>
|
||||
<label class="switch"><input type="checkbox" name="free"> <span>free downloads only</span></label>
|
||||
<div class="bpm">
|
||||
<div class="bpm-head"><span>bpm</span><output id="bpm-out">any</output></div>
|
||||
@@ -125,6 +126,7 @@
|
||||
<button type="button" id="filters-clear" class="link">clear</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="settings-divider"><h3>listening preferences</h3><p class="setting-note">Changes take effect immediately.</p></div>
|
||||
<fieldset class="modes">
|
||||
<legend>visuals</legend>
|
||||
<label><input type="radio" name="visuals" value="liquid"> liquid</label>
|
||||
@@ -264,7 +266,6 @@
|
||||
<dt>← →</dt><dd>seek 10 s</dd>
|
||||
<dt>f</dt><dd>follow artist</dd>
|
||||
<dt>r</dt><dd>artist radio</dd>
|
||||
<dt>x</dt><dd>not for me</dd>
|
||||
<dt>s</dt><dd>share</dd>
|
||||
<dt>v</dt><dd>liquid / simple visuals</dd>
|
||||
<dt>z</dt><dd>zen: just the drop</dd>
|
||||
@@ -278,7 +279,7 @@
|
||||
<p>Drag the liquid artwork sideways to spin it. On riddim it turns to ferrofluid: spikes
|
||||
stand up with the bass, and each kick raises the same crown every bar. Lock-screen and
|
||||
headphone buttons work too.</p>
|
||||
<p class="pointer-only fineprint">Keys like r or x doing something else, such as reloading or
|
||||
<p class="pointer-only fineprint">Keys like r doing something else, such as reloading or
|
||||
closing the tab? A vim-keys browser extension (Vimium and the like) takes them first:
|
||||
turn it off for this site.</p>
|
||||
<button type="button" id="help-close">got it</button>
|
||||
@@ -308,14 +309,13 @@
|
||||
<div class="chips"></div>
|
||||
<p class="desc"></p>
|
||||
<div class="wave-wrap">
|
||||
<canvas class="wave"></canvas>
|
||||
<canvas class="wave" aria-hidden="true"></canvas>
|
||||
<span class="wave-tip"></span>
|
||||
</div>
|
||||
<div class="time"><span class="cur">0:00</span><button type="button" class="to-drop" title="jump to the drop (d)">drop <svg><use href="#i-down"/></svg></button><span class="counts"></span><span class="dur"></span></div>
|
||||
<div class="player-controls"><button type="button" class="play-toggle" aria-label="pause"><svg aria-hidden="true"><use href="#i-pause"/></svg></button><div class="seek-controls"><input class="seek" type="range" min="0" max="0" value="0" step="1" aria-label="seek track" disabled><div class="time"><span class="cur">0:00</span><button type="button" class="to-drop" title="jump to the next marked drop (d)">drop <svg><use href="#i-down"/></svg></button><span class="counts"></span><span class="dur"></span></div></div></div>
|
||||
</div>
|
||||
<nav class="actions">
|
||||
<button type="button" class="act like" title="like (l)" aria-label="like"><span class="ring"><svg><use href="#i-heart"/></svg></span><span class="n saves"></span></button>
|
||||
<button type="button" class="act dislike" title="not for me (x)" aria-label="not for me"><span class="ring"><svg><use href="#i-x"/></svg></span><span class="n">nope</span></button>
|
||||
<button type="button" class="act share" title="share (s)" aria-label="share"><span class="ring"><svg><use href="#i-share"/></svg></span><span class="n">share</span></button>
|
||||
<a class="act open" target="_blank" rel="noopener" title="open on pl8list" aria-label="open on pl8list"><span class="ring"><svg><use href="#i-comment"/></svg></span><span class="n comments"></span></a>
|
||||
<button type="button" class="act next" title="next (j)" aria-label="next"><span class="ring"><svg><use href="#i-down"/></svg></span><span class="n">next</span></button>
|
||||
|
||||
+24
-3
@@ -7,7 +7,7 @@ import { WaveComments } from "./comments";
|
||||
|
||||
export interface CardActions {
|
||||
like(card: Card): void;
|
||||
dislike(card: Card): void;
|
||||
togglePlayback(card: Card): void;
|
||||
share(card: Card): void;
|
||||
next(card: Card): void;
|
||||
follow(card: Card): void;
|
||||
@@ -191,7 +191,9 @@ export class Card {
|
||||
$(".comments").textContent = compact(t.comments);
|
||||
|
||||
$(".like").addEventListener("click", () => this.actions.like(this));
|
||||
$(".dislike").addEventListener("click", () => this.actions.dislike(this));
|
||||
$(".play-toggle").addEventListener("click", () => this.actions.togglePlayback(this));
|
||||
const seek = $<HTMLInputElement>(".seek");
|
||||
seek.addEventListener("input", () => { if (this.actions.canSeek(this)) this.actions.seek(this, Number(seek.value)); });
|
||||
$(".share").addEventListener("click", () => this.actions.share(this));
|
||||
$(".next").addEventListener("click", () => this.actions.next(this));
|
||||
$(".follow").addEventListener("click", () => this.actions.follow(this));
|
||||
@@ -230,6 +232,7 @@ export class Card {
|
||||
const btn = this.el?.querySelector(".like");
|
||||
if (!btn) return;
|
||||
btn.classList.toggle("on", this.liked);
|
||||
btn.setAttribute("aria-pressed", String(this.liked));
|
||||
if (pop && this.liked) {
|
||||
btn.classList.remove("pop");
|
||||
void (btn as HTMLElement).offsetWidth; // restart the animation
|
||||
@@ -242,6 +245,7 @@ export class Card {
|
||||
if (!btn) return;
|
||||
const on = !!this.track.followed;
|
||||
btn.classList.toggle("on", on);
|
||||
btn.setAttribute("aria-pressed", String(on));
|
||||
btn.querySelector("span")!.textContent = on ? "following" : "follow";
|
||||
btn.querySelector("use")!.setAttribute("href", on ? "#i-check" : "#i-plus");
|
||||
}
|
||||
@@ -249,6 +253,21 @@ export class Card {
|
||||
setTime(seconds: number): void {
|
||||
const cur = this.el?.querySelector(".cur");
|
||||
if (cur) cur.textContent = fmtTime(seconds);
|
||||
const seek = this.el?.querySelector<HTMLInputElement>(".seek");
|
||||
if (seek) {
|
||||
const duration = this.actions.duration(this);
|
||||
seek.disabled = !this.actions.canSeek(this) || !Number.isFinite(duration);
|
||||
seek.max = String(Number.isFinite(duration) ? duration : 0);
|
||||
seek.value = String(seconds);
|
||||
seek.setAttribute("aria-valuetext", `${fmtTime(seconds)} of ${fmtTime(Number.isFinite(duration) ? duration : 0)}`);
|
||||
}
|
||||
}
|
||||
|
||||
setPlaying(playing: boolean): void {
|
||||
const button = this.el?.querySelector<HTMLButtonElement>(".play-toggle");
|
||||
if (!button) return;
|
||||
button.setAttribute("aria-label", playing ? "pause" : "play");
|
||||
button.querySelector("use")?.setAttribute("href", playing ? "#i-pause" : "#i-play");
|
||||
}
|
||||
|
||||
private lastProgress = 0;
|
||||
@@ -294,7 +313,7 @@ export class Card {
|
||||
const v = level(i);
|
||||
const x = i * BAR;
|
||||
const top = Math.max(2, v * (base - 2));
|
||||
const color = i < played ? accent : i < loaded ? "rgba(255,255,255,0.5)" : "rgba(255,255,255,0.22)";
|
||||
const color = i < played ? accent : i < loaded ? "rgba(255,255,255,0.7)" : "rgba(255,255,255,0.42)";
|
||||
// The bar, standing on the line; played ones glow.
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = i < played ? grad : color;
|
||||
@@ -312,6 +331,8 @@ export class Card {
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.fillStyle = "#fff";
|
||||
ctx.fillRect(Math.min(w - 2, Math.max(0, progress * w)), 5, 2, base - 5);
|
||||
// A small mark over each drop.
|
||||
for (const d of this.drops) {
|
||||
const x = Math.round(d * bars) * BAR + (BAR - 1) / 2;
|
||||
|
||||
+9
-20
@@ -202,6 +202,8 @@ function activate(idx: number): void {
|
||||
const card = cards[idx];
|
||||
if (pager.index !== idx) pager.go(idx, { instant: true });
|
||||
card.element().classList.add("current");
|
||||
card.setPlaying(false);
|
||||
card.setTime(0);
|
||||
visual.attach(card);
|
||||
if (waveComments) card.loadComments();
|
||||
if (started) play(card.track);
|
||||
@@ -260,7 +262,7 @@ upNext.addEventListener("click", () => go(active + 1));
|
||||
|
||||
const actions: CardActions = {
|
||||
like: (c) => setLike(c, !c.liked),
|
||||
dislike: (c) => dislike(c),
|
||||
togglePlayback: (c) => { if (c === current() && started) player.toggle(); },
|
||||
share: (c) => share(c),
|
||||
next: (c) => go(cards.indexOf(c) + 1),
|
||||
follow: (c) => setFollow(c, !c.track.followed),
|
||||
@@ -299,22 +301,6 @@ function nudgeSignIn(): void {
|
||||
toast("sign in with pl8list to keep these, and bring your library along", 8000, { label: "sign in", run: () => showLogin() });
|
||||
}
|
||||
|
||||
function dislike(card: Card): void {
|
||||
api.sendEvent(card.track, "dislike");
|
||||
if (card === current()) reported = true; // a dislike says more than a skip
|
||||
const wasLiked = card.liked;
|
||||
if (wasLiked) setLike(card, false);
|
||||
go(cards.indexOf(card) + 1);
|
||||
toast("skipped, less of this", 5000, {
|
||||
label: "undo",
|
||||
run: () => {
|
||||
api.sendEvent(card.track, "undislike");
|
||||
if (wasLiked) setLike(card, true);
|
||||
go(cards.indexOf(card));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function setFollow(card: Card, follow: boolean): Promise<void> {
|
||||
const { handle, artist_name } = card.track;
|
||||
// Every card by this artist changes together.
|
||||
@@ -429,10 +415,12 @@ player.on("ended", () => {
|
||||
|
||||
player.on("play", () => {
|
||||
current()?.el?.classList.remove("paused");
|
||||
current()?.setPlaying(true);
|
||||
visual.start();
|
||||
if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "playing";
|
||||
});
|
||||
player.on("pause", () => {
|
||||
current()?.setPlaying(false);
|
||||
if (!player.audio.ended) current()?.el?.classList.add("paused");
|
||||
current()?.el?.classList.remove("buffering");
|
||||
if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "paused";
|
||||
@@ -608,7 +596,7 @@ function start(): void {
|
||||
if (!localStorage.getItem(HINT_KEY)) {
|
||||
localStorage.setItem(HINT_KEY, "1");
|
||||
toast(isTouch ? "tap to pause · double-tap to like · swipe up for next"
|
||||
: "click to pause · double-click to like · ? for shortcuts", 5000);
|
||||
: "click to pause · double-click to like · ? for shortcuts", 8000, { label: "got it", run: () => {} });
|
||||
}
|
||||
}
|
||||
startEl.addEventListener("click", (e) => {
|
||||
@@ -656,6 +644,7 @@ function trackRow(t: Track, sub: string, extra?: HTMLElement): HTMLLIElement {
|
||||
a.target = "_blank";
|
||||
a.rel = "noopener";
|
||||
a.textContent = cleanTitle(t.title);
|
||||
a.title = cleanTitle(t.title);
|
||||
const by = document.createElement("span");
|
||||
by.textContent = sub;
|
||||
meta.append(a, by);
|
||||
@@ -1033,13 +1022,14 @@ showFilters();
|
||||
// Keyboard --------------------------------------------------------------
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if ((e.target as Element).tagName === "INPUT" || e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
||||
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
||||
if (k === "Escape") {
|
||||
setZen(false);
|
||||
station.close();
|
||||
return;
|
||||
}
|
||||
if ((e.target as Element).closest("input, button, a, select, textarea")) return;
|
||||
if (k === "?") { sheets.help.toggle(); return; }
|
||||
if (station.isOpen || Sheet.current || !loginEl.hidden) return;
|
||||
if (!started) {
|
||||
@@ -1060,7 +1050,6 @@ document.addEventListener("keydown", (e) => {
|
||||
else if (k === "l" && card) setLike(card, !card.liked);
|
||||
else if (k === "f" && card) setFollow(card, !card.track.followed);
|
||||
else if (k === "r" && card) actions.radio(card);
|
||||
else if (k === "x" && card) dislike(card);
|
||||
else if (k === "s" && card) share(card);
|
||||
else if (k === "h") sheets.history.open();
|
||||
else if (k === "c") sheets.filters.open();
|
||||
|
||||
+30
-12
@@ -4,8 +4,8 @@
|
||||
color-scheme: dark;
|
||||
--bg: #000;
|
||||
--fg: #f2f2f2;
|
||||
--muted: #9a9a9a;
|
||||
--faint: #666;
|
||||
--muted: #b5b5b8;
|
||||
--faint: #96969a;
|
||||
--accent: #ff4d6d;
|
||||
--panel: rgba(18, 18, 20, 0.94);
|
||||
--line: #ffffff1f;
|
||||
@@ -18,7 +18,7 @@
|
||||
--radius-l: 18px;
|
||||
--shadow: 0 16px 40px #000a;
|
||||
--font: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--font-display: var(--font);
|
||||
--font-display: "Arial Narrow", "Roboto Condensed", "Liberation Sans Narrow", sans-serif;
|
||||
--ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
--fast: 0.15s;
|
||||
}
|
||||
@@ -232,6 +232,9 @@ body.zen #bar { display: none; }
|
||||
#sleep-left { text-transform: none; letter-spacing: 0; color: var(--accent); }
|
||||
#spikes-setting legend small { text-transform: none; letter-spacing: 0; color: var(--muted); font-weight: 400; }
|
||||
.sheet-actions { display: flex; align-items: center; gap: 1rem; }
|
||||
.setting-note { margin: 0; color: var(--muted); font-size: 0.8rem; }
|
||||
.settings-divider { border-top: 1px solid var(--line-strong); padding-top: 1rem; }
|
||||
.settings-divider h3 { margin: 0 0 0.25rem; color: var(--fg); }
|
||||
.sheet-tools { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
|
||||
.sheet-tools input[type=search] {
|
||||
flex: 1 1 10rem;
|
||||
@@ -335,7 +338,7 @@ body.zen #bar { display: none; }
|
||||
}
|
||||
.presets { display: flex; gap: 0.4rem; }
|
||||
.presets button { font-size: 0.8rem; padding: 0.25em 0.8em; }
|
||||
.presets button[aria-pressed="true"] { background: var(--accent); border-color: transparent; color: #fff; }
|
||||
.presets button[aria-pressed="true"] { background: var(--accent); border-color: transparent; color: #111; }
|
||||
|
||||
/* Likes and history: artwork that plays, title, artist. */
|
||||
.track-list { margin: 0; padding: 0; list-style: none; color: var(--muted); }
|
||||
@@ -349,7 +352,7 @@ body.zen #bar { display: none; }
|
||||
}
|
||||
.track-row:last-child { border-bottom: none; }
|
||||
.track-row .meta { min-width: 0; flex: 1; }
|
||||
.track-row .meta a { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-decoration: none; color: var(--fg); }
|
||||
.track-row .meta a { display: block; overflow-wrap: anywhere; text-decoration: none; color: var(--fg); }
|
||||
.track-row .meta a:hover { text-decoration: underline; }
|
||||
.track-row .meta span { font-size: 0.8rem; }
|
||||
.row-play {
|
||||
@@ -365,8 +368,8 @@ body.zen #bar { display: none; }
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
.row-play::before { content: ""; position: absolute; inset: 0; background: #0006; opacity: 0; transition: opacity var(--fast); }
|
||||
.row-play svg { position: relative; width: 1rem; height: 1rem; opacity: 0; transition: opacity var(--fast); }
|
||||
.row-play::before { content: ""; position: absolute; inset: 0; background: #000a; opacity: 0.75; transition: opacity var(--fast); }
|
||||
.row-play svg { position: relative; width: 1rem; height: 1rem; opacity: 1; transition: opacity var(--fast); }
|
||||
.row-play:hover::before, .row-play:focus-visible::before, .row-play:hover svg, .row-play:focus-visible svg { opacity: 1; }
|
||||
@media not ((hover: hover) and (pointer: fine)) {
|
||||
.row-play::before { opacity: 0.6; }
|
||||
@@ -565,6 +568,7 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
text-wrap: balance;
|
||||
font-stretch: condensed;
|
||||
}
|
||||
.why {
|
||||
margin: 0;
|
||||
@@ -632,6 +636,12 @@ button.danger:not(.link) { background: var(--accent); border: none; color: #fff;
|
||||
|
||||
.wave-wrap { position: relative; width: 100%; margin-top: 0.6rem; }
|
||||
.wave { display: block; width: 100%; height: 64px; cursor: pointer; touch-action: none; }
|
||||
.player-controls { display: flex; align-items: center; gap: 0.75rem; width: 100%; }
|
||||
.play-toggle { flex: none; width: 2.75rem; height: 2.75rem; padding: 0; display: grid; place-items: center; border-radius: 8px; background: var(--fg); color: #111; }
|
||||
.play-toggle svg { width: 1.35rem; height: 1.35rem; }
|
||||
.seek-controls { min-width: 0; flex: 1; }
|
||||
.seek { display: block; width: 100%; height: 1rem; margin: 0 0 0.2rem; accent-color: var(--accent); cursor: pointer; }
|
||||
.seek:disabled { opacity: 0.5; cursor: default; }
|
||||
.wave-tip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 4px);
|
||||
@@ -811,7 +821,7 @@ body.no-wave-comments :is(.wave-comments, .wc-bubble), body.zen :is(.wave-commen
|
||||
height: 3rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
transition: background var(--fast), transform var(--fast);
|
||||
}
|
||||
@@ -1078,7 +1088,7 @@ body.zen #up-next { display: none; }
|
||||
button.primary {
|
||||
background: var(--accent);
|
||||
border: none;
|
||||
color: #fff;
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
padding: 0.7em 1.2em;
|
||||
display: inline-flex;
|
||||
@@ -1163,11 +1173,11 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
|
||||
.login-links { display: flex; justify-content: space-between; }
|
||||
.login-box label.check { display: flex; gap: 0.6rem; align-items: flex-start; color: var(--fg); cursor: pointer; }
|
||||
.check input { margin-top: 0.2rem; accent-color: var(--accent); }
|
||||
.check small { display: block; margin-top: 0.2rem; color: #777; font-size: 0.75rem; line-height: 1.4; }
|
||||
.check small { display: block; margin-top: 0.2rem; color: #aaa; font-size: 0.75rem; line-height: 1.4; }
|
||||
#login-cancel { align-self: center; }
|
||||
.link { background: none; border: none; padding: 0; color: var(--muted); text-decoration: underline; font-size: 0.85rem; }
|
||||
#login-error { margin: 0; min-height: 1.2em; color: var(--accent); font-size: 0.9rem; }
|
||||
.fineprint { margin: 0; color: #666; font-size: 0.75rem; }
|
||||
.fineprint { margin: 0; color: #aaa; font-size: 0.75rem; }
|
||||
|
||||
/* Phones ----------------------------------------------------------------- */
|
||||
|
||||
@@ -1213,7 +1223,7 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
|
||||
border-radius: 999px;
|
||||
background: #fff5;
|
||||
}
|
||||
.content { width: calc(100vw - 2rem); margin-bottom: 5.5rem; }
|
||||
.content { width: calc(100vw - 2rem); margin-bottom: 6.5rem; gap: 0.6rem; }
|
||||
.actions {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
@@ -1226,6 +1236,14 @@ button.primary:disabled { opacity: 0.35; cursor: default; }
|
||||
#toast { bottom: calc(max(0.8rem, env(safe-area-inset-bottom)) + 8rem); }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) and (max-height: 700px) {
|
||||
.art { width: min(13rem, 27dvh, 55vw); margin-bottom: 0.15rem; }
|
||||
.content { gap: 0.3rem; margin-top: 4rem; }
|
||||
.wave-wrap { margin-top: 0; }
|
||||
.wave { height: 48px; }
|
||||
.desc { display: none; }
|
||||
}
|
||||
|
||||
/* Zen: nothing but the drop, bigger, and the music. */
|
||||
body.zen #bar, body.zen .actions, body.zen .chips, body.zen .desc, body.zen .counts,
|
||||
body.zen .why, body.zen .byline button, body.zen #feed-status { display: none; }
|
||||
|
||||
Reference in new issue
Block a user