PLAYER: added simple rating
Build and Publish / Build and Publish Docker Image (push) Successful in 2m55s
Build and Publish / Build and Publish Docker Image (push) Successful in 2m55s
This commit is contained in:
Generated
+1
-1
@@ -1418,7 +1418,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "furumusic"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumusic"
|
||||
version = "0.1.17"
|
||||
version = "0.1.18"
|
||||
edition = "2024"
|
||||
description = "Reusable web-app boilerplate: auth, OIDC/SSO, admin panel, user management, i18n, PostgreSQL"
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const T = {
|
||||
info: "{{ t.player_info }}",
|
||||
noDetails: "{{ t.player_no_details }}",
|
||||
trackInfoTitle: "{{ t.player_track_info }}",
|
||||
loadingHistory: "{{ t.player_loading_history }}",
|
||||
failedLoadHistory: "{{ t.player_failed_load_history }}",
|
||||
totalPlays: "{{ t.player_total_plays }}",
|
||||
@@ -853,6 +854,47 @@ document.addEventListener('alpine:init', () => {
|
||||
return (idx === 0 ? size.toFixed(0) : size.toFixed(1)) + ' ' + units[idx];
|
||||
},
|
||||
|
||||
trackPopularityValue(track) {
|
||||
const value = Number(track?.lastfm_rating);
|
||||
return Number.isFinite(value) && value > 0 ? value : null;
|
||||
},
|
||||
|
||||
hasPopularity(track) {
|
||||
return this.trackPopularityValue(track) != null;
|
||||
},
|
||||
|
||||
popularityLabel(track) {
|
||||
const value = this.trackPopularityValue(track);
|
||||
if (value == null) return 'i';
|
||||
if (value >= 10000) return Math.round(value / 1000) + 'k';
|
||||
if (value >= 1000) return (value / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
|
||||
return Math.round(value).toString();
|
||||
},
|
||||
|
||||
popularityStyle(track) {
|
||||
const value = this.trackPopularityValue(track);
|
||||
if (value == null) return '';
|
||||
const t = Math.max(0, Math.min(1, Math.log1p(value) / Math.log1p(180)));
|
||||
const hue = 210 - (190 * t);
|
||||
const saturation = 42 + (46 * t);
|
||||
const lightness = 30 + (16 * t);
|
||||
const bg = `hsla(${hue.toFixed(0)}, ${saturation.toFixed(0)}%, ${lightness.toFixed(0)}%, 0.28)`;
|
||||
const hoverBg = `hsla(${hue.toFixed(0)}, ${saturation.toFixed(0)}%, ${Math.min(54, lightness + 6).toFixed(0)}%, 0.36)`;
|
||||
const border = `hsla(${hue.toFixed(0)}, ${saturation.toFixed(0)}%, ${Math.min(66, lightness + 16).toFixed(0)}%, 0.52)`;
|
||||
const fg = `hsl(${hue.toFixed(0)}, ${Math.min(96, saturation + 8).toFixed(0)}%, ${Math.min(86, lightness + 34).toFixed(0)}%)`;
|
||||
return `--popularity-bg:${bg};--popularity-hover-bg:${hoverBg};--popularity-border:${border};--popularity-fg:${fg}`;
|
||||
},
|
||||
|
||||
trackInfoTitle(track) {
|
||||
const value = this.trackPopularityValue(track);
|
||||
if (value == null) return this.trackInfo(track);
|
||||
return `${T.lastfmRating}: ${Math.round(value)}\n${this.trackInfo(track)}`;
|
||||
},
|
||||
|
||||
openTrackInfo(track) {
|
||||
Alpine.store('info').open(T.trackInfoTitle, this.trackInfo(track));
|
||||
},
|
||||
|
||||
uploadersInfo(uploaders) {
|
||||
const rows = uploaders || [];
|
||||
if (!rows.length) return 'UFO';
|
||||
@@ -893,7 +935,7 @@ document.addEventListener('alpine:init', () => {
|
||||
];
|
||||
if (track.lastfm_rating != null || track.lastfm_listeners != null || track.lastfm_playcount != null) {
|
||||
const rating = Number(track.lastfm_rating || 0);
|
||||
lines.push(`${T.lastfmRating}: ${Number.isFinite(rating) ? rating.toFixed(2) : T.unknown}`);
|
||||
lines.push(`${T.lastfmRating}: ${Number.isFinite(rating) ? Math.round(rating) : T.unknown}`);
|
||||
lines.push(`${T.lastfmListeners}: ${new Intl.NumberFormat().format(track.lastfm_listeners || 0)}`);
|
||||
lines.push(`${T.lastfmPlaycount}: ${new Intl.NumberFormat().format(track.lastfm_playcount || 0)}`);
|
||||
if (track.lastfm_updated_at) lines.push(`${T.lastfmUpdated}: ${track.lastfm_updated_at}`);
|
||||
|
||||
+40
-10
@@ -434,8 +434,14 @@
|
||||
</div>
|
||||
<span></span>
|
||||
<div class="track-actions">
|
||||
<button class="track-action-btn info-btn" @click.stop="$store.info.open('{{ t.player_track_info }}', $store.library.trackInfo(track))" :title="$store.library.trackInfo(track)" aria-label="{{ t.player_track_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
<button class="track-action-btn info-btn popularity-info-btn"
|
||||
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
|
||||
:style="$store.library.popularityStyle(track)"
|
||||
@click.stop="$store.library.openTrackInfo(track)"
|
||||
:title="$store.library.trackInfoTitle(track)"
|
||||
aria-label="{{ t.player_track_info }}">
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.library.playSearchTrack(idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
@@ -608,8 +614,14 @@
|
||||
</div>
|
||||
<span></span>
|
||||
<div class="track-actions">
|
||||
<button class="track-action-btn info-btn" @click.stop="$store.info.open('{{ t.player_track_info }}', $store.library.trackInfo(track))" :title="$store.library.trackInfo(track)" aria-label="{{ t.player_track_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
<button class="track-action-btn info-btn popularity-info-btn"
|
||||
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
|
||||
:style="$store.library.popularityStyle(track)"
|
||||
@click.stop="$store.library.openTrackInfo(track)"
|
||||
:title="$store.library.trackInfoTitle(track)"
|
||||
aria-label="{{ t.player_track_info }}">
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease($store.library.currentArtist.featured_tracks, idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
@@ -725,8 +737,14 @@
|
||||
</div>
|
||||
<span></span>
|
||||
<div class="track-actions">
|
||||
<button class="track-action-btn info-btn" @click.stop="$store.info.open('{{ t.player_track_info }}', $store.library.trackInfo(track))" :title="$store.library.trackInfo(track)" aria-label="{{ t.player_track_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
<button class="track-action-btn info-btn popularity-info-btn"
|
||||
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
|
||||
:style="$store.library.popularityStyle(track)"
|
||||
@click.stop="$store.library.openTrackInfo(track)"
|
||||
:title="$store.library.trackInfoTitle(track)"
|
||||
aria-label="{{ t.player_track_info }}">
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease($store.library.currentRelease.tracks, idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
@@ -795,8 +813,14 @@
|
||||
</div>
|
||||
<span></span>
|
||||
<div class="track-actions">
|
||||
<button class="track-action-btn info-btn" @click.stop="$store.info.open('{{ t.player_track_info }}', $store.library.trackInfo(track))" :title="$store.library.trackInfo(track)" aria-label="{{ t.player_track_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
<button class="track-action-btn info-btn popularity-info-btn"
|
||||
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
|
||||
:style="$store.library.popularityStyle(track)"
|
||||
@click.stop="$store.library.openTrackInfo(track)"
|
||||
:title="$store.library.trackInfoTitle(track)"
|
||||
aria-label="{{ t.player_track_info }}">
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="track-action-btn play-btn" @click.stop="$store.queue.playRelease($store.library.currentPlaylist.tracks, idx)" title="{{ t.player_play }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
@@ -871,8 +895,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-track-actions">
|
||||
<button class="queue-track-remove info-btn" @click.stop="$store.info.open('{{ t.player_track_info }}', $store.library.trackInfo(track))" :title="$store.library.trackInfo(track)" aria-label="{{ t.player_track_info }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
<button class="queue-track-remove info-btn popularity-info-btn"
|
||||
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
|
||||
:style="$store.library.popularityStyle(track)"
|
||||
@click.stop="$store.library.openTrackInfo(track)"
|
||||
:title="$store.library.trackInfoTitle(track)"
|
||||
aria-label="{{ t.player_track_info }}">
|
||||
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span>
|
||||
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span>
|
||||
</button>
|
||||
<button class="queue-track-remove" @click.stop="$store.queue.remove(idx)" title="{{ t.player_remove }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
|
||||
@@ -661,11 +661,20 @@ button.user-stat:hover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.track-row:hover .track-actions { opacity: 1; }
|
||||
.track-actions > :not(.popularity-info-btn) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.track-row:hover .track-actions > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.track-action-btn {
|
||||
background: none;
|
||||
@@ -692,6 +701,43 @@ button.user-stat:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.popularity-info-btn {
|
||||
min-width: 26px;
|
||||
height: 20px;
|
||||
padding: 0 3px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.popularity-info-btn.has-popularity {
|
||||
color: var(--popularity-fg, var(--text-primary));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.popularity-info-btn.has-popularity:hover {
|
||||
color: var(--popularity-fg, var(--text-primary));
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.popularity-info-btn.no-popularity {
|
||||
min-width: 18px;
|
||||
width: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popularity-info-btn .info-letter {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-info-btn {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
@@ -926,12 +972,21 @@ button.user-stat:hover {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.queue-track:hover .queue-track-actions { opacity: 1; }
|
||||
.queue-track-actions > :not(.popularity-info-btn) {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.queue-track:hover .queue-track-actions > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.queue-track-remove {
|
||||
background: none;
|
||||
@@ -948,6 +1003,20 @@ button.user-stat:hover {
|
||||
|
||||
.queue-track-remove:hover { color: var(--text-primary); background: var(--bg-hover); }
|
||||
|
||||
.queue-track-remove.popularity-info-btn {
|
||||
min-width: 26px;
|
||||
width: auto;
|
||||
height: 20px;
|
||||
padding: 0 3px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.queue-track-remove.popularity-info-btn.no-popularity {
|
||||
min-width: 18px;
|
||||
width: 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Drag handle */
|
||||
.queue-drag-handle {
|
||||
cursor: grab;
|
||||
@@ -2807,6 +2876,24 @@ button.user-stat:hover {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.popularity-info-btn {
|
||||
min-width: 28px;
|
||||
height: 22px;
|
||||
padding: 0 3px;
|
||||
}
|
||||
|
||||
.popularity-info-btn.no-popularity {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.track-actions > *,
|
||||
.queue-track-actions > * {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.track-action-btn svg,
|
||||
.like-btn svg {
|
||||
width: 17px;
|
||||
|
||||
Reference in New Issue
Block a user