Fix search dropdown, async artist add, OOB swaps

- Fix search results dropdown: absolute position relative to search wrapper
- Make release group fetching async via Vert.x executeBlocking (avoid blocking add)
- Add OOB swap for artist list so +Add works from any page
- Simplify search-results template styling
- Add Response type for combined OOB responses
This commit is contained in:
2026-07-02 16:11:38 +02:00
parent b08eb2573c
commit 08aa5b8a2d
4 changed files with 38 additions and 16 deletions
@@ -9,9 +9,11 @@ import com.discdrop.service.ReleaseGroupService;
import io.quarkus.qute.Location; import io.quarkus.qute.Location;
import io.quarkus.qute.Template; import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateInstance;
import io.vertx.core.Vertx;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.*; import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -27,6 +29,9 @@ public class ArtistController {
@Inject @Inject
FeedService feedService; FeedService feedService;
@Inject
Vertx vertx;
@Inject @Inject
@Location("fragments/search-results") @Location("fragments/search-results")
Template searchResultsTemplate; Template searchResultsTemplate;
@@ -55,12 +60,18 @@ public class ArtistController {
@POST @POST
@Path("/{mbid}/track") @Path("/{mbid}/track")
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
public TemplateInstance trackArtist(@PathParam("mbid") String mbid) { public Response trackArtist(@PathParam("mbid") String mbid) {
var artist = artistService.addArtist(mbid); var artist = artistService.addArtist(mbid);
var types = getMonitoredTypes(artist); var types = getMonitoredTypes(artist);
vertx.executeBlocking(promise -> {
releaseGroupService.fetchAndCache(artist.mbid(), artist.name(), types); releaseGroupService.fetchAndCache(artist.mbid(), artist.name(), types);
promise.complete();
});
var allArtists = artistService.findAll(); var allArtists = artistService.findAll();
return artistListTemplate.data("artists", allArtists); var listHtml = artistListTemplate.data("artists", allArtists).render();
var emptySearch = searchResultsTemplate.data("artists", List.of()).render();
var combined = emptySearch + listHtml;
return Response.ok(combined).build();
} }
@DELETE @DELETE
@@ -1,3 +1,4 @@
<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="card bg-base-100 shadow-xl p-8 text-center">
<p class="text-lg text-base-content/60">No artists tracked yet</p> <p class="text-lg text-base-content/60">No artists tracked yet</p>
@@ -30,35 +31,40 @@
<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-swap="innerHTML"> hx-target="#artist-list"
hx-swap="innerHTML">
<span class="label-text text-xs">Album</span> <span class="label-text text-xs">Album</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1">
<input type="checkbox" class="checkbox checkbox-xs" <input type="checkbox" class="checkbox checkbox-xs"
{#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-swap="innerHTML"> hx-target="#artist-list"
hx-swap="innerHTML">
<span class="label-text text-xs">Single</span> <span class="label-text text-xs">Single</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-1">
<input type="checkbox" class="checkbox checkbox-xs" <input type="checkbox" class="checkbox checkbox-xs"
{#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-swap="innerHTML"> hx-target="#artist-list"
hx-swap="innerHTML">
<span class="label-text text-xs">EP</span> <span class="label-text text-xs">EP</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-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-swap="innerHTML"> hx-target="#artist-list"
hx-swap="innerHTML">
<span class="label-text text-xs">Broadcast</span> <span class="label-text text-xs">Broadcast</span>
</label> </label>
<label class="label cursor-pointer gap-1"> <label class="label cursor-pointer gap-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-swap="innerHTML"> hx-target="#artist-list"
hx-swap="innerHTML">
<span class="label-text text-xs">Other</span> <span class="label-text text-xs">Other</span>
</label> </label>
</div> </div>
@@ -78,3 +84,4 @@
</table> </table>
</div> </div>
{/if} {/if}
</div>
@@ -1,4 +1,4 @@
<div class="card bg-base-100 shadow-xl mt-1 max-h-64 overflow-y-auto"> <div class="bg-base-100 shadow-xl border border-base-300 rounded-box mt-1 max-h-64 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-3 text-sm text-base-content/60">No artists found</div>
{#else} {#else}
@@ -18,9 +18,9 @@
{#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="#artist-list" hx-target="#search-results"
hx-swap="innerHTML" hx-swap="innerHTML"
onclick="this.closest('#search-results').innerHTML=''; document.querySelector('[name=q]').value=''"> onclick="document.getElementById('search-input').value=''">
+ Add + Add
</button> </button>
{/if} {/if}
@@ -7,12 +7,16 @@
<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.12.23/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.0.4/dist/htmx.min.js"></script>
<style> <style>
.search-dropdown { #search-wrapper {
position: relative;
}
#search-results {
position: absolute; position: absolute;
top: 100%; top: 100%;
left: 0; left: 0;
right: 0; right: 0;
z-index: 50; z-index: 50;
width: 100%;
} }
</style> </style>
<script> <script>
@@ -40,7 +44,7 @@
</div> </div>
<div class="navbar-center"> <div class="navbar-center">
<div class="relative"> <div id="search-wrapper" class="w-48 md:w-64">
<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"
@@ -48,18 +52,18 @@
<input id="search-input" <input id="search-input"
type="text" type="text"
placeholder="Search artists..." placeholder="Search artists..."
class="input input-bordered input-sm w-48 md:w-64" class="input input-bordered input-sm w-full"
name="q" name="q"
autocomplete="off"> autocomplete="off">
</form> </form>
<div id="search-results" class="search-dropdown"></div> <div id="search-results"></div>
</div> </div>
</div> </div>
<div class="navbar-end gap-2"> <div class="navbar-end gap-2">
<select class="select select-bordered select-sm w-full max-w-[8rem]" <select class="select select-bordered select-sm w-full max-w-[8rem]"
onchange="setTheme(this)"> onchange="setTheme(this)">
<option value="dark">Dark</option> <option value="dark" selected>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>