chore: remove NSFW blur gate and Discreet mode (UI only)

User reported the NSFW toggle was not appearing, then asked to remove
it entirely. For a solo home-NAS deploy a global blur layer adds no
value — if the user wants to filter, the existing `/api/media?nsfw=`
query param still works.

Removed
- Two header buttons: NSFW 👁/🙈 and Discreto 🤫/👁
- The discreet-banner above the alerts
- body x-data + x-init (no longer needed without the store)
- filter-nsfw select from the gallery header
- nsfwFilter param building from loadGallery's URL
- isNsfw / nsfwBadge / thumbClass branching in appendToGallery
  (every gallery thumb now just gets class "gallery-thumb")
- Alpine.store('prefs') registration and the autoDiscreet IIFE
- CSS blocks: .nsfw-thumb, .show-nsfw, .nsfw-badge, .discreet, and
  .discreet-banner

Kept
- posts.nsfw column populated from Reddit's over_18
- GET /api/media?nsfw=all|hide|only query param + corresponding
  Database filtering
- over18=1 cookie on the requests Session
- PIN lock and 🔒 Lock header button (independent feature)

Bump 1.5.3 -> 1.5.4 (patch — UI removal, no backend or contract
change).

151 tests + ruff + mypy still green; grep confirms zero showNsfw /
nsfw-thumb / discreet / autoDiscreet references left in src/web/.
This commit is contained in:
authentik Default Admin 2026-05-18 00:15:41 +01:00
parent 74c6c4d759
commit 896b42f935
5 changed files with 8 additions and 137 deletions

View file

@ -7,7 +7,7 @@ packages = ["src"]
[project]
name = "reddit-media-collector"
version = "1.5.3"
version = "1.5.4"
description = "Self-hosted media collector for Reddit with Immich integration"
readme = "README.md"
requires-python = ">=3.11"

View file

