Initial commit

This commit is contained in:
2026-07-15 10:34:32 +02:00
committed by david.alvarez
commit 5861da89b3
38 changed files with 1711 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>DiscDrop Artists</title>
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg"/>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/[email protected]"></script>
<link rel="stylesheet" href="/static/css/app.css"/>
<script src="/static/js/app.js" defer></script>
</head>
<body>
<header class="sticky top-0 z-30 bg-base-100/90 backdrop-blur border-b border-base-300">
<div class="navbar flex flex-wrap justify-center gap-4 px-4 py-3 max-w-5xl mx-auto">
<div class="flex items-center gap-2">
<img src="/static/img/logo.svg" alt="DiscDrop logo" class="h-9 w-9"/>
<span class="text-xl font-bold">DiscDrop</span>
</div>
<div class="flex items-center gap-2">
<a href="/" class="btn btn-ghost btn-sm">Feed</a>
<button id="theme-switcher" class="btn btn-ghost btn-circle" aria-label="Toggle theme" onclick="DiscDrop.toggleTheme()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
</button>
</div>
</div>
</header>
<main class="max-w-5xl mx-auto px-4 py-6 space-y-3">
<h1 class="text-2xl font-bold mb-2">Followed Artists</h1>
{#if artists.empty}
<div class="text-center text-base-content/60 py-12">
<p>You're not following any artists yet.</p>
<a href="/" class="link">Go to the feed to search and follow artists.</a>
</div>
{#else}
{#for artist in artists}
<div hx-get="/artists/{artist.mbid}/row" hx-trigger="revealed" hx-swap="innerHTML">
<div class="loading loading-spinner loading-sm"></div>
</div>
{/for}
{/if}
</main>
</body>
</html>
@@ -0,0 +1,36 @@
<div class="bg-base-100 rounded-box border border-base-300 p-3 shadow-sm space-y-3">
<div class="flex flex-wrap items-center gap-3">
<div class="flex-1 min-w-0">
<div class="font-semibold truncate">{artist.name}</div>
<div class="text-xs text-base-content/60 truncate">
{#if artist.disambiguation??}{artist.disambiguation}{#else}Artist{/if}
{#if artist.areaName??}<span class="badge badge-ghost badge-xs ml-1">{artist.areaName}</span>{/if}
</div>
</div>
<button class="btn btn-ghost btn-xs text-error"
hx-delete="/artists/{artist.mbid}"
hx-target="closest div.space-y-3"
hx-swap="outerHTML"
hx-on::after-request="DiscDrop.refreshFeed()">
Unfollow
</button>
</div>
<div class="flex flex-wrap gap-2">
{#for s in settings}
<label class="label cursor-pointer gap-1 badge badge-outline py-2">
<input type="checkbox"
class="checkbox checkbox-xs"
value="true"
{#if s.enabled}checked{/if}
hx-post="/artists/{artist.mbid}/types"
hx-vals='{"primaryType":"{s.primaryType}"}'
hx-include="this"
hx-target="#feed-list"
hx-swap="innerHTML"
hx-on::after-request="DiscDrop.refreshFeed()"/>
<span class="text-xs">{s.primaryType}</span>
</label>
{/for}
</div>
</div>
@@ -0,0 +1,18 @@
{#for row in rows}
{#include fragments/feed-row.html row=row /}
{/for}
{#if hasMore}
<button id="load-more"
class="btn btn-outline btn-block mt-2"
hx-get="/feed?offset={nextOffset}"
hx-target="this"
hx-swap="outerHTML">
Load more
</button>
{/if}
{#if !hasMore && rows.empty}
<div class="text-center text-base-content/60 py-12">
<p class="text-lg">No releases yet.</p>
<p class="text-sm">Search for an artist above and click <strong>Follow</strong> to start tracking their release groups.</p>
</div>
{/if}
@@ -0,0 +1,22 @@
<div class="flex gap-3 items-center bg-base-100 rounded-box border border-base-300 p-3 shadow-sm">
<img src="https://coverartarchive.org/release-group/{row.mbid}/front"
alt="cover"
loading="lazy"
class="w-16 h-16 rounded object-cover bg-base-200"
onerror="this.onerror=null;this.src='/static/img/no-cover.svg'"/>
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class="font-semibold truncate">{row.artist.name}</span>
{#if row.primaryType??}<span class="badge badge-primary badge-sm">{row.primaryType}</span>{/if}
{#if row.secondaryTypes??}<span class="badge badge-ghost badge-sm">{row.secondaryTypes}</span>{/if}
</div>
<a href="{row.mbzUrl}" target="_blank" rel="noopener" class="link link-hover text-base-content/90 block truncate">
{row.title}
</a>
</div>
<div class="text-right text-sm text-base-content/70 shrink-0">
{#if row.firstReleaseDateRaw??}{row.firstReleaseDateRaw}{#else}—{/if}
</div>
</div>
@@ -0,0 +1,39 @@
{#if results.empty}
{#if query?? && query.length() >= 2}
<div class="p-3 text-sm text-base-content/60">No artists found for "{query}".</div>
{/if}
{/if}
{#for a in results}
<div class="flex items-center gap-2 p-2 hover:bg-base-200 rounded">
<div class="flex-1 min-w-0">
<div class="font-medium truncate">{a.name}</div>
<div class="text-xs text-base-content/60 truncate">
{#if a.disambiguation??}{a.disambiguation}{#else}Artist{/if}
{#if a.area?? && a.area.name??}<span class="badge badge-ghost badge-xs ml-1">{a.area.name}</span>{/if}
</div>
</div>
{#if followedMbids.contains(a.id)}
<form hx-delete="/artists/{a.id}"
hx-target="#feed-list"
hx-swap="innerHTML"
hx-on::after-request="DiscDrop.clearSearch()">
<button type="submit" class="btn btn-ghost btn-xs text-error">Unfollow</button>
</form>
{#else}
<form hx-post="/artists/follow"
hx-target="#feed-list"
hx-swap="innerHTML"
hx-on::after-request="DiscDrop.clearSearch()">
<input type="hidden" name="mbid" value="{a.id}"/>
<input type="hidden" name="name" value="{a.name}"/>
<input type="hidden" name="disambiguation" value="{a.disambiguation}"/>
<input type="hidden" name="type" value="{a.type}"/>
<input type="hidden" name="country" value="{a.country}"/>
{#if a.area?? && a.area.name??}
<input type="hidden" name="area" value="{a.area.name}"/>
{/if}
<button type="submit" class="btn btn-primary btn-xs">Follow</button>
</form>
{/if}
</div>
{/for}
@@ -0,0 +1,39 @@
<div class="space-y-3">
<h3 class="font-bold text-lg">Settings</h3>
{#if flash == "saved"}
<div class="alert alert-success py-1 text-sm">Saved</div>
{/if}
<form hx-post="/settings" hx-target="#settings-panel-host" hx-swap="innerHTML" class="space-y-3">
<div>
<div class="text-sm font-semibold mb-1">Default primary types for new artists</div>
<div class="flex flex-wrap gap-2">
{#for t in allPrimaryTypes}
<label class="label cursor-pointer gap-1 badge badge-outline py-2">
<input type="checkbox" name="defaultPrimaryTypes" value="{t}"
class="checkbox checkbox-xs"
{#if defaultPrimaryTypes.contains(t)}checked{/if}/>
<span class="text-xs">{t}</span>
</label>
{/for}
</div>
</div>
<div>
<div class="text-sm font-semibold mb-1">Feed update schedule</div>
<div class="flex gap-2">
{#for h in scheduleOptions}
<label class="label cursor-pointer gap-1">
<input type="radio" name="syncScheduleHours" value="{h}"
class="radio radio-sm"
{#if syncScheduleHours == h}checked{/if}/>
<span class="text-xs">{h}h</span>
</label>
{/for}
</div>
</div>
<button class="btn btn-primary btn-sm w-full">Save</button>
</form>
</div>
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>DiscDrop</title>
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg"/>
<link rel="alternate" type="application/rss+xml" title="DiscDrop New Releases" href="/rss"/>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.min.css" rel="stylesheet" type="text/css"/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/[email protected]"></script>
<link rel="stylesheet" href="/static/css/app.css"/>
<script src="/static/js/app.js" defer></script>
</head>
<body>
<header class="sticky top-0 z-30 bg-base-100/90 backdrop-blur border-b border-base-300">
<div class="navbar flex flex-wrap justify-center gap-4 px-4 py-3 max-w-5xl mx-auto">
<div class="flex items-center gap-2">
<img src="/static/img/logo.svg" alt="DiscDrop logo" class="h-9 w-9"/>
<span class="text-xl font-bold">DiscDrop</span>
</div>
<div id="search-wrap" class="relative flex-1 min-w-[220px] max-w-xl">
<form id="search-form" class="join w-full" autocomplete="off">
<input
id="search-input"
type="search"
name="q"
placeholder="Search MusicBrainz artists…"
class="input input-bordered join-item w-full"
hx-get="/search/artists"
hx-trigger="input changed delay:300ms"
hx-target="#search-dropdown"
hx-swap="innerHTML"
hx-include="this"/>
</form>
<div id="search-dropdown"
class="absolute left-0 right-0 z-40 mt-1 bg-base-100 rounded-box shadow-lg border border-base-300 max-h-96 overflow-y-auto"></div>
</div>
<div class="flex items-center gap-2">
<div class="dropdown" id="settings-dropdown">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle" aria-label="Settings"
onclick="document.getElementById('settings-dropdown').classList.toggle('dropdown-open')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.428 1.756 2.925 0 3.353a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.428 1.756-2.925 1.756-3.353 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.428-1.756-2.925 0-3.353a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
<div id="settings-panel-host"
class="dropdown-content z-40 mt-2 p-4 shadow bg-base-100 rounded-box w-80 border border-base-300"
hx-get="/settings" hx-trigger="revealed" hx-target="this" hx-swap="innerHTML">
</div>
</div>
<button id="theme-switcher" class="btn btn-ghost btn-circle" aria-label="Toggle theme" onclick="DiscDrop.toggleTheme()">
<svg id="theme-icon-moon" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
<svg id="theme-icon-sun" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/></svg>
</button>
<a href="/artists" class="btn btn-ghost btn-sm" aria-label="Artists">Artists</a>
</div>
</div>
</header>
<main class="max-w-5xl mx-auto px-4 py-6">
<section id="feed" class="space-y-3">
<div id="feed-list">
{#include fragments/feed-list.html rows=rows nextOffset=nextOffset hasMore=hasMore pageSize=pageSize /}
</div>
</section>
</main>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
<channel>
<title>DiscDrop New Releases</title>
<link>http://localhost:8080/</link>
<description>Release groups from the MusicBrainz artists you follow on DiscDrop.</description>
<lastBuildDate>{buildDate}</lastBuildDate>
{#for item in items}
<item>
<title>{item.title}</title>
<link>{item.link}</link>
<guid isPermaLink="true">{item.guid}</guid>
{#if item.pubDate??}
<pubDate>{item.pubDate}</pubDate>
{/if}
<description><![CDATA[ {item.description} ]]></description>
<media:content url="{item.mediaUrl}" type="image/jpeg" medium="image"/>
</item>
{/for}
</channel>
</rss>