From df7503eb9b6a951b1019cb5a4bd86c4341576072 Mon Sep 17 00:00:00 2001
From: Ismael Leon
Date: Sun, 6 Sep 2026 03:03:19 -0600
Subject: [PATCH] =?UTF-8?q?feat(watchlist):=20stop=20a=20title=20being=20m?=
=?UTF-8?q?arked=20watched=20before=20it=20is=20out=20"Watched"=20sat=20on?=
=?UTF-8?q?=20a=20film=20that=20opens=20next=20spring,=20which=20is=20eith?=
=?UTF-8?q?er=20a=20mistake=20waiting=20to=20happen=20or=20a=20note=20the?=
=?UTF-8?q?=20list=20has=20no=20way=20to=20interpret.=20The=20app=20alread?=
=?UTF-8?q?y=20refuses=20to=20tick=20a=20show=20past=20its=20aired=20seaso?=
=?UTF-8?q?ns;=20this=20is=20the=20same=20rule=20for=20the=20other=20half?=
=?UTF-8?q?=20of=20the=20library.=20Where=20the=20button=20was,=20the=20ca?=
=?UTF-8?q?rd=20now=20says=20when=20the=20title=20lands=20=E2=80=94=20"Out?=
=?UTF-8?q?=20Sep=209",=20"Premieres=20Nov=205".=20Not=20a=20disabled=20bu?=
=?UTF-8?q?tton:=20a=20greyed=20control=20still=20reads=20as=20an=20action?=
=?UTF-8?q?=20you=20are=20failing=20to=20perform=20and=20invites=20the=20t?=
=?UTF-8?q?ap=20that=20does=20nothing,=20while=20a=20date=20answers=20the?=
=?UTF-8?q?=20question=20the=20button=20was=20standing=20in=20front=20of.?=
=?UTF-8?q?=20The=20remove=20button=20stays,=20because=20changing=20your?=
=?UTF-8?q?=20mind=20about=20waiting=20for=20something=20is=20not=20the=20?=
=?UTF-8?q?same=20as=20claiming=20to=20have=20seen=20it.=20Only=20a=20conf?=
=?UTF-8?q?irmed=20future=20date=20refuses.=20A=20title=20TMDB=20holds=20n?=
=?UTF-8?q?o=20date=20for=20is=20still=20tickable,=20because=20that=20stat?=
=?UTF-8?q?e=20means=20two=20opposite=20things=20=E2=80=94=20a=20productio?=
=?UTF-8?q?n=20announced=20years=20out,=20and=20an=20obscure=20catalogue?=
=?UTF-8?q?=20title=20nobody=20has=20dated=20=E2=80=94=20and=20refusing=20?=
=?UTF-8?q?on=20it=20would=20lock=20a=20film=20somebody=20watched=20decade?=
=?UTF-8?q?s=20ago=20on=20the=20strength=20of=20a=20missing=20field.=20Un-?=
=?UTF-8?q?watching=20is=20never=20blocked=20either,=20so=20no=20row=20is?=
=?UTF-8?q?=20left=20claiming=20something=20its=20owner=20cannot=20take=20?=
=?UTF-8?q?back.=20The=20block=20is=20only=20as=20good=20as=20the=20date?=
=?UTF-8?q?=20behind=20it,=20and=20a=20film's=20date=20was=20snapshotted?=
=?UTF-8?q?=20on=20save=20and=20never=20revised:=20a=20studio=20moving=20a?=
=?UTF-8?q?=20release=20forward=20would=20have=20left=20the=20card=20refus?=
=?UTF-8?q?ing=20forever,=20with=20removing=20and=20re-saving=20the=20only?=
=?UTF-8?q?=20way=20out.=20So=20the=20read=20path=20now=20re-asks=20about?=
=?UTF-8?q?=20titles=20that=20have=20not=20come=20out,=20the=20way=20it=20?=
=?UTF-8?q?already=20re-asks=20about=20seasons.=20That=20also=20fixes=20tw?=
=?UTF-8?q?o=20places=20the=20stale=20date=20was=20already=20wrong=20?=
=?UTF-8?q?=E2=80=94=20the=20Upcoming=20grouping,=20and=20the=20calendar?=
=?UTF-8?q?=20feed,=20which=20was=20publishing=20slipped=20premieres=20int?=
=?UTF-8?q?o=20people's=20calendars=20as=20appointments=20for=20a=20night?=
=?UTF-8?q?=20with=20nothing=20on.=20Two=20things=20found=20while=20drivin?=
=?UTF-8?q?g=20it=20in=20a=20browser:=20"In=20theaters=20In=203=20days"=20?=
=?UTF-8?q?read=20like=20a=20typo,=20so=20the=20line=20takes=20the=20date?=
=?UTF-8?q?=20and=20leaves=20the=20countdown=20to=20the=20poster=20badge?=
=?UTF-8?q?=20above=20it;=20and=20a=20far-off=20date=20carries=20its=20yea?=
=?UTF-8?q?r,=20which=20does=20not=20fit=20a=20tile=20on=20a=20five-column?=
=?UTF-8?q?=20grid,=20so=20the=20line=20wraps=20rather=20than=20truncating?=
=?UTF-8?q?=20the=20year=20off=20the=20end=20of=20it.=20Both=20are=20pinne?=
=?UTF-8?q?d=20by=20a=20measuring=20test.=20Coming=20Soon=20also=20called?=
=?UTF-8?q?=20a=20film's=20release=20a=20premiere,=20which=20only=20became?=
=?UTF-8?q?=20visible=20once=20the=20card=20above=20it=20started=20saying?=
=?UTF-8?q?=20"Out".?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/lib/components/media/ComingSoon.svelte | 10 +-
src/lib/components/media/WatchlistCard.svelte | 83 ++++++++---
.../media/WatchlistCard.svelte.spec.ts | 135 ++++++++++++++++++
src/lib/domain/release.spec.ts | 75 +++++++++-
src/lib/domain/release.ts | 79 +++++++++-
src/lib/server/watchlist/actions.spec.ts | 33 +++++
src/lib/server/watchlist/actions.ts | 17 +++
src/lib/server/watchlist/index.ts | 2 +-
src/lib/server/watchlist/releases.ts | 37 +++++
src/lib/server/watchlist/upkeep.spec.ts | 96 ++++++++++++-
src/lib/server/watchlist/upkeep.ts | 63 ++++++++
src/routes/watchlist/+page.server.ts | 18 ++-
12 files changed, 619 insertions(+), 29 deletions(-)
create mode 100644 src/lib/components/media/WatchlistCard.svelte.spec.ts
create mode 100644 src/lib/server/watchlist/releases.ts
diff --git a/src/lib/components/media/ComingSoon.svelte b/src/lib/components/media/ComingSoon.svelte
index 2f6c72a..c300a49 100644
--- a/src/lib/components/media/ComingSoon.svelte
+++ b/src/lib/components/media/ComingSoon.svelte
@@ -4,6 +4,7 @@
import PosterGrid from './PosterGrid.svelte';
import WatchlistCard from './WatchlistCard.svelte';
import { groupByUpcomingWindow } from '$lib/domain/upcoming';
+ import { releaseVerb } from '$lib/domain/release';
import type { WatchlistItem } from '$lib/server/db/schema';
/**
@@ -80,7 +81,14 @@
{#if upcoming.kind === 'season'}
Season {upcoming.seasonNumber}
{:else}
- Premieres
+
+ {releaseVerb(item.mediaType)}
{/if}
· {upcoming.date ? upcoming.fullDate : 'date not announced'}
diff --git a/src/lib/components/media/WatchlistCard.svelte b/src/lib/components/media/WatchlistCard.svelte
index 749a84a..48773dc 100644
--- a/src/lib/components/media/WatchlistCard.svelte
+++ b/src/lib/components/media/WatchlistCard.svelte
@@ -6,7 +6,7 @@
import SeasonTracker from './SeasonTracker.svelte';
import { getSeasonProgress } from '$lib/domain/progress';
import { progressNote } from '$lib/domain/episodes';
- import { getReleaseInfo } from '$lib/domain/release';
+ import { canMarkWatched, getReleaseInfo, pendingReleaseLabel } from '$lib/domain/release';
import {
daysUntilDeletion,
shouldWarnAboutDeletion,
@@ -58,8 +58,27 @@
const progress = $derived(getSeasonProgress(item));
- // "Mark as watched" still works on an unreleased title (premieres exist), but
- // it shouldn't be the loudest thing on a card for something that isn't out.
+ /**
+ * What replaces the "Watched" button on a title that is not out yet, or null
+ * when there is nothing to wait for.
+ *
+ * The button used to sit here, quieter but still pressable, which meant a film
+ * opening next spring could be filed as seen. Naming the date instead is not
+ * only a refusal — it is the answer to the question the button was standing in
+ * front of. `canMarkWatched` holds the rule and the reason it stops at
+ * confirmed dates.
+ *
+ * Only ever shown for a title that is *not* already marked watched. A row that
+ * predates this rule keeps its way back out; the rule governs the way in.
+ */
+ const pendingRelease = $derived(
+ !item.watched && !canMarkWatched(item.releaseDate)
+ ? pendingReleaseLabel(item.mediaType, item.releaseDate)
+ : null
+ );
+
+ // A title with no date at all is still tickable — see `canMarkWatched` — but
+ // it should not wear the loudest button on the card either.
const unreleased = $derived(getReleaseInfo(item.releaseDate).state !== 'released');
/**
@@ -120,21 +139,51 @@
{:else}
-
+
+
+
{pendingRelease}
+
+ {:else}
+
+ {/if}