@ -1101,38 +1101,6 @@ button:hover {
font-size: 12px;
}
/* NSFW + discreet */
.nsfw-thumb {
filter: blur(22px);
transition: filter 0.2s;
}
.show-nsfw .nsfw-thumb {
filter: none;
}
.gallery-item:hover .nsfw-thumb {
filter: blur(12px);
}
.show-nsfw .gallery-item:hover .nsfw-thumb {
filter: none;
}
.nsfw-badge {
position: absolute;
top: 5px;
left: 5px;
background: rgba(234, 0, 39, 0.95);
color: #fff;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: bold;
z-index: 6;
letter-spacing: 0.5px;
}
.select-mode .gallery-item .nsfw-badge {
left: 38px;
}
.flair-badge {
display: inline-block;
margin-top: 4px;
@ -1147,41 +1115,6 @@ button:hover {
white-space: nowrap;
}
/* Discreet mode: compact grids, no large modals */
.discreet .gallery-grid,
.discreet .authors-grid {
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)) !important;
}
.discreet .recent-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.discreet .recent-thumb {
width: 36px;
height: 36px;
}
.discreet .gallery-info,
.discreet .author-info {
padding: 6px;
}
.discreet .gallery-title,
.discreet .author-name {
font-size: 11px;
}
.discreet .flair-badge {
display: none;
}
.discreet-banner {
background: #343536;
color: #818384;
text-align: center;
padding: 4px 10px;
font-size: 11px;
border-bottom: 1px solid #1a1a1b;
}
body:not(.discreet) .discreet-banner {
display: none;
}
/* Tag chips on gallery item (Fase 2 preview) */
.tag-chips {
display: flex;

View file

@ -400,9 +400,8 @@ async function loadGallery(append = false) {
const mediaType = document.getElementById('filter-type').value;
const favoritesFilter = document.getElementById('filter-favorites').value;
const sortOrder = document.getElementById('filter-sort').value;
const nsfwFilter = document.getElementById('filter-nsfw')?.value || 'all';
try {
let url = `/api/media?limit=${pageSize}&offset=${galleryOffset}&sort=${sortOrder}&nsfw=${nsfwFilter}`;
let url = `/api/media?limit=${pageSize}&offset=${galleryOffset}&sort=${sortOrder}`;
if (subreddit) url += `&subreddit=${encodeURIComponent(subreddit)}`;
if (author) url += `&author=${encodeURIComponent(author)}`;
if (mediaType) url += `&media_type=${encodeURIComponent(mediaType)}`;
@ -507,12 +506,9 @@ function appendToGallery(files) {
const thumbSrc = isVideo
? `/api/media/thumb/${filename}`
: `/api/media/file/${filename}`;
const isNsfw = !!item.nsfw;
const thumbClass = isNsfw ? 'gallery-thumb nsfw-thumb' : 'gallery-thumb';
const sourceIcon = item.source_type === 'user' ? '👤' : '📂';
const flair = item.flair ? `<div class="flair-badge" title="Flair">${escapeHtml(item.flair)}</div>` : '';
const chips = renderTagChips(item.tags);
const nsfwBadge = isNsfw ? '<div class="nsfw-badge" title="NSFW">NSFW</div>' : '';
const age = relativeTime(item.created_utc);
return `
<div class="gallery-item" data-id="${item.id}" data-author="${escapeHtml(item.author || '')}" onclick="handleGalleryClick(event, this)">
@ -523,11 +519,10 @@ function appendToGallery(files) {
<div class="score-badge ${scoreClass}" title="Score: ${score}">
<span>&#9650;</span>${formatScore(score)}
</div>
${nsfwBadge}
${item.is_author_favorite ? '<div class="author-fav-badge" title="Autor favoritado">★ FAV</div>' : ''}
<img class="${thumbClass}" src="${thumbSrc}" loading="lazy"
<img class="gallery-thumb" src="${thumbSrc}" loading="lazy"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="${thumbClass}" style="display:none;align-items:center;justify-content:center;font-size:48px;">
<div class="gallery-thumb" style="display:none;align-items:center;justify-content:center;font-size:48px;">
${isVideo ? '🎬' : '🖼️'}
</div>
${isVideo ? '<div style="position:absolute;bottom:5px;left:5px;background:rgba(0,0,0,0.7);padding:2px 6px;border-radius:3px;font-size:11px;">VIDEO</div>' : ''}
@ -1731,50 +1726,9 @@ window.removeTagRelation = removeTagRelation;
// regardless of how many close paths we added. `/api/search` stays in the
// backend in case anything wants the universal typeahead by URL.)
// Alpine.store('prefs') — NSFW gate + discreet mode (Fase 1).
document.addEventListener('alpine:init', () => {
const stored = JSON.parse(localStorage.getItem('rmc:prefs') || '{}');
Alpine.store('prefs', {
showNsfw: stored.showNsfw ?? false,
discreet: stored.discreet ?? false,
toggleNsfw() {
this.showNsfw = !this.showNsfw;
this.persist();
},
toggleDiscreet() {
this.discreet = !this.discreet;
this.persist();
},
persist() {
localStorage.setItem('rmc:prefs', JSON.stringify({
showNsfw: this.showNsfw,
discreet: this.discreet,
}));
document.body.classList.toggle('show-nsfw', this.showNsfw);
document.body.classList.toggle('discreet', this.discreet);
},
init() { this.persist(); },
});
});
// Auto-discreet after 60s idle (mouse/keyboard stopped).
// Hands-off privacy: walk away, screen quietly downsizes thumbs.
(function autoDiscreet() {
const IDLE_MS = 60 * 1000;
let timer = null;
function arm() {
clearTimeout(timer);
timer = setTimeout(() => {
if (window.Alpine && Alpine.store('prefs') && !Alpine.store('prefs').discreet) {
Alpine.store('prefs').toggleDiscreet();
}
}, IDLE_MS);
}
['mousemove', 'keydown', 'click', 'scroll'].forEach(ev =>
document.addEventListener(ev, arm, { passive: true })
);
arm();
})();
// (NSFW blur gate + Discreet mode + auto-discreet were removed — the blur
// did not make sense for the solo home-NAS deploy. Backend keeps the
// `posts.nsfw` column and `/api/media?nsfw=` query param for filtering.)
// Keyboard shortcuts on gallery: j/k navigate, f favorite, b blacklist author,
// Esc closes modal or clears selection. Inactive while typing in inputs.

View file

@ -11,7 +11,7 @@
<script src="/static/js/api.js?v={{ app_version }}"></script>
<script src="/static/js/app.js?v={{ app_version }}" defer></script>
</head>
<body x-data x-init="$store.prefs.init()">
<body>
<div class="header">
<h1>Reddit Media Collector</h1>
<div class="tabs">
@ -21,16 +21,6 @@
<button class="tab-btn" onclick="switchTab('sources')">Fontes</button>
<button class="tab-btn" onclick="switchTab('tags')">Tags</button>
<button class="tab-btn" onclick="switchTab('settings')">Configuracoes</button>
<button class="tab-btn"
:title="$store.prefs.showNsfw ? 'NSFW visivel — clique para ocultar' : 'NSFW oculto — clique para mostrar'"
:style="$store.prefs.showNsfw ? 'color:#ff4500' : ''"
@click="$store.prefs.toggleNsfw()"
x-text="$store.prefs.showNsfw ? 'NSFW 👁' : 'NSFW 🙈'"></button>
<button class="tab-btn"
:title="$store.prefs.discreet ? 'Modo discreto ativo' : 'Modo discreto desativado'"
:style="$store.prefs.discreet ? 'color:#ff4500' : ''"
@click="$store.prefs.toggleDiscreet()"
x-text="$store.prefs.discreet ? 'Discreto 🤫' : 'Discreto 👁'"></button>
{% if pin_enabled %}
<button class="tab-btn" title="Trancar sessao agora"
onclick="fetch('/lock',{method:'POST'}).then(()=>location.href='/unlock')">
@ -39,7 +29,6 @@
{% endif %}
</div>
</div>
<div class="discreet-banner">Modo discreto ativo &mdash; clique em "Discreto 🤫" para desligar</div>
<div id="alert" class="alert"></div>

View file

@ -24,11 +24,6 @@
<option value="score_high">Maior Score</option>
<option value="score_low">Menor Score</option>
</select>
<select id="filter-nsfw" onchange="debouncedLoadGallery()">
<option value="all">NSFW: tudo</option>
<option value="hide">NSFW: ocultar</option>
<option value="only">NSFW: apenas</option>
</select>
<button class="btn-select-mode" id="btn-select-mode" onclick="toggleSelectMode()">
Selecionar
</button>