Improved player
Build and Publish / Build and Publish Docker Image (push) Successful in 2m34s

This commit is contained in:
Ultradesu
2026-06-03 03:45:23 +03:00
parent 1e1453e465
commit d31dce3ece
5 changed files with 67 additions and 23 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "furumusic" name = "furumusic"
version = "0.2.15" version = "0.3.0"
edition = "2024" edition = "2024"
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL" description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
+1
View File
@@ -273,6 +273,7 @@ translations! {
player_library: "Library" , "Библиотека"; player_library: "Library" , "Библиотека";
player_artists: "Artists" , "Артисты"; player_artists: "Artists" , "Артисты";
player_global_library: "Global" , "Global"; player_global_library: "Global" , "Global";
player_featured_only_artists: "Featured only" , "Только фиты";
player_release: "Release" , "Релиз"; player_release: "Release" , "Релиз";
player_releases: "Releases" , "Релизы"; player_releases: "Releases" , "Релизы";
player_tracks: "Tracks" , "Треки"; player_tracks: "Tracks" , "Треки";
+11
View File
@@ -2048,6 +2048,17 @@ document.addEventListener('alpine:init', () => {
} }
}, },
isFeaturedOnlyArtist(artist) {
return Number(artist?.release_count || 0) <= 0 && Number(artist?.track_count || 0) > 0;
},
shouldShowFeaturedSeparator(index) {
if (!(this.view === 'artists' || this.view === 'my_uploads') || index <= 0) return false;
const current = this.artists[index];
const previous = this.artists[index - 1];
return this.isFeaturedOnlyArtist(current) && !this.isFeaturedOnlyArtist(previous);
},
async openArtist(id, options = {}) { async openArtist(id, options = {}) {
this._beginNavigation('#artist/' + id, options); this._beginNavigation('#artist/' + id, options);
this.searchQuery = ''; this.searchQuery = '';
+29 -22
View File
@@ -523,29 +523,36 @@
<div> <div>
<h1 class="section-title" x-text="$store.library.view === 'my_uploads' ? '{{ t.player_my_uploads }}' : '{{ t.player_global_library }}'"></h1> <h1 class="section-title" x-text="$store.library.view === 'my_uploads' ? '{{ t.player_my_uploads }}' : '{{ t.player_global_library }}'"></h1>
<div class="card-grid"> <div class="card-grid">
<template x-for="artist in $store.library.artists" :key="artist.id"> <template x-for="(artist, idx) in $store.library.artists" :key="artist.id">
<div class="card" @click="$store.library.openArtist(artist.id)"> <div class="artist-grid-entry">
<div class="card-img"> <div class="artist-section-divider"
<template x-if="artist.image_url"> x-show="$store.library.shouldShowFeaturedSeparator(idx)"
<img :src="artist.image_url" :alt="artist.name" loading="lazy"> x-cloak>
</template> <span>{{ t.player_featured_only_artists }}</span>
<template x-if="!artist.image_url"> </div>
<span class="placeholder-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg></span> <div class="card" @click="$store.library.openArtist(artist.id)">
</template> <div class="card-img">
<button class="artist-follow-card-btn" <template x-if="artist.image_url">
:class="{ followed: $store.follows.has(artist.id) }" <img :src="artist.image_url" :alt="artist.name" loading="lazy">
@click.stop="$store.follows.toggle(artist.id)" </template>
:title="$store.follows.has(artist.id) ? '{{ t.player_unfollow_artist }}' : '{{ t.player_follow_artist }}'"> <template x-if="!artist.image_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <span class="placeholder-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="8" r="4"/><path d="M20 21a8 8 0 10-16 0"/></svg></span>
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/> </template>
<circle cx="9" cy="7" r="4"/> <button class="artist-follow-card-btn"
<path x-show="!$store.follows.has(artist.id)" d="M19 8v6M16 11h6"/> :class="{ followed: $store.follows.has(artist.id) }"
<path x-show="$store.follows.has(artist.id)" d="M16 11l2 2 4-5"/> @click.stop="$store.follows.toggle(artist.id)"
</svg> :title="$store.follows.has(artist.id) ? '{{ t.player_unfollow_artist }}' : '{{ t.player_follow_artist }}'">
</button> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 00-4-4H6a4 4 0 00-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path x-show="!$store.follows.has(artist.id)" d="M19 8v6M16 11h6"/>
<path x-show="$store.follows.has(artist.id)" d="M16 11l2 2 4-5"/>
</svg>
</button>
</div>
<div class="card-title" x-text="artist.name"></div>
<div class="card-subtitle" x-text="artist.release_count + ' {{ t.player_releases_count }} · ' + artist.track_count + ' {{ t.player_tracks_count }}'"></div>
</div> </div>
<div class="card-title" x-text="artist.name"></div>
<div class="card-subtitle" x-text="artist.release_count + ' {{ t.player_releases_count }} · ' + artist.track_count + ' {{ t.player_tracks_count }}'"></div>
</div> </div>
</template> </template>
</div> </div>
+25
View File
@@ -513,6 +513,31 @@ button.user-stat:hover {
gap: 20px; gap: 20px;
} }
.artist-grid-entry {
display: contents;
}
.artist-section-divider {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 12px;
min-height: 28px;
margin: 2px 0 -2px;
color: var(--text-subdued);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.artist-section-divider::before,
.artist-section-divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--border-color);
}
.card { .card {
background: var(--bg-secondary); background: var(--bg-secondary);
border-radius: 8px; border-radius: 8px;