Add pagination to feed with Load more button using HTMX
This commit is contained in:
@@ -211,9 +211,11 @@ public class DatabaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Record.ReleaseGroup> getFeed(List<String> artistMbids, List<String> primaryTypes) {
|
public record FeedResult(List<Record.ReleaseGroup> entries, boolean hasMore) {}
|
||||||
|
|
||||||
|
public FeedResult getFeed(List<String> artistMbids, List<String> primaryTypes, int limit, int offset) {
|
||||||
var groups = new ArrayList<Record.ReleaseGroup>();
|
var groups = new ArrayList<Record.ReleaseGroup>();
|
||||||
if (artistMbids.isEmpty()) return groups;
|
if (artistMbids.isEmpty()) return new FeedResult(groups, false);
|
||||||
|
|
||||||
var placeholders = String.join(",", artistMbids.stream().map(m -> "?").toList());
|
var placeholders = String.join(",", artistMbids.stream().map(m -> "?").toList());
|
||||||
var sql = "SELECT * FROM release_group_cache WHERE artist_mbid IN (" + placeholders + ")";
|
var sql = "SELECT * FROM release_group_cache WHERE artist_mbid IN (" + placeholders + ")";
|
||||||
@@ -222,12 +224,15 @@ public class DatabaseService {
|
|||||||
sql += " AND primary_type IN (" + typePlaceholders + ")";
|
sql += " AND primary_type IN (" + typePlaceholders + ")";
|
||||||
}
|
}
|
||||||
sql += " ORDER BY (CASE WHEN first_release_date IS NULL OR first_release_date = '' THEN 1 ELSE 0 END), first_release_date DESC";
|
sql += " ORDER BY (CASE WHEN first_release_date IS NULL OR first_release_date = '' THEN 1 ELSE 0 END), first_release_date DESC";
|
||||||
|
sql += " LIMIT ? OFFSET ?";
|
||||||
|
|
||||||
try (Connection conn = getConnection();
|
try (Connection conn = getConnection();
|
||||||
PreparedStatement ps = conn.prepareStatement(sql)) {
|
PreparedStatement ps = conn.prepareStatement(sql)) {
|
||||||
int idx = 1;
|
int idx = 1;
|
||||||
for (var mbid : artistMbids) ps.setString(idx++, mbid);
|
for (var mbid : artistMbids) ps.setString(idx++, mbid);
|
||||||
for (var type : primaryTypes) ps.setString(idx++, type);
|
for (var type : primaryTypes) ps.setString(idx++, type);
|
||||||
|
ps.setInt(idx++, limit + 1);
|
||||||
|
ps.setInt(idx, offset);
|
||||||
try (ResultSet rs = ps.executeQuery()) {
|
try (ResultSet rs = ps.executeQuery()) {
|
||||||
while (rs.next()) {
|
while (rs.next()) {
|
||||||
groups.add(mapReleaseGroup(rs));
|
groups.add(mapReleaseGroup(rs));
|
||||||
@@ -236,7 +241,9 @@ public class DatabaseService {
|
|||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
return groups;
|
boolean hasMore = groups.size() > limit;
|
||||||
|
if (hasMore) groups.remove(groups.size() - 1);
|
||||||
|
return new FeedResult(groups, hasMore);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteReleaseGroupsForArtist(String mbid) {
|
public void deleteReleaseGroupsForArtist(String mbid) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.discdrop.service;
|
package com.discdrop.service;
|
||||||
|
|
||||||
import com.discdrop.db.DatabaseService;
|
import com.discdrop.db.DatabaseService;
|
||||||
|
import com.discdrop.db.DatabaseService.FeedResult;
|
||||||
import com.discdrop.db.Record.ReleaseGroup;
|
import com.discdrop.db.Record.ReleaseGroup;
|
||||||
import com.discdrop.db.Record.TrackedArtist;
|
import com.discdrop.db.Record.TrackedArtist;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
@@ -23,9 +24,15 @@ public class FeedService {
|
|||||||
@Inject
|
@Inject
|
||||||
DatabaseService db;
|
DatabaseService db;
|
||||||
|
|
||||||
|
public static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
public List<ReleaseGroup> getFeedEntries() {
|
public List<ReleaseGroup> getFeedEntries() {
|
||||||
|
return getFeedEntries(1).entries();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FeedResult getFeedEntries(int page) {
|
||||||
var artists = artistService.findAll();
|
var artists = artistService.findAll();
|
||||||
if (artists.isEmpty()) return List.of();
|
if (artists.isEmpty()) return new FeedResult(List.of(), false);
|
||||||
|
|
||||||
var artistMbids = new ArrayList<String>();
|
var artistMbids = new ArrayList<String>();
|
||||||
for (var a : artists) {
|
for (var a : artists) {
|
||||||
@@ -42,7 +49,7 @@ public class FeedService {
|
|||||||
monitoredTypes.addAll(getMonitoredTypes(a));
|
monitoredTypes.addAll(getMonitoredTypes(a));
|
||||||
}
|
}
|
||||||
|
|
||||||
return db.getFeed(artistMbids, monitoredTypes.isEmpty() ? allTypes : monitoredTypes);
|
return db.getFeed(artistMbids, monitoredTypes.isEmpty() ? allTypes : monitoredTypes, PAGE_SIZE, (page - 1) * PAGE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getMonitoredTypes(TrackedArtist a) {
|
private List<String> getMonitoredTypes(TrackedArtist a) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.discdrop.web;
|
package com.discdrop.web;
|
||||||
|
|
||||||
|
import com.discdrop.db.DatabaseService.FeedResult;
|
||||||
import com.discdrop.db.Record.MonitoringFlags;
|
import com.discdrop.db.Record.MonitoringFlags;
|
||||||
import com.discdrop.db.Record.TrackedArtist;
|
import com.discdrop.db.Record.TrackedArtist;
|
||||||
import com.discdrop.service.ArtistService;
|
import com.discdrop.service.ArtistService;
|
||||||
@@ -106,9 +107,12 @@ public class ArtistController {
|
|||||||
@GET
|
@GET
|
||||||
@Path("/feed-table")
|
@Path("/feed-table")
|
||||||
@Produces(MediaType.TEXT_HTML)
|
@Produces(MediaType.TEXT_HTML)
|
||||||
public TemplateInstance feedTable() {
|
public TemplateInstance feedTable(@QueryParam("page") @DefaultValue("1") int page) {
|
||||||
var entries = feedService.getFeedEntries();
|
var result = feedService.getFeedEntries(page);
|
||||||
return feedEntriesTemplate.data("entries", entries);
|
return feedEntriesTemplate
|
||||||
|
.data("entries", result.entries())
|
||||||
|
.data("hasMore", result.hasMore())
|
||||||
|
.data("nextPage", page + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> getMonitoredTypes(TrackedArtist a) {
|
private List<String> getMonitoredTypes(TrackedArtist a) {
|
||||||
|
|||||||
@@ -4,37 +4,43 @@
|
|||||||
<p class="text-sm text-base-content/40 mt-2">Add artists to your collection to see their releases here</p>
|
<p class="text-sm text-base-content/40 mt-2">Add artists to your collection to see their releases here</p>
|
||||||
</div>
|
</div>
|
||||||
{#else}
|
{#else}
|
||||||
<div style="display:flex;flex-direction:column;gap:2.5rem">
|
{#for entry in entries}
|
||||||
{#for entry in entries}
|
<a href="https://musicbrainz.org/release-group/{entry.mbid}" target="_blank" rel="noopener noreferrer"
|
||||||
<a href="https://musicbrainz.org/release-group/{entry.mbid}" target="_blank" rel="noopener noreferrer"
|
class="card card-side bg-base-200 border border-base-300 hover:bg-base-300 card-shadow overflow-hidden">
|
||||||
class="card card-side bg-base-200 border border-base-300 hover:bg-base-300 card-shadow overflow-hidden">
|
<figure class="w-64">
|
||||||
<figure class="w-64">
|
<img src="https://coverartarchive.org/release-group/{entry.mbid}/front-250"
|
||||||
<img src="https://coverartarchive.org/release-group/{entry.mbid}/front-250"
|
alt="{entry.title}" class="object-cover w-full h-full"
|
||||||
alt="{entry.title}" class="object-cover w-full h-full"
|
loading="lazy"
|
||||||
loading="lazy"
|
onerror="this.parentElement.innerHTML='<div class=\'flex items-center justify-center h-full text-base-content/20\'>No Cover</div>'">
|
||||||
onerror="this.parentElement.innerHTML='<div class=\'flex items-center justify-center h-full text-base-content/20\'>No Cover</div>'">
|
</figure>
|
||||||
</figure>
|
<div class="card-body">
|
||||||
<div class="card-body">
|
<h3 style="font-size:1.5rem;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--fallback-bc,oklch(var(--bc)/0.6))">{entry.artistName}</h3>
|
||||||
<h3 style="font-size:1.5rem;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:var(--fallback-bc,oklch(var(--bc)/0.6))">{entry.artistName}</h3>
|
<h2 style="font-size:2rem;font-weight:700">{entry.title}</h2>
|
||||||
<h2 style="font-size:2rem;font-weight:700">{entry.title}</h2>
|
<div style="margin-top:0.75rem">
|
||||||
<div style="margin-top:0.75rem">
|
{#if entry.firstReleaseDate}
|
||||||
{#if entry.firstReleaseDate}
|
<div style="font-size:1.5rem;color:var(--fallback-bc,oklch(var(--bc)/0.4))">{entry.firstReleaseDate}</div>
|
||||||
<div style="font-size:1.5rem;color:var(--fallback-bc,oklch(var(--bc)/0.4))">{entry.firstReleaseDate}</div>
|
{/if}
|
||||||
|
<div style="margin-top:0.4rem">
|
||||||
|
{#if entry.primaryType == 'Album'}
|
||||||
|
<span class="badge badge-primary" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">Album</span>
|
||||||
|
{#else if entry.primaryType == 'Single'}
|
||||||
|
<span class="badge badge-secondary" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">Single</span>
|
||||||
|
{#else if entry.primaryType == 'EP'}
|
||||||
|
<span class="badge badge-accent" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">EP</span>
|
||||||
|
{#else}
|
||||||
|
<span class="badge badge-ghost" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">{entry.primaryType}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<div style="margin-top:0.4rem">
|
|
||||||
{#if entry.primaryType == 'Album'}
|
|
||||||
<span class="badge badge-primary" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">Album</span>
|
|
||||||
{#else if entry.primaryType == 'Single'}
|
|
||||||
<span class="badge badge-secondary" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">Single</span>
|
|
||||||
{#else if entry.primaryType == 'EP'}
|
|
||||||
<span class="badge badge-accent" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">EP</span>
|
|
||||||
{#else}
|
|
||||||
<span class="badge badge-ghost" style="font-size:1.5rem;padding:0.3rem 0.8rem;height:auto">{entry.primaryType}</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
{/for}
|
</a>
|
||||||
|
{/for}
|
||||||
|
{#if hasMore}
|
||||||
|
<div style="display:flex;justify-content:center;padding:1rem 0">
|
||||||
|
<button class="btn btn-primary btn-sm"
|
||||||
|
hx-get="/api/artists/feed-table?page={nextPage}"
|
||||||
|
hx-target="closest div"
|
||||||
|
hx-swap="outerHTML">Load more</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="feed-entries" hx-get="/api/artists/feed-table" hx-trigger="load">
|
<div id="feed-entries" hx-get="/api/artists/feed-table" hx-trigger="load" style="display:flex;flex-direction:column;gap:2.5rem">
|
||||||
<div style="display:flex;justify-content:center;padding:4rem 0">
|
<div style="display:flex;justify-content:center;padding:4rem 0">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<span class="loading loading-spinner loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user