From c186fead83243e45093607d46395205800ab23c1 Mon Sep 17 00:00:00 2001 From: opencode Date: Wed, 15 Jul 2026 12:53:06 +0200 Subject: [PATCH] Add sync loading indicators: spinner in feed when fetching releases, syncing indicator on artist row when toggling types --- .../io/discdrop/resource/ArtistResource.java | 15 ++++--- .../io/discdrop/resource/PageResource.java | 8 +++- .../java/io/discdrop/service/SyncService.java | 44 +++++++++++++------ .../templates/fragments/artist-row.html | 12 +++++ .../templates/fragments/feed-list.html | 15 ++++--- .../templates/fragments/feed-row.html | 2 +- src/main/resources/templates/index.html | 2 +- 7 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/discdrop/resource/ArtistResource.java b/src/main/java/io/discdrop/resource/ArtistResource.java index e435ba8..681732c 100644 --- a/src/main/java/io/discdrop/resource/ArtistResource.java +++ b/src/main/java/io/discdrop/resource/ArtistResource.java @@ -68,14 +68,14 @@ public class ArtistResource { } artistService.follow(dto); CompletableFuture.runAsync(() -> syncService.syncArtist(mbid)); - return feedFragment(0, true); + return feedFragment(0, true, true); } @DELETE @Path("/{mbid}") public TemplateInstance unfollow(@PathParam("mbid") String mbid) { artistService.unfollow(mbid); - return feedFragment(0, false); + return feedFragment(0, false, false); } @GET @@ -90,7 +90,8 @@ public class ArtistResource { FollowedArtist artist = artistService.findArtist(mbid); List settings = artist != null ? artistService.settingsFor(artist) : List.of(); return fragments_artist_row.data("artist", artist) - .data("settings", settings); + .data("settings", settings) + .data("syncing", syncService.isSyncing(mbid)); } @POST @@ -103,10 +104,11 @@ public class ArtistResource { FollowedArtist artist = artistService.findArtist(mbid); List settings = artist != null ? artistService.settingsFor(artist) : List.of(); 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) { offset = 0; } @@ -117,6 +119,7 @@ public class ArtistResource { .data("nextOffset", nextOffset) .data("hasMore", hasMore) .data("pageSize", feedService.pageSize()) - .data("autoRefresh", autoRefresh); + .data("autoRefresh", autoRefresh) + .data("syncing", syncing); } } diff --git a/src/main/java/io/discdrop/resource/PageResource.java b/src/main/java/io/discdrop/resource/PageResource.java index cf227fa..11d0081 100644 --- a/src/main/java/io/discdrop/resource/PageResource.java +++ b/src/main/java/io/discdrop/resource/PageResource.java @@ -3,6 +3,7 @@ package io.discdrop.resource; import io.discdrop.persistence.FollowedArtist; import io.discdrop.persistence.ReleaseGroupEntity; import io.discdrop.service.FeedService; +import io.discdrop.service.SyncService; import io.quarkus.qute.Location; import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; @@ -20,6 +21,9 @@ public class PageResource { @Inject FeedService feedService; + @Inject + SyncService syncService; + @Inject Template index; @@ -41,7 +45,8 @@ public class PageResource { .data("nextOffset", nextOffset) .data("hasMore", hasMore) .data("pageSize", feedService.pageSize()) - .data("autoRefresh", false); + .data("autoRefresh", false) + .data("syncing", syncService.isAnySyncing()); } @GET @@ -54,6 +59,7 @@ public class PageResource { .data("hasMore", hasMore) .data("pageSize", feedService.pageSize()) .data("autoRefresh", false) + .data("syncing", syncService.isAnySyncing()) .data("followedCount", FollowedArtist.count()) .data("feedCount", ReleaseGroupEntity.count()); } diff --git a/src/main/java/io/discdrop/service/SyncService.java b/src/main/java/io/discdrop/service/SyncService.java index ee2ee78..bd6278f 100644 --- a/src/main/java/io/discdrop/service/SyncService.java +++ b/src/main/java/io/discdrop/service/SyncService.java @@ -11,6 +11,7 @@ import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -31,6 +32,8 @@ public class SyncService { @Inject SettingsService settingsService; + private final Set syncing = ConcurrentHashMap.newKeySet(); + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "discdrop-sync"); @@ -73,23 +76,36 @@ public class SyncService { } } - public void syncArtist(String mbid) { - Set enabledTypes = artistService.enabledTypes(mbid); - String typeFilter = enabledTypes.isEmpty() ? null : String.join("|", enabledTypes); + public boolean isSyncing(String mbid) { + return syncing.contains(mbid); + } - 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; + public boolean isAnySyncing() { + return !syncing.isEmpty(); + } + + public void syncArtist(String mbid) { + syncing.add(mbid); + try { + Set 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); - offset += page.releaseGroups.size(); - more = page.releaseGroups.size() == limit && offset < page.count; + repo.markSynced(mbid); + } finally { + syncing.remove(mbid); } - repo.markSynced(mbid); } public void resyncArtist(String mbid) { diff --git a/src/main/resources/templates/fragments/artist-row.html b/src/main/resources/templates/fragments/artist-row.html index 945b660..8c932ff 100644 --- a/src/main/resources/templates/fragments/artist-row.html +++ b/src/main/resources/templates/fragments/artist-row.html @@ -34,4 +34,16 @@ {/for} + + {#if syncing?? && syncing} +
+ + Syncing releases from MusicBrainz… +
+ + {/if} diff --git a/src/main/resources/templates/fragments/feed-list.html b/src/main/resources/templates/fragments/feed-list.html index 46eda48..8d81db9 100644 --- a/src/main/resources/templates/fragments/feed-list.html +++ b/src/main/resources/templates/fragments/feed-list.html @@ -11,15 +11,20 @@ {/if} {#if !hasMore && rows.empty} -
-

No releases yet.

-

Search for an artist above and click Follow to start tracking their release groups.

+{#if syncing?? && syncing} +
+ +

Fetching releases from MusicBrainz…

-{/if} -{#if autoRefresh?? && autoRefresh} +{#else} +
+

No releases yet.

+

Search for an artist above and click Follow to start tracking their release groups.

+
+{/if} {/if} diff --git a/src/main/resources/templates/fragments/feed-row.html b/src/main/resources/templates/fragments/feed-row.html index 1792fc9..35c8178 100644 --- a/src/main/resources/templates/fragments/feed-row.html +++ b/src/main/resources/templates/fragments/feed-row.html @@ -2,7 +2,7 @@ cover
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 3f30fae..f6419b2 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -65,7 +65,7 @@
-
+
{#include fragments/feed-list.html rows=rows nextOffset=nextOffset hasMore=hasMore pageSize=pageSize /}