Add planning to shows and movies in one merged grid

This commit is contained in:
SirBlob committed 2026-11-01 22:12:39 -05:00
1 parent cd9a8a186a
commit d473aebb79
9 files changed
+56 -41

No files matched your search

+2
View File
@@ -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) }
],
+12 -3
View File
@@ -22,7 +22,9 @@
{/if}
{#if item.status === 'watching'}
<span class="watching"><Icon icon="mdi:play" width="13" />watching</span>
<span class="status watching"><Icon icon="mdi:play" width="13" />watching</span>
{:else if item.status === 'planning'}
<span class="status planning"><Icon icon="mdi:bookmark-outline" width="13" />planning</span>
{/if}
</div>
@@ -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;
@@ -1,6 +1,6 @@
<script lang="ts">
import type { MediaCollection } from '#lib/media/collections';
import { isWatching } from '#lib/media/collections';
import { isPlanning, isWatching, statusTone } from '#lib/media/collections';
import type { MediaDetails } from '#lib/media/types';
import CollectionGroup from '#lib/components/ui/CollectionGroup.svelte';
import MediaCard from '../MediaCard.svelte';
@@ -13,6 +13,7 @@
const icon = $derived(kind === 'show' ? 'mdi:television-classic' : 'mdi:movie-open-outline');
const watchingCount = $derived(collection.items.filter(isWatching).length);
const planningCount = $derived(collection.items.filter(isPlanning).length);
const rated = $derived(collection.items.filter((item) => item.rating !== undefined));
const average = $derived(rated.length ? rated.reduce((total, item) => total + (item.rating ?? 0), 0) / rated.length : null);
const covers = $derived(
@@ -21,7 +22,7 @@
href: item.url,
title: item.name,
image: mediaDetails[item.url]?.poster ?? null,
tone: isWatching(item) ? 'blue' : 'green'
tone: statusTone(item)
}))
);
</script>
@@ -32,6 +33,9 @@
{#if watchingCount}
<span style:color="var(--blue)">{watchingCount} watching</span>
{/if}
{#if planningCount}
<span style:color="var(--magenta)">{planningCount} planning</span>
{/if}
{#if average !== null}
<span style:color="var(--yellow)">avg {average.toFixed(1)}/10</span>
{/if}
@@ -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');
</script>
{#if grouped.watching.length}
<h3 class="subheading"><Icon icon="mdi:play-circle-outline" width="16" /> Currently watching <span class="muted">{grouped.watching.length}</span></h3>
<div class="watching-grid">
{#each grouped.watching as item (item.url)}
<MediaCard {item} details={mediaDetails[item.url]} {kind} />
{/each}
</div>
{/if}
{#if grouped.collections.length}
<h3 class="subheading"><Icon icon="mdi:layers-outline" width="16" /> {collectionLabel} <span class="muted">{grouped.collections.length}</span></h3>
<div class="collection-list">
@@ -36,8 +26,8 @@
{/if}
{#if grouped.standalone.length}
{#if grouped.watching.length || grouped.collections.length}
<h3 class="subheading"><Icon icon="mdi:check-circle-outline" width="16" /> Watched <span class="muted">{grouped.standalone.length}</span></h3>
{#if grouped.collections.length}
<h3 class="subheading"><Icon icon="mdi:view-grid-outline" width="16" /> All {kind === 'show' ? 'shows' : 'movies'} <span class="muted">{grouped.standalone.length}</span></h3>
{/if}
<MediaGrid items={grouped.standalone} {mediaDetails} {kind} />
{/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));
}
}
</style>
+1 -1
View File
@@ -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;
+19 -4
View File
@@ -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<string, MediaDetails>): 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<string, MediaDet
for (const item of items) {
if (item.collection) {
groups.set(item.collection, [...(groups.get(item.collection) ?? []), item]);
} else if (!isWatching(item)) {
} else {
standalone.push(item);
}
}
@@ -50,8 +66,7 @@ export function groupMedia(items: RatedTitle[], details: Record<string, MediaDet
.sort((first, second) => 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)
};
}