Add sync loading indicators: spinner in feed when fetching releases, syncing indicator on artist row when toggling types
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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 /}
|
||||||
|
|||||||
Reference in New Issue
Block a user