diff --git a/README.md b/README.md
index 230734f..d2026b4 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ bun run content
| `f` | Toggle featured on a project |
| `p` | Preview a theme in the editor |
| `t` | Switch the editor to the next theme |
-| `s` | Move a book to another shelf, or make a theme the site default |
+| `s` | Move a book to another shelf, set a show or movie to watched, watching or planning, or make a theme the site default |
| `d` | Delete |
| `K` / `J` | Move a song or entry up or down |
| `u` / `U` | Refresh the selected book, or every book, from Goodreads |
diff --git a/src/lib/btop/hobbies.ts b/src/lib/btop/hobbies.ts
index 03f176b..7850601 100644
--- a/src/lib/btop/hobbies.ts
+++ b/src/lib/btop/hobbies.ts
@@ -61,12 +61,14 @@ export function bookStats(books: Book[]): BookStats | null {
export function screenStats(titles: RatedTitle[]): ScreenStats {
const ratings = titles.flatMap((title) => (title.rating === undefined ? [] : [title.rating]));
const watching = titles.filter((title) => title.status === 'watching').map((title) => title.name);
+ const planningCount = titles.filter((title) => title.status === 'planning').length;
const groups = new Set(titles.flatMap((title) => (title.collection ? [title.collection] : [])));
return {
rows: [
{ label: 'Titles', value: `${titles.length}` },
{ label: 'Watching', value: `${watching.length}` },
+ { label: 'Planning', value: `${planningCount}` },
{ label: 'Groups', value: `${groups.size}` },
{ label: 'Avg rating', value: average(ratings) }
],
diff --git a/src/lib/components/about/MediaCard.svelte b/src/lib/components/about/MediaCard.svelte
index 59d4164..7ac02f5 100644
--- a/src/lib/components/about/MediaCard.svelte
+++ b/src/lib/components/about/MediaCard.svelte
@@ -22,7 +22,9 @@
{/if}
{#if item.status === 'watching'}
- watching
+ watching
+ {:else if item.status === 'planning'}
+ planning
{/if}
@@ -89,7 +91,7 @@
font-size: 0.78rem;
}
- .watching {
+ .status {
position: absolute;
bottom: 0.4rem;
left: 0.4rem;
@@ -97,12 +99,19 @@
align-items: center;
gap: 0.2rem;
padding: 0 0.4rem;
- background: var(--blue);
color: var(--background);
font-size: 0.75rem;
font-weight: 700;
}
+ .status.watching {
+ background: var(--blue);
+ }
+
+ .status.planning {
+ background: var(--magenta);
+ }
+
.text {
display: flex;
flex-direction: column;
diff --git a/src/lib/components/about/media/MediaCollectionGroup.svelte b/src/lib/components/about/media/MediaCollectionGroup.svelte
index 5f3245e..cc52e06 100644
--- a/src/lib/components/about/media/MediaCollectionGroup.svelte
+++ b/src/lib/components/about/media/MediaCollectionGroup.svelte
@@ -1,6 +1,6 @@
@@ -32,6 +33,9 @@
{#if watchingCount}
{watchingCount} watching
{/if}
+ {#if planningCount}
+ {planningCount} planning
+ {/if}
{#if average !== null}
avg {average.toFixed(1)}/10
{/if}
diff --git a/src/lib/components/about/media/MediaSection.svelte b/src/lib/components/about/media/MediaSection.svelte
index cdc9a2f..d46d0e6 100644
--- a/src/lib/components/about/media/MediaSection.svelte
+++ b/src/lib/components/about/media/MediaSection.svelte
@@ -4,7 +4,6 @@
import type { MediaDetails } from '#lib/media/types';
import { groupMedia } from '#lib/media/collections';
import MediaGrid from '../MediaGrid.svelte';
- import MediaCard from '../MediaCard.svelte';
import MediaCollectionGroup from './MediaCollectionGroup.svelte';
let {
@@ -17,15 +16,6 @@
const collectionLabel = $derived(kind === 'show' ? 'Series' : 'Collections');
-{#if grouped.watching.length}
-
Currently watching {grouped.watching.length}
-
- {#each grouped.watching as item (item.url)}
-
- {/each}
-
-{/if}
-
{#if grouped.collections.length}
{collectionLabel} {grouped.collections.length}
@@ -36,8 +26,8 @@
{/if}
{#if grouped.standalone.length}
- {#if grouped.watching.length || grouped.collections.length}
-
Watched {grouped.standalone.length}
+ {#if grouped.collections.length}
+ All {kind === 'show' ? 'shows' : 'movies'} {grouped.standalone.length}
{/if}
{/if}
@@ -56,22 +46,10 @@
margin-top: 0;
}
- .watching-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
- gap: 1rem 0.85rem;
- }
-
.collection-list {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.75rem;
}
-
- @container (max-width: 520px) {
- .watching-grid {
- grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
- }
- }
diff --git a/src/lib/content/types.ts b/src/lib/content/types.ts
index f0c6f52..61eed6e 100644
--- a/src/lib/content/types.ts
+++ b/src/lib/content/types.ts
@@ -133,7 +133,7 @@ export interface Game {
url: string;
}
-export type WatchStatus = 'watched' | 'watching';
+export type WatchStatus = 'watched' | 'watching' | 'planning';
export interface RatedTitle {
name: string;
diff --git a/src/lib/media/collections.ts b/src/lib/media/collections.ts
index 090240c..4d5610c 100644
--- a/src/lib/media/collections.ts
+++ b/src/lib/media/collections.ts
@@ -7,19 +7,35 @@ export interface MediaCollection {
}
export interface GroupedMedia {
- watching: RatedTitle[];
collections: MediaCollection[];
standalone: RatedTitle[];
}
+const statusOrder = { watching: 0, watched: 1, planning: 2 };
+
export function isWatching(item: RatedTitle): boolean {
return item.status === 'watching';
}
+export function isPlanning(item: RatedTitle): boolean {
+ return item.status === 'planning';
+}
+
+export function statusTone(item: RatedTitle): string {
+ if (isWatching(item)) return 'blue';
+ if (isPlanning(item)) return 'magenta';
+ return 'green';
+}
+
export function ratingOf(item: RatedTitle): number {
return item.rating ?? -1;
}
+function standaloneOrder(first: RatedTitle, second: RatedTitle): number {
+ const statusDifference = statusOrder[first.status ?? 'watched'] - statusOrder[second.status ?? 'watched'];
+ return statusDifference || ratingOf(second) - ratingOf(first);
+}
+
function releaseYear(item: RatedTitle, details: Record): number {
const year = Number.parseInt(details[item.url]?.year ?? '', 10);
return Number.isNaN(year) ? Number.MAX_SAFE_INTEGER : year;
@@ -37,7 +53,7 @@ export function groupMedia(items: RatedTitle[], details: Record averageRating(second.items) - averageRating(first.items) || first.name.localeCompare(second.name));
return {
- watching: items.filter(isWatching),
collections,
- standalone: [...standalone].sort((first, second) => ratingOf(second) - ratingOf(first))
+ standalone: [...standalone].sort(standaloneOrder)
};
}
diff --git a/tools/content/media.py b/tools/content/media.py
index df5d2cf..f1f260b 100644
--- a/tools/content/media.py
+++ b/tools/content/media.py
@@ -6,7 +6,8 @@ MEDIA_KINDS = {
"show": {"key": "shows", "label": "TV show", "omdb_type": "series"},
"movie": {"key": "movies", "label": "movie", "omdb_type": "movie"},
}
-STATUSES = ["watched", "watching"]
+STATUSES = ["watched", "watching", "planning"]
+STATUS_ORDER = {"watching": 0, "watched": 1, "planning": 2}
def check_rating(rating: int | None) -> int | None:
@@ -33,7 +34,7 @@ def status_of(item: dict) -> str:
def list_items(kind: str) -> list[dict]:
_, items = load_items(kind)
rating = lambda item: item["rating"] if item.get("rating") is not None else -1
- return sorted(items, key=lambda item: (status_of(item) != "watching", -rating(item)))
+ return sorted(items, key=lambda item: (STATUS_ORDER[status_of(item)], -rating(item)))
def collection_names(kind: str) -> list[str]:
diff --git a/tools/content/tui/media_pane.py b/tools/content/tui/media_pane.py
index 801d834..dc8e8f4 100644
--- a/tools/content/tui/media_pane.py
+++ b/tools/content/tui/media_pane.py
@@ -8,7 +8,7 @@ import media
from tui.content_pane import ContentPane
from tui.dialogs import ConfirmDialog, FormDialog, FormField
-STATUS_STYLES = {"watching": "blue", "watched": "green"}
+STATUS_STYLES = {"watching": "blue", "watched": "green", "planning": "magenta"}
def rating_text(item: dict) -> str:
@@ -19,7 +19,7 @@ class MediaPane(ContentPane):
BINDINGS = [
Binding("a", "add", "Add"),
Binding("e", "rate", "Rate"),
- Binding("s", "status", "Watching"),
+ Binding("s", "status", "Status"),
Binding("g", "collection", "Group"),
Binding("d", "delete", "Delete"),
Binding("r", "reload", "Reload"),
@@ -113,8 +113,14 @@ class MediaPane(ContentPane):
if not item:
return
identifier = imdb.imdb_id(item["url"])
- next_status = "watched" if media.status_of(item) == "watching" else "watching"
- self.run_now(lambda: media.set_status(self.kind, identifier, next_status), f"{item['name']} is now {next_status}", identifier)
+ fields = [FormField("status", f"Status for {item['name']}", kind="select", options=media.STATUSES, value=media.status_of(item))]
+
+ def changed(values: dict | None):
+ if values:
+ status = values["status"]
+ self.run_now(lambda: media.set_status(self.kind, identifier, status), f"{item['name']} is now {status}", identifier)
+
+ self.app.push_screen(FormDialog("Change status", fields, "Save"), changed)
def action_collection(self):
item = self.selected_item()