Merge branch 'feat/hide-comments-when-disabled'

This commit is contained in:
MechaCat02
2026-07-19 18:27:05 +02:00
11 changed files with 133 additions and 78 deletions

View File

@@ -302,6 +302,7 @@ pub async fn rebuild_export(
r.event_id, r.event_id,
r.event_name, r.event_name,
r.epoch, r.epoch,
state.config.comments_enabled,
std::time::Duration::ZERO, std::time::Duration::ZERO,
state.pool.clone(), state.pool.clone(),
state.config.media_path.clone(), state.config.media_path.clone(),
@@ -549,6 +550,7 @@ pub fn start_regen(state: &AppState, regen: crate::services::export::PendingRege
regen.event_id, regen.event_id,
regen.event_name, regen.event_name,
regen.epoch, regen.epoch,
state.config.comments_enabled,
// Debounced: a takedown pass is a burst, and each request retires the last generation. The // Debounced: a takedown pass is a burst, and each request retires the last generation. The
// delay lets superseded workers fail their claim and do zero work instead of each building // delay lets superseded workers fail their claim and do zero work instead of each building
// a full archive. See export::REGEN_DEBOUNCE. // a full archive. See export::REGEN_DEBOUNCE.
@@ -753,6 +755,7 @@ pub async fn release_gallery(
event_id, event_id,
event_name, event_name,
epoch, epoch,
state.config.comments_enabled,
std::time::Duration::ZERO, std::time::Duration::ZERO,
state.pool.clone(), state.pool.clone(),
state.config.media_path.clone(), state.config.media_path.clone(),

View File

@@ -58,6 +58,7 @@ async fn main() -> Result<()> {
pool.clone(), pool.clone(),
config.media_path.clone(), config.media_path.clone(),
config.export_path.clone(), config.export_path.clone(),
config.comments_enabled,
state.sse_tx.clone(), state.sse_tx.clone(),
) )
.await; .await;

View File

@@ -53,6 +53,9 @@ struct ViewerData {
struct ViewerEvent { struct ViewerEvent {
name: String, name: String,
exported_at: String, exported_at: String,
// Mirrors the live COMMENTS_ENABLED flag so the offline keepsake hides all comment
// UI (buttons, counts, sections) when the feature was off for the event.
comments_enabled: bool,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -238,6 +241,7 @@ pub async fn recover_exports(
pool: PgPool, pool: PgPool,
media_path: PathBuf, media_path: PathBuf,
export_path: PathBuf, export_path: PathBuf,
comments_enabled: bool,
sse_tx: broadcast::Sender<SseEvent>, sse_tx: broadcast::Sender<SseEvent>,
) { ) {
let rows = match sqlx::query_as::<_, (Uuid, String, i64)>( let rows = match sqlx::query_as::<_, (Uuid, String, i64)>(
@@ -297,6 +301,7 @@ pub async fn recover_exports(
event_id, event_id,
event_name, event_name,
epoch, epoch,
comments_enabled,
Duration::ZERO, Duration::ZERO,
pool.clone(), pool.clone(),
media_path.clone(), media_path.clone(),
@@ -403,6 +408,7 @@ pub fn spawn_export_jobs(
event_id: Uuid, event_id: Uuid,
event_name: String, event_name: String,
epoch: i64, epoch: i64,
comments_enabled: bool,
delay: Duration, delay: Duration,
pool: PgPool, pool: PgPool,
media_path: PathBuf, media_path: PathBuf,
@@ -439,6 +445,7 @@ pub fn spawn_export_jobs(
event_id, event_id,
epoch, epoch,
&event_name2, &event_name2,
comments_enabled,
&pool2, &pool2,
&media_path2, &media_path2,
&export_path2, &export_path2,
@@ -635,10 +642,12 @@ impl MediaSource {
} }
} }
#[allow(clippy::too_many_arguments)]
async fn run_html_export( async fn run_html_export(
event_id: Uuid, event_id: Uuid,
epoch: i64, epoch: i64,
event_name: &str, event_name: &str,
comments_enabled: bool,
pool: &PgPool, pool: &PgPool,
media_path: &Path, media_path: &Path,
export_path: &Path, export_path: &Path,
@@ -653,6 +662,7 @@ async fn run_html_export(
epoch, epoch,
event_id, event_id,
event_name, event_name,
comments_enabled,
pool, pool,
media_path, media_path,
export_path, export_path,
@@ -672,10 +682,12 @@ async fn run_html_export(
abandon_if_superseded("HTML", event_id, epoch, res) abandon_if_superseded("HTML", event_id, epoch, res)
} }
#[allow(clippy::too_many_arguments)]
async fn run_html_export_inner( async fn run_html_export_inner(
epoch: i64, epoch: i64,
event_id: Uuid, event_id: Uuid,
event_name: &str, event_name: &str,
comments_enabled: bool,
pool: &PgPool, pool: &PgPool,
media_path: &Path, media_path: &Path,
export_path: &Path, export_path: &Path,
@@ -882,6 +894,7 @@ async fn run_html_export_inner(
event: ViewerEvent { event: ViewerEvent {
name: event_name.to_string(), name: event_name.to_string(),
exported_at: Utc::now().to_rfc3339(), exported_at: Utc::now().to_rfc3339(),
comments_enabled,
}, },
posts: viewer_posts, posts: viewer_posts,
}; };

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,9 @@ export interface ViewerData {
event: { event: {
name: string; name: string;
exported_at: string; exported_at: string;
// Mirrors the live COMMENTS_ENABLED flag. Older exports predate this field, so
// treat a missing value as enabled (`?? true`) to preserve their comment UI.
comments_enabled?: boolean;
}; };
posts: ViewerPost[]; posts: ViewerPost[];
} }

View File

@@ -31,6 +31,9 @@
let posts = $derived(data?.posts ?? []); let posts = $derived(data?.posts ?? []);
// Mirror the live COMMENTS_ENABLED flag. Missing on older exports → treat as enabled.
let commentsEnabled = $derived(data?.event.comments_enabled ?? true);
let allTags = $derived.by(() => { let allTags = $derived.by(() => {
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- local throwaway counter inside a $derived.by; never stored in $state. // eslint-disable-next-line svelte/prefer-svelte-reactivity -- local throwaway counter inside a $derived.by; never stored in $state.
const freq = new Map<string, number>(); const freq = new Map<string, number>();
@@ -550,22 +553,24 @@
</svg> </svg>
{post.likes} {post.likes}
</span> </span>
<span class="flex items-center gap-1.5 text-sm font-medium text-gray-500"> {#if commentsEnabled}
<svg <span class="flex items-center gap-1.5 text-sm font-medium text-gray-500">
class="h-5 w-5" <svg
fill="none" class="h-5 w-5"
viewBox="0 0 24 24" fill="none"
stroke="currentColor" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
> stroke-width="2"
<path >
stroke-linecap="round" <path
stroke-linejoin="round" stroke-linecap="round"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" stroke-linejoin="round"
/> d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
</svg> />
{post.comments.length} </svg>
</span> {post.comments.length}
</span>
{/if}
</div> </div>
<!-- Caption --> <!-- Caption -->
@@ -641,17 +646,24 @@
</svg> </svg>
{post.likes} {post.likes}
</span> </span>
<span class="flex items-center gap-0.5"> {#if commentsEnabled}
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <span class="flex items-center gap-0.5">
<path <svg
stroke-linecap="round" class="h-3.5 w-3.5"
stroke-linejoin="round" fill="none"
stroke-width="2" viewBox="0 0 24 24"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" stroke="currentColor"
/> >
</svg> <path
{post.comments.length} stroke-linecap="round"
</span> stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
{post.comments.length}
</span>
{/if}
</div> </div>
</div> </div>
</div> </div>
@@ -784,23 +796,25 @@
</div> </div>
<!-- Comments list --> <!-- Comments list -->
<div class="flex-1 overflow-y-auto p-3"> {#if commentsEnabled}
{#if selectedPost.comments.length === 0} <div class="flex-1 overflow-y-auto p-3">
<p class="text-center text-sm text-gray-400">Keine Kommentare.</p> {#if selectedPost.comments.length === 0}
{:else} <p class="text-center text-sm text-gray-400">Keine Kommentare.</p>
<div class="space-y-3"> {:else}
{#each selectedPost.comments as comment, i (i)} <div class="space-y-3">
<div> {#each selectedPost.comments as comment, i (i)}
<span class="text-sm font-medium text-gray-900">{comment.author}</span> <div>
<span class="ml-1 text-sm text-gray-700">{comment.text}</span> <span class="text-sm font-medium text-gray-900">{comment.author}</span>
<div class="mt-0.5 text-xs text-gray-400"> <span class="ml-1 text-sm text-gray-700">{comment.text}</span>
{formatShortDate(comment.timestamp)} <div class="mt-0.5 text-xs text-gray-400">
{formatShortDate(comment.timestamp)}
</div>
</div> </div>
</div> {/each}
{/each} </div>
</div> {/if}
{/if} </div>
</div> {/if}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -6,6 +6,7 @@
import CameraCapture from '$lib/components/CameraCapture.svelte'; import CameraCapture from '$lib/components/CameraCapture.svelte';
import type { PendingFile } from '$lib/pending-upload-store'; import type { PendingFile } from '$lib/pending-upload-store';
import { eventState, uploadsClosed } from '$lib/event-state-store'; import { eventState, uploadsClosed } from '$lib/event-state-store';
import { commentsEnabled } from '$lib/event-config-store';
// Uploads closed (event locked or gallery released) — show a lock notice instead of // Uploads closed (event locked or gallery released) — show a lock notice instead of
// the capture options, so a guest can't stage a photo that would just be rejected. // the capture options, so a guest can't stage a photo that would just be rejected.
@@ -163,8 +164,9 @@
<div class="rounded-xl bg-amber-50 px-5 py-4 text-center dark:bg-amber-950/30"> <div class="rounded-xl bg-amber-50 px-5 py-4 text-center dark:bg-amber-950/30">
<p class="font-semibold text-amber-800 dark:text-amber-300">Uploads geschlossen</p> <p class="font-semibold text-amber-800 dark:text-amber-300">Uploads geschlossen</p>
<p class="mt-1 text-sm text-amber-700 dark:text-amber-400"> <p class="mt-1 text-sm text-amber-700 dark:text-amber-400">
Der Host hat die Uploads für dieses Event beendet. Du kannst weiterhin Fotos ansehen, Der Host hat die Uploads für dieses Event beendet. Du kannst weiterhin Fotos ansehen{$commentsEnabled
liken und kommentieren. ? ', liken und kommentieren'
: ' und liken'}.
</p> </p>
</div> </div>
<button onclick={close} class="btn btn-secondary btn-block"> Schließen </button> <button onclick={close} class="btn btn-secondary btn-block"> Schließen </button>

View File

@@ -22,6 +22,7 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import type { FeedUpload } from '$lib/types'; import type { FeedUpload } from '$lib/types';
import { dataMode } from '$lib/data-mode-store'; import { dataMode } from '$lib/data-mode-store';
import { commentsEnabled } from '$lib/event-config-store';
import { longpress } from '$lib/actions/longpress'; import { longpress } from '$lib/actions/longpress';
import FeedListCard from './FeedListCard.svelte'; import FeedListCard from './FeedListCard.svelte';
@@ -277,24 +278,31 @@
</svg> </svg>
{upload.like_count} {upload.like_count}
</button> </button>
<button {#if $commentsEnabled}
class="pointer-events-auto -m-1 flex items-center gap-0.5 p-1" <button
onclick={(e) => { class="pointer-events-auto -m-1 flex items-center gap-0.5 p-1"
e.stopPropagation(); onclick={(e) => {
oncomment(upload.id); e.stopPropagation();
}} oncomment(upload.id);
aria-label="Kommentare anzeigen" }}
> aria-label="Kommentare anzeigen"
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> >
<path <svg
stroke-linecap="round" class="h-4 w-4"
stroke-linejoin="round" fill="none"
stroke-width="2" viewBox="0 0 24 24"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" stroke="currentColor"
/> >
</svg> <path
{upload.comment_count} stroke-linecap="round"
</button> stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
{upload.comment_count}
</button>
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,12 @@
import Modal from '$lib/components/Modal.svelte'; import Modal from '$lib/components/Modal.svelte';
import IconButton from '$lib/components/IconButton.svelte'; import IconButton from '$lib/components/IconButton.svelte';
import { PRESETS, DEFAULT_SEED, buildPaletteCss, type ThemeConfig } from '$lib/theme/palette'; import { PRESETS, DEFAULT_SEED, buildPaletteCss, type ThemeConfig } from '$lib/theme/palette';
import { previewTheme, PALETTE_CACHE_KEY, loadEventConfig } from '$lib/event-config-store'; import {
previewTheme,
PALETTE_CACHE_KEY,
loadEventConfig,
commentsEnabled
} from '$lib/event-config-store';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
interface StatsDto { interface StatsDto {
@@ -644,12 +649,14 @@
</p> </p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p> <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
</div> </div>
<div class="card p-4 text-center"> {#if $commentsEnabled}
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100"> <div class="card p-4 text-center">
{stats.comment_count} <p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
</p> {stats.comment_count}
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Kommentare</p> </p>
</div> <p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Kommentare</p>
</div>
{/if}
<div class="card p-4 text-center"> <div class="card p-4 text-center">
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100"> <p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{diskPct(stats)} % {diskPct(stats)} %
@@ -1053,7 +1060,7 @@
onclick={() => onclick={() =>
(confirmAction = { (confirmAction = {
title: 'Sperre aufheben?', title: 'Sperre aufheben?',
message: `${user.display_name} kann danach wieder hochladen, liken und kommentieren.`, message: `${user.display_name} kann danach wieder hochladen${$commentsEnabled ? ', liken und kommentieren' : ' und liken'}.`,
confirmLabel: 'Entsperren', confirmLabel: 'Entsperren',
tone: 'default', tone: 'default',
run: () => unban(user) run: () => unban(user)

View File

@@ -7,6 +7,7 @@
import { toastError } from '$lib/toast-store'; import { toastError } from '$lib/toast-store';
import { focusTrap } from '$lib/actions/focus-trap'; import { focusTrap } from '$lib/actions/focus-trap';
import IconButton from '$lib/components/IconButton.svelte'; import IconButton from '$lib/components/IconButton.svelte';
import { commentsEnabled } from '$lib/event-config-store';
interface JobStatus { interface JobStatus {
status: 'locked' | 'pending' | 'running' | 'done' | 'failed'; status: 'locked' | 'pending' | 'running' | 'done' | 'failed';
@@ -266,7 +267,8 @@
<div class="min-w-0"> <div class="min-w-0">
<h2 class="section-title">HTML-Viewer</h2> <h2 class="section-title">HTML-Viewer</h2>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400"> <p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
Schöne Offline-Galerie mit Filterung, Kommentaren und Likes — kein Internet nötig. Schöne Offline-Galerie mit Filterung{$commentsEnabled ? ', Kommentaren' : ''} und Likes
— kein Internet nötig.
</p> </p>
<p <p
class="mt-1 text-xs {status.html.status === 'done' class="mt-1 text-xs {status.html.status === 'done'

View File

@@ -9,6 +9,7 @@
import ConfirmSheet from '$lib/components/ConfirmSheet.svelte'; import ConfirmSheet from '$lib/components/ConfirmSheet.svelte';
import Modal from '$lib/components/Modal.svelte'; import Modal from '$lib/components/Modal.svelte';
import IconButton from '$lib/components/IconButton.svelte'; import IconButton from '$lib/components/IconButton.svelte';
import { commentsEnabled } from '$lib/event-config-store';
interface UserSummary { interface UserSummary {
id: string; id: string;
@@ -448,9 +449,10 @@
Benutzer sperren Benutzer sperren
</h2> </h2>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400"> <p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
<strong>{banTarget.display_name}</strong> wird gesperrt: alle Uploads verschwinden aus Galerie, <strong>{banTarget.display_name}</strong> wird gesperrt: alle Uploads verschwinden aus
Diashow und Export, und Hochladen, Liken und Kommentieren werden blockiert. Der Lesezugriff (Feed Galerie, Diashow und Export, und Hochladen, Liken{$commentsEnabled ? ' und Kommentieren' : ''} werden
ansehen, Keepsake herunterladen) bleibt bestehen. Rückgängig machbar über „Entsperren“. blockiert. Der Lesezugriff (Feed ansehen, Keepsake herunterladen) bleibt bestehen. Rückgängig machbar
über „Entsperren“.
</p> </p>
<div class="flex gap-2"> <div class="flex gap-2">
<button onclick={() => (banTarget = null)} class="btn btn-secondary flex-1"> <button onclick={() => (banTarget = null)} class="btn btn-secondary flex-1">
@@ -823,7 +825,7 @@
onclick={() => onclick={() =>
(confirmAction = { (confirmAction = {
title: 'Sperre aufheben?', title: 'Sperre aufheben?',
message: `${user.display_name} kann danach wieder hochladen, liken und kommentieren.`, message: `${user.display_name} kann danach wieder hochladen${$commentsEnabled ? ', liken und kommentieren' : ' und liken'}.`,
confirmLabel: 'Entsperren', confirmLabel: 'Entsperren',
tone: 'default', tone: 'default',
run: () => unban(user) run: () => unban(user)