Sync loading indicators

This commit is contained in:
2026-07-15 12:53:06 +02:00
committed by david.alvarez
parent 03e5b2810c
commit 4ef83cd701
7 changed files with 70 additions and 28 deletions
@@ -68,14 +68,14 @@ public class ArtistResource {
} }
artistService.follow(dto); artistService.follow(dto);
CompletableFuture.runAsync(() -> syncService.syncArtist(mbid)); CompletableFuture.runAsync(() -> syncService.syncArtist(mbid));
return feedFragment(0, true); return feedFragment(0, true, true);
} }
@DELETE @DELETE
@Path("/{mbid}") @Path("/{mbid}")
public TemplateInstance unfollow(@PathParam("mbid") String mbid) { public TemplateInstance unfollow(@PathParam("mbid") String mbid) {
artistService.unfollow(mbid); artistService.unfollow(mbid);
return feedFragment(0, false); return feedFragment(0, false, false);
} }
@GET @GET
@@ -90,7 +90,8 @@ public class ArtistResource {
FollowedArtist artist = artistService.findArtist(mbid); FollowedArtist artist = artistService.findArtist(mbid);
List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of(); List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of();
return fragments_artist_row.data("artist", artist) return fragments_artist_row.data("artist", artist)
.data("settings", settings); .data("settings", settings)
.data("syncing", syncService.isSyncing(mbid));
} }
@POST @POST
@@ -103,10 +104,11 @@ public class ArtistResource {
FollowedArtist artist = artistService.findArtist(mbid); FollowedArtist artist = artistService.findArtist(mbid);
List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of(); List<ArtistTypeSetting> settings = artist != null ? artistService.settingsFor(artist) : List.of();
return fragments_artist_row.data("artist", artist) return fragments_artist_row.data("artist", artist)
.data("settings", settings); .data("settings", settings)
.data("syncing", true);
} }
private TemplateInstance feedFragment(int offset, boolean autoRefresh) { private TemplateInstance feedFragment(int offset, boolean autoRefresh, boolean syncing) {
if (offset < 0) { if (offset < 0) {
offset = 0; offset = 0;
} }
@@ -117,6 +119,7 @@ public class ArtistResource {
.data("nextOffset", nextOffset) .data("nextOffset", nextOffset)
.data("hasMore", hasMore) .data("hasMore", hasMore)
.data("pageSize", feedService.pageSize()) .data("pageSize", feedService.pageSize())
.data("autoRefresh", autoRefresh); .data("autoRefresh", autoRefresh)
.data("syncing", syncing);
} }
} }
@@ -3,6 +3,7 @@ package io.discdrop.resource;
import io.discdrop.persistence.FollowedArtist; import io.discdrop.persistence.FollowedArtist;
import io.discdrop.persistence.ReleaseGroupEntity; import io.discdrop.persistence.ReleaseGroupEntity;
import io.discdrop.service.FeedService; import io.discdrop.service.FeedService;
import io.discdrop.service.SyncService;
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;
@@ -20,6 +21,9 @@ public class PageResource {
@Inject @Inject
FeedService feedService; FeedService feedService;
@Inject
SyncService syncService;
@Inject @Inject
Template index; Template index;
@@ -41,7 +45,8 @@ public class PageResource {
.data("nextOffset", nextOffset) .data("nextOffset", nextOffset)
.data("hasMore", hasMore) .data("hasMore", hasMore)
.data("pageSize", feedService.pageSize()) .data("pageSize", feedService.pageSize())
.data("autoRefresh", false); .data("autoRefresh", false)
.data("syncing", syncService.isAnySyncing());
} }
@GET @GET
@@ -54,6 +59,7 @@ public class PageResource {
.data("hasMore", hasMore) .data("hasMore", hasMore)
.data("pageSize", feedService.pageSize()) .data("pageSize", feedService.pageSize())
.data("autoRefresh", false) .data("autoRefresh", false)
.data("syncing", syncService.isAnySyncing())
.data("followedCount", FollowedArtist.count()) .data("followedCount", FollowedArtist.count())
.data("feedCount", ReleaseGroupEntity.count()); .data("feedCount", ReleaseGroupEntity.count());
} }
@@ -11,6 +11,7 @@ import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
@@ -31,6 +32,8 @@ public class SyncService {
@Inject @Inject
SettingsService settingsService; SettingsService settingsService;
private final Set<String> syncing = ConcurrentHashMap.newKeySet();
private final ScheduledExecutorService executor = private final ScheduledExecutorService executor =
Executors.newSingleThreadScheduledExecutor(r -> { Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "discdrop-sync"); Thread t = new Thread(r, "discdrop-sync");
@@ -73,23 +76,36 @@ public class SyncService {
} }
} }
public void syncArtist(String mbid) { public boolean isSyncing(String mbid) {
Set<String> enabledTypes = artistService.enabledTypes(mbid); return syncing.contains(mbid);
String typeFilter = enabledTypes.isEmpty() ? null : String.join("|", enabledTypes); }
int limit = 100; public boolean isAnySyncing() {
int offset = 0; return !syncing.isEmpty();
boolean more = true; }
while (more) {
ReleaseGroupBrowseResult page = mbzService.browseReleaseGroups(mbid, typeFilter, limit, offset); public void syncArtist(String mbid) {
if (page == null || page.releaseGroups == null || page.releaseGroups.isEmpty()) { syncing.add(mbid);
break; try {
Set<String> enabledTypes = artistService.enabledTypes(mbid);
String typeFilter = enabledTypes.isEmpty() ? null : String.join("|", enabledTypes);
int limit = 100;
int offset = 0;
boolean more = true;
while (more) {
ReleaseGroupBrowseResult page = mbzService.browseReleaseGroups(mbid, typeFilter, limit, offset);
if (page == null || page.releaseGroups == null || page.releaseGroups.isEmpty()) {
break;
}
repo.upsertPage(mbid, page.releaseGroups);
offset += page.releaseGroups.size();
more = page.releaseGroups.size() == limit && offset < page.count;
} }
repo.upsertPage(mbid, page.releaseGroups); repo.markSynced(mbid);
offset += page.releaseGroups.size(); } finally {
more = page.releaseGroups.size() == limit && offset < page.count; syncing.remove(mbid);
} }
repo.markSynced(mbid);
} }
public void resyncArtist(String mbid) { public void resyncArtist(String mbid) {
@@ -34,4 +34,16 @@
</label> </label>
{/for} {/for}
</div> </div>
{#if syncing?? && syncing}
<div class="flex items-center gap-2 text-sm text-base-content/60 pt-1">
<span class="loading loading-spinner loading-xs"></span>
<span>Syncing releases from MusicBrainz…</span>
</div>
<div hx-get="/artists/{artist.mbid}/row"
hx-target="#artist-{artist.mbid}"
hx-swap="innerHTML"
hx-trigger="load delay:3s"
class="hidden"></div>
{/if}
</div> </div>
@@ -11,15 +11,20 @@
</button> </button>
{/if} {/if}
{#if !hasMore && rows.empty} {#if !hasMore && rows.empty}
<div class="text-center text-base-content/60 py-12"> {#if syncing?? && syncing}
<p class="text-lg">No releases yet.</p> <div class="text-center py-12">
<p class="text-sm">Search for an artist above and click <strong>Follow</strong> to start tracking their release groups.</p> <span class="loading loading-spinner loading-lg text-primary"></span>
<p class="mt-3 text-base-content/70">Fetching releases from MusicBrainz…</p>
</div> </div>
{/if}
{#if autoRefresh?? && autoRefresh}
<div hx-get="/feed?offset=0" <div hx-get="/feed?offset=0"
hx-target="#feed-list" hx-target="#feed-list"
hx-swap="innerHTML" hx-swap="innerHTML"
hx-trigger="load delay:3s" hx-trigger="load delay:3s"
class="hidden"></div> class="hidden"></div>
{#else}
<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}
{/if} {/if}
@@ -2,7 +2,7 @@
<img src="https://coverartarchive.org/release-group/{row.mbid}/front" <img src="https://coverartarchive.org/release-group/{row.mbid}/front"
alt="cover" alt="cover"
loading="lazy" loading="lazy"
class="cover-art w-20 h-20 object-cover shrink-0" class="cover-art w-40 h-40 object-cover shrink-0"
onerror="this.onerror=null;this.src='/img/no-cover.svg'"/> onerror="this.onerror=null;this.src='/img/no-cover.svg'"/>
<div class="flex-1 min-w-0 flex flex-col justify-center gap-1"> <div class="flex-1 min-w-0 flex flex-col justify-center gap-1">
+1 -1
View File
@@ -65,7 +65,7 @@
</div> </div>
</header> </header>
<main class="max-w-2xl mx-auto px-4 py-6"> <main class="max-w-5xl mx-auto px-4 py-6">
<section id="feed" class="space-y-3"> <section id="feed" class="space-y-3">
<div id="feed-list"> <div id="feed-list">
{#include fragments/feed-list.html rows=rows nextOffset=nextOffset hasMore=hasMore pageSize=pageSize /} {#include fragments/feed-list.html rows=rows nextOffset=nextOffset hasMore=hasMore pageSize=pageSize /}