Compare commits

...

3 Commits

Author SHA1 Message Date
droideparanoico d66b6e884f Fix UI: replace Tailwind classes with inline styles for CDN compatibility 2026-07-03 11:04:41 +02:00
droideparanoico 16f23557f0 UI polish: centering, search width, dropdown contrast, cover thumbnails, branding, sticky headers
- Constrain main content to max-w-5xl centered
- Widen search input (w-72 md:w-80 lg:w-96)
- Search dropdown: card with shadow, border, and primary-tinted hover
- Branding: SVG music note icon + gradient text + tagline
- Covers: 64px with front-250 thumbnail URL for faster loading
- Badge colors per type (primary/secondary/accent/ghost)
- Sticky table headers via table-pin-rows
- SVG icons for Add/Remove/RSS/OPML buttons
- Consistent hover transitions across tables
2026-07-03 10:58:44 +02:00
droideparanoico 0bae5fd288 Fix cover art sizing with explicit width/height styles 2026-07-02 21:52:35 +02:00
7 changed files with 139 additions and 82 deletions
@@ -79,6 +79,8 @@ public class RSSFeedBuilder {
private String buildDescription(ReleaseGroup rg) { private String buildDescription(ReleaseGroup rg) {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.append("<img src=\"https://coverartarchive.org/release-group/")
.append(rg.mbid()).append("/front\" alt=\"Cover\">");
sb.append("<p><b>").append(escHtml(rg.artistName())).append("</b>") sb.append("<p><b>").append(escHtml(rg.artistName())).append("</b>")
.append(" <i>").append(escHtml(rg.title())).append("</i><br>"); .append(" <i>").append(escHtml(rg.title())).append("</i><br>");
sb.append("Type: ").append(escHtml(rg.primaryType())); sb.append("Type: ").append(escHtml(rg.primaryType()));
@@ -86,8 +88,6 @@ public class RSSFeedBuilder {
sb.append(" | Secondary: ").append(escHtml(rg.secondaryTypes())); sb.append(" | Secondary: ").append(escHtml(rg.secondaryTypes()));
} }
sb.append("<br>First Release Date: ").append(rg.firstReleaseDate()); sb.append("<br>First Release Date: ").append(rg.firstReleaseDate());
sb.append("<br><a href=\"https://coverartarchive.org/release-group/")
.append(rg.mbid()).append("/front\">Cover Art</a>");
sb.append("</p>"); sb.append("</p>");
return sb.toString(); return sb.toString();
} }
+3 -3
View File
@@ -1,9 +1,9 @@
{#include layouts/base} {#include layouts/base}
{#page-content} {#page-content}
<div class="w-full max-w-4xl mx-auto"> <div class="w-full">
<h2 class="text-2xl font-bold mb-4">🎤 Artists</h2> <h2 class="text-2xl font-bold mb-5">🎤 Tracked Artists</h2>
<div id="artist-list" hx-get="/api/artists/list" hx-trigger="load"> <div id="artist-list" hx-get="/api/artists/list" hx-trigger="load">
<span class="loading loading-spinner loading-lg block mx-auto"></span> <span class="loading loading-spinner loading-lg block mx-auto my-12"></span>
</div> </div>
</div> </div>
{/page-content} {/page-content}
@@ -1,80 +1,84 @@
<div id="artist-list" hx-swap-oob="true"> <div id="artist-list" hx-swap-oob="true">
{#if artists.isEmpty()} {#if artists.isEmpty()}
<div class="card bg-base-100 shadow-xl p-8 text-center"> <div class="bg-base-100 shadow-xl rounded-box p-10 text-center border border-base-300">
<p class="text-lg text-base-content/60">No artists tracked yet</p> <svg class="icon-lg" style="margin:0 auto 0.75rem;color:var(--bc);opacity:0.15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<p class="text-sm text-base-content/40 mt-2">Search for artists using the search bar above and click "+ Add"</p> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
<p style="font-size:1.125rem;font-weight:500;color:var(--bc);opacity:0.6">No artists tracked yet</p>
<p style="font-size:0.875rem;color:var(--bc);opacity:0.4;margin-top:0.25rem">Search for artists using the search bar above</p>
</div> </div>
{#else} {#else}
<div class="overflow-x-auto"> <div class="overflow-x-auto rounded-box border border-base-300">
<table class="table table-zebra"> <table class="table table-pin-rows">
<thead> <thead>
<tr> <tr style="background:color-mix(in srgb, var(--b2) 80%, transparent)">
<th>Artist</th> <th>Artist</th>
<th>Since</th> <th>Since</th>
<th>Release Types</th> <th>Release Types</th>
<th>Actions</th> <th style="width:6rem">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#for artist in artists} {#for artist in artists}
<tr> <tr style="transition:background 0.15s" onmouseover="this.style.background='color-mix(in srgb, var(--b2) 50%, transparent)'" onmouseout="this.style.background=''">
<td> <td>
<div class="font-medium">{artist.name}</div> <div style="font-weight:500">{artist.name}</div>
{#if artist.disambiguation} {#if artist.disambiguation}
<div class="text-xs text-base-content/60">{artist.disambiguation}</div> <div style="font-size:0.75rem;color:var(--bc);opacity:0.5">{artist.disambiguation}</div>
{/if} {/if}
</td> </td>
<td class="text-sm text-base-content/60">{artist.createdAt}</td> <td style="font-size:0.875rem;color:var(--bc);opacity:0.5">{artist.createdAt}</td>
<td> <td>
<div class="flex flex-wrap gap-1"> <div style="display:flex;flex-wrap:wrap;gap:0.375rem">
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1 p-1">
<input type="checkbox" class="checkbox checkbox-xs checkbox-primary" <input type="checkbox" class="checkbox checkbox-xs checkbox-primary"
{#if artist.albumMonitored}checked{/if} {#if artist.albumMonitored}checked{/if}
hx-patch="/api/artists/{artist.mbid}/monitor?type=album" hx-patch="/api/artists/{artist.mbid}/monitor?type=album"
hx-target="#artist-list" hx-target="#artist-list"
hx-swap="innerHTML"> hx-swap="innerHTML">
<span class="label-text text-xs">Album</span> <span class="label-text text-xs" style="color:var(--p)">Album</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1 p-1">
<input type="checkbox" class="checkbox checkbox-xs" <input type="checkbox" class="checkbox checkbox-xs checkbox-secondary"
{#if artist.singleMonitored}checked{/if} {#if artist.singleMonitored}checked{/if}
hx-patch="/api/artists/{artist.mbid}/monitor?type=single" hx-patch="/api/artists/{artist.mbid}/monitor?type=single"
hx-target="#artist-list" hx-target="#artist-list"
hx-swap="innerHTML"> hx-swap="innerHTML">
<span class="label-text text-xs">Single</span> <span class="label-text text-xs" style="color:var(--s)">Single</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1 p-1">
<input type="checkbox" class="checkbox checkbox-xs" <input type="checkbox" class="checkbox checkbox-xs checkbox-accent"
{#if artist.epMonitored}checked{/if} {#if artist.epMonitored}checked{/if}
hx-patch="/api/artists/{artist.mbid}/monitor?type=ep" hx-patch="/api/artists/{artist.mbid}/monitor?type=ep"
hx-target="#artist-list" hx-target="#artist-list"
hx-swap="innerHTML"> hx-swap="innerHTML">
<span class="label-text text-xs">EP</span> <span class="label-text text-xs" style="color:var(--a)">EP</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1 p-1">
<input type="checkbox" class="checkbox checkbox-xs" <input type="checkbox" class="checkbox checkbox-xs"
{#if artist.broadcastMonitored}checked{/if} {#if artist.broadcastMonitored}checked{/if}
hx-patch="/api/artists/{artist.mbid}/monitor?type=broadcast" hx-patch="/api/artists/{artist.mbid}/monitor?type=broadcast"
hx-target="#artist-list" hx-target="#artist-list"
hx-swap="innerHTML"> hx-swap="innerHTML">
<span class="label-text text-xs">Broadcast</span> <span class="label-text text-xs" style="color:var(--bc);opacity:0.6">Broadcast</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1 p-1">
<input type="checkbox" class="checkbox checkbox-xs" <input type="checkbox" class="checkbox checkbox-xs"
{#if artist.otherMonitored}checked{/if} {#if artist.otherMonitored}checked{/if}
hx-patch="/api/artists/{artist.mbid}/monitor?type=other" hx-patch="/api/artists/{artist.mbid}/monitor?type=other"
hx-target="#artist-list" hx-target="#artist-list"
hx-swap="innerHTML"> hx-swap="innerHTML">
<span class="label-text text-xs">Other</span> <span class="label-text text-xs" style="color:var(--bc);opacity:0.6">Other</span>
</label> </label>
</div> </div>
</td> </td>
<td> <td>
<button class="btn btn-error btn-xs" <button class="btn btn-ghost btn-xs" style="color:var(--er)"
hx-delete="/api/artists/{artist.mbid}" hx-delete="/api/artists/{artist.mbid}"
hx-target="#artist-list" hx-target="#artist-list"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="return confirm('Remove {artist.name}?')"> onclick="return confirm('Remove {artist.name}?')">
<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
Remove Remove
</button> </button>
</td> </td>
@@ -1,34 +1,50 @@
{#if entries.isEmpty()} {#if entries.isEmpty()}
<div class="card bg-base-100 shadow-xl p-8 text-center"> <div class="bg-base-100 shadow-xl rounded-box p-10 text-center border border-base-300">
<p class="text-lg text-base-content/60">No releases found</p> <svg class="icon-lg" style="margin:0 auto 0.75rem;color:var(--bc);opacity:0.15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<p class="text-sm text-base-content/40 mt-2">Add artists to your collection to see their releases here</p> <circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>
<path d="M9 18V5l12-2v13"/><path d="M9 9l12-2"/>
</svg>
<p style="font-size:1.125rem;font-weight:500;color:var(--bc);opacity:0.6">No releases found</p>
<p style="font-size:0.875rem;color:var(--bc);opacity:0.4;margin-top:0.25rem">Add artists to your collection to see their releases here</p>
</div> </div>
{#else} {#else}
<div class="overflow-x-auto"> <div class="overflow-x-auto rounded-box border border-base-300">
<table class="table table-zebra"> <table class="table table-pin-rows">
<thead> <thead>
<tr> <tr style="background:color-mix(in srgb, var(--b2) 80%, transparent)">
<th style="width:5rem">Cover</th>
<th>Artist</th> <th>Artist</th>
<th>Release</th> <th>Release</th>
<th>Type</th> <th>Type</th>
<th>Date</th> <th style="width:7rem">Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#for entry in entries} {#for entry in entries}
<tr> <tr style="transition:background 0.15s" onmouseover="this.style.background='color-mix(in srgb, var(--b2) 50%, transparent)'" onmouseout="this.style.background=''">
<td class="font-medium">{entry.artistName}</td>
<td> <td>
<a href="https://musicbrainz.org/release-group/{entry.mbid}" <a href="https://musicbrainz.org/release-group/{entry.mbid}" target="_blank" rel="noopener noreferrer">
target="_blank" rel="noopener noreferrer" <img src="https://coverartarchive.org/release-group/{entry.mbid}/front-250" alt="Cover" class="cover-img" loading="lazy" onerror="this.style.display='none'">
class="link link-hover"> </a>
</td>
<td><span style="font-weight:500">{entry.artistName}</span></td>
<td>
<a href="https://musicbrainz.org/release-group/{entry.mbid}" target="_blank" rel="noopener noreferrer" class="link link-hover" style="font-weight:500">
{entry.title} {entry.title}
</a> </a>
</td> </td>
<td> <td>
<span class="badge badge-outline badge-sm">{entry.primaryType}</span> {#if entry.primaryType == 'Album'}
<span class="badge badge-primary badge-sm">Album</span>
{#else if entry.primaryType == 'Single'}
<span class="badge badge-secondary badge-sm">Single</span>
{#else if entry.primaryType == 'EP'}
<span class="badge badge-accent badge-sm">EP</span>
{#else}
<span class="badge badge-ghost badge-sm">{entry.primaryType}</span>
{/if}
</td> </td>
<td class="text-sm text-base-content/60">{entry.firstReleaseDate}</td> <td style="font-size:0.875rem;color:var(--bc);opacity:0.5">{entry.firstReleaseDate}</td>
</tr> </tr>
{/for} {/for}
</tbody> </tbody>
@@ -1,27 +1,37 @@
<div class="bg-base-100 shadow-xl border border-base-300 rounded-box mt-1 max-h-64 overflow-y-auto"> <div class="bg-base-100 shadow-xl border border-base-300 rounded-box mt-1" style="max-height:18rem;overflow-y:auto">
{#if artists.isEmpty()} {#if artists.isEmpty()}
<div class="p-3 text-sm text-base-content/60">No artists found</div> <div class="p-4 text-sm text-center" style="color:var(--bc);opacity:0.5">No artists found</div>
{#else} {#else}
{#for artist in artists} {#for artist in artists}
<div class="flex items-center justify-between p-3 hover:bg-base-200 border-b border-base-200 last:border-b-0"> <div class="search-dropdown-item p-3 border-b border-base-200" style="display:flex;align-items:center;justify-content:space-between">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0 mr-2">
<span class="font-medium">{artist.name}</span> <div style="display:flex;align-items:center;gap:0.5rem">
<span style="font-weight:600;font-size:0.875rem">{artist.name}</span>
{#if artist.type}
<span class="badge badge-ghost badge-xs">{artist.type}</span>
{/if}
</div>
{#if artist.disambiguation} {#if artist.disambiguation}
<span class="text-sm text-base-content/60">{artist.disambiguation}</span> <div style="font-size:0.75rem;color:var(--bc);opacity:0.5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{artist.disambiguation}</div>
{/if}
{#if artist.area}
<div style="font-size:0.75rem;color:var(--bc);opacity:0.4">{artist.area}</div>
{/if} {/if}
<br>
<span class="text-xs text-base-content/40">{artist.area}</span>
</div> </div>
<div class="ml-2 flex-shrink-0"> <div style="flex-shrink:0">
{#if artist.alreadyTracked} {#if artist.alreadyTracked}
<span class="badge badge-success badge-sm">✓ Added</span> <span class="badge badge-success badge-sm gap-1">
<svg class="icon-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
Added
</span>
{#else} {#else}
<button class="btn btn-primary btn-xs" <button class="btn btn-primary btn-xs"
hx-post="/api/artists/{artist.mbid}/track" hx-post="/api/artists/{artist.mbid}/track"
hx-target="#search-results" hx-target="#search-results"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="document.getElementById('search-input').value=''"> onclick="document.getElementById('search-input').value=''">
+ Add <svg class="icon-xs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Add
</button> </button>
{/if} {/if}
</div> </div>
+34 -20
View File
@@ -4,20 +4,23 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DiscDrop</title> <title>DiscDrop</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.23/dist/full.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script> <script src="https://unpkg.com/htmx.org@2/dist/htmx.min.js"></script>
<style> <style>
#search-wrapper { #search-wrapper { position: relative; width: 280px; }
position: relative; @media (min-width: 768px) { #search-wrapper { width: 320px; } }
} @media (min-width: 1024px) { #search-wrapper { width: 384px; } }
#search-results { #search-results {
position: absolute; position: absolute; top: 100%; left: 0; right: 0; z-index: 50;
top: 100%;
left: 0;
right: 0;
z-index: 50;
width: 100%;
} }
main { max-width: 1024px; margin: 0 auto; padding: 1.5rem 1rem; }
@media (min-width: 640px) { main { padding: 1.5rem 1.5rem; } }
.brand-icon { width: 28px; height: 28px; }
.icon-sm { width: 16px; height: 16px; }
.icon-xs { width: 12px; height: 12px; }
.icon-lg { width: 48px; height: 48px; }
.cover-img { width: 64px; height: 64px; object-fit: cover; border-radius: 8px; }
.search-dropdown-item:hover { background: color-mix(in srgb, var(--p) 10%, transparent); }
</style> </style>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
@@ -35,16 +38,27 @@
<div class="navbar bg-base-100 shadow-lg px-4"> <div class="navbar bg-base-100 shadow-lg px-4">
<div class="navbar-start"> <div class="navbar-start">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="/" class="text-xl font-bold">🎵 DiscDrop</a> <a href="/" class="flex items-center gap-2 no-underline">
<svg class="brand-icon text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="6" cy="18" r="3"/>
<circle cx="18" cy="16" r="3"/>
<path d="M9 18V5l12-2v13"/>
<path d="M9 9l12-2"/>
</svg>
<div>
<div class="text-xl font-bold" style="background:linear-gradient(135deg,var(--p),var(--a));-webkit-background-clip:text;-webkit-text-fill-color:transparent">DiscDrop</div>
<div class="text-xs text-base-content/50 leading-none" style="display:none">Release Groups Feed</div>
</div>
</a>
<div class="tabs tabs-box ml-2 hidden sm:flex"> <div class="tabs tabs-box ml-2 hidden sm:flex">
<a href="/" class="tab {#if activeTab == 'releases'}tab-active{/if}">Releases</a> <a href="/" class="tab tab-sm {#if activeTab == 'releases'}tab-active{/if}">Releases</a>
<a href="/artists" class="tab {#if activeTab == 'artists'}tab-active{/if}">Artists</a> <a href="/artists" class="tab tab-sm {#if activeTab == 'artists'}tab-active{/if}">Artists</a>
</div> </div>
</div> </div>
</div> </div>
<div class="navbar-center"> <div class="navbar-center">
<div id="search-wrapper" class="w-48 md:w-64"> <div id="search-wrapper">
<form hx-get="/api/artists/search" <form hx-get="/api/artists/search"
hx-target="#search-results" hx-target="#search-results"
hx-swap="innerHTML" hx-swap="innerHTML"
@@ -60,10 +74,10 @@
</div> </div>
</div> </div>
<div class="navbar-end gap-2"> <div class="navbar-end gap-1">
<select class="select select-bordered select-sm w-full max-w-[8rem]" <select class="select select-bordered select-sm" style="width:7rem"
onchange="setTheme(this)"> onchange="setTheme(this)">
<option value="dark" selected>Dark</option> <option value="dark">Dark</option>
<option value="light">Light</option> <option value="light">Light</option>
<option value="synthwave">Synthwave</option> <option value="synthwave">Synthwave</option>
<option value="retro">Retro</option> <option value="retro">Retro</option>
@@ -73,12 +87,12 @@
<option value="nord">Nord</option> <option value="nord">Nord</option>
</select> </select>
<button class="btn btn-ghost btn-sm" <button class="btn btn-ghost btn-sm px-2"
onclick="settingsModal.showModal()">⚙️</button> onclick="settingsModal.showModal()">⚙️</button>
</div> </div>
</div> </div>
<main class="container mx-auto p-4"> <main>
{#insert page-content}{/insert} {#insert page-content}{/insert}
</main> </main>
+20 -7
View File
@@ -1,13 +1,26 @@
{#include layouts/base} {#include layouts/base}
{#page-content} {#page-content}
<div class="w-full max-w-4xl mx-auto"> <div class="w-full">
<h2 class="text-2xl font-bold mb-4 text-center">📡 Releases</h2> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem">
<div id="feed-entries" hx-get="/api/artists/feed-table" hx-trigger="load"> <h2 style="font-size:1.5rem;font-weight:700">📡 Releases</h2>
<span class="loading loading-spinner loading-lg block mx-auto"></span> <div style="display:flex;gap:0.5rem">
<a href="/feed/rss.xml" class="btn btn-primary btn-sm gap-2">
<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 11a9 9 0 019 9"/><path d="M4 4a16 16 0 0116 16"/>
<circle cx="5" cy="19" r="1"/>
</svg>
RSS Feed
</a>
<a href="/feed/opml" class="btn btn-ghost btn-sm gap-2">
<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
OPML
</a>
</div>
</div> </div>
<div class="mt-6 flex justify-center gap-4"> <div id="feed-entries" hx-get="/api/artists/feed-table" hx-trigger="load">
<a href="/feed/rss.xml" class="btn btn-primary">📥 RSS Feed</a> <div style="display:flex;justify-content:center;padding:4rem 0">
<a href="/feed/opml" class="btn btn-ghost">📋 OPML</a> <span class="loading loading-spinner loading-lg"></span>
</div>
</div> </div>
</div> </div>
{/page-content} {/page-content}