Added user page. fixed player.
Build and Publish / Build and Publish Docker Image (push) Successful in 3m32s

This commit is contained in:
Ultradesu
2026-06-02 20:45:00 +03:00
parent a1dafaa5f2
commit f716c22f86
10 changed files with 835 additions and 25 deletions
+34
View File
@@ -46,6 +46,7 @@ const T = {
radioFailed: "{{ t.player_radio_failed }}",
connectionLost: "{{ t.player_connection_lost }}",
connectionLostDetail: "{{ t.player_connection_lost_detail }}",
activeDevice: "{{ t.player_active_device }}",
trackWord: "{{ t.player_tracks_count }}",
releaseWord: "{{ t.player_releases_count }}",
ofWord: "{{ t.player_of }}",
@@ -1176,6 +1177,10 @@ document.addEventListener('alpine:init', () => {
jamUsers: [],
jamSelectedUsers: [],
jamSearching: false,
remoteHintVisible: false,
remoteHintDeviceName: '',
_remoteHintShown: false,
_remoteHintTimer: null,
_pollTimer: null,
_jamSearchTimer: null,
_stateRefreshTick: 0,
@@ -1265,6 +1270,34 @@ document.addEventListener('alpine:init', () => {
if (wasActive && !this.isActive()) {
Alpine.store('player')?._pauseLocal();
}
this._maybeShowRemoteHint();
},
remoteActiveDevice() {
if (!this.activeDeviceId || this.activeDeviceId === this.id) return null;
return this.devices.find(device => device.id === this.activeDeviceId) || null;
},
_maybeShowRemoteHint() {
if (this._remoteHintShown || this.remoteHintVisible || this.open) return;
const active = this.remoteActiveDevice();
if (!active) return;
this.remoteHintDeviceName = active.name || 'Devices';
this.remoteHintVisible = true;
this._remoteHintShown = true;
clearTimeout(this._remoteHintTimer);
this._remoteHintTimer = setTimeout(() => this.dismissRemoteHint(), 60000);
},
dismissRemoteHint() {
this.remoteHintVisible = false;
this._remoteHintShown = true;
clearTimeout(this._remoteHintTimer);
this._remoteHintTimer = null;
},
remoteHintText() {
return `${T.activeDevice}: ${this.remoteHintDeviceName || this.activeLabel()}`;
},
selectedJam() {
@@ -1330,6 +1363,7 @@ document.addEventListener('alpine:init', () => {
},
toggle() {
this.dismissRemoteHint();
this.open = !this.open;
if (this.open) this.poll();
},
+36 -12
View File
@@ -639,23 +639,37 @@
<span style="text-align:right">{{ t.player_duration }}</span>
</div>
<template x-for="(track, idx) in $store.library.currentArtist.featured_tracks" :key="track.id">
<div class="track-row"
<div class="track-row artist-appearance-row"
:class="{ playing: $store.player.currentTrack && $store.player.currentTrack.id === track.id }"
@dblclick="$store.queue.playRelease($store.library.currentArtist.featured_tracks, idx)">
<span class="track-num" x-text="idx + 1"></span>
<div class="track-info">
<div class="track-title">
<span x-text="track.title"></span>
<span style="color:var(--text-subdued)"> · </span>
<a class="artist-link" @click.stop="$store.library.openRelease(track.release_id)" x-text="track.release_title"></a>
</div>
<div class="track-artists-inline">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
<button class="artist-appearance-cover"
type="button"
@click.stop="$store.library.openRelease(track.release_id)"
:title="track.release_title"
aria-label="{{ t.player_release }}">
<template x-if="track.cover_url">
<img :src="track.cover_url" :alt="track.release_title" loading="lazy">
</template>
<template x-if="!track.cover_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="4"/></svg>
</template>
</button>
<div class="artist-appearance-copy">
<div class="track-title">
<span x-text="track.title"></span>
<span style="color:var(--text-subdued)"> · </span>
<a class="artist-link" @click.stop="$store.library.openRelease(track.release_id)" x-text="track.release_title"></a>
</div>
<div class="track-artists-inline">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
</span>
</template>
</div>
</div>
</div>
<span></span>
@@ -1074,6 +1088,16 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
</button>
<div class="device-picker" @click.outside="$store.devices.open = false">
<template x-if="$store.devices.remoteHintVisible">
<button class="remote-playback-hint"
type="button"
@click="$store.devices.dismissRemoteHint()"
x-transition
x-cloak>
<span class="remote-playback-hint-text" x-text="$store.devices.remoteHintText()"></span>
<span class="remote-playback-hint-progress"></span>
</button>
</template>
<button class="queue-toggle-btn device-toggle-btn"
:class="{ active: $store.devices.isActive() }"
@click="$store.devices.toggle()"
+126 -10
View File
@@ -735,6 +735,47 @@ button.user-stat:hover {
margin-top: 2px;
}
.artist-appearance-row .track-info {
display: flex;
align-items: center;
gap: 12px;
}
.artist-appearance-cover {
width: 42px;
height: 42px;
border: 0;
border-radius: 4px;
background: var(--bg-elevated);
color: var(--text-subdued);
cursor: pointer;
flex: 0 0 42px;
overflow: hidden;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.artist-appearance-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.artist-appearance-cover svg {
width: 22px;
height: 22px;
}
.artist-appearance-cover:hover {
filter: brightness(1.14);
}
.artist-appearance-copy {
min-width: 0;
}
.track-album { font-size: 13px; color: var(--text-subdued); }
.track-duration { font-size: 13px; color: var(--text-subdued); text-align: right; pointer-events: none; }
@@ -1370,19 +1411,35 @@ button.user-stat:hover {
.volume-slider {
width: 104px;
height: 4px;
background: var(--bg-active);
border-radius: 2px;
height: 28px;
background: transparent;
border-radius: 6px;
cursor: pointer;
position: relative;
touch-action: none;
display: flex;
align-items: center;
}
.volume-slider::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 4px;
border-radius: 2px;
background: var(--bg-active);
transform: translateY(-50%);
}
.volume-slider-fill {
height: 100%;
height: 4px;
background: var(--text-primary);
border-radius: 2px;
position: relative;
z-index: 1;
pointer-events: none;
}
.volume-slider:hover .volume-slider-fill { background: var(--accent); }
@@ -1452,6 +1509,51 @@ button.user-stat:hover {
justify-content: center;
}
.remote-playback-hint {
position: absolute;
right: 0;
bottom: 38px;
width: max-content;
max-width: min(280px, calc(100vw - 24px));
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-elevated);
color: var(--text-primary);
box-shadow: 0 14px 34px rgba(0,0,0,0.46);
cursor: pointer;
font: inherit;
overflow: hidden;
padding: 10px 12px 12px;
text-align: left;
z-index: 44;
}
.remote-playback-hint-text {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
line-height: 1.35;
}
.remote-playback-hint-progress {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 2px;
background: var(--accent);
transform-origin: left center;
animation: remote-playback-hint-progress 60s linear forwards;
}
@keyframes remote-playback-hint-progress {
from { transform: scaleX(1); }
to { transform: scaleX(0); }
}
.device-popover {
position: absolute;
right: 0;
@@ -4164,13 +4266,19 @@ button.user-stat:hover {
}
.volume-slider {
display: block;
display: flex;
width: 100%;
height: 9px;
border-radius: 999px;
height: 34px;
border-radius: 6px;
}
.volume-slider-fill {
height: 7px;
border-radius: 999px;
}
.volume-slider::before {
height: 7px;
border-radius: 999px;
}
@@ -4417,8 +4525,8 @@ button.user-stat:hover {
}
.player-bar.mobile-expanded .mobile-full-player .volume-slider {
display: block;
height: 7px;
display: flex;
height: 44px;
}
.player-bar.mobile-expanded .mobile-full-player .device-popover {
@@ -4432,6 +4540,14 @@ button.user-stat:hover {
max-height: 42dvh;
}
.player-bar.mobile-expanded .mobile-full-player .remote-playback-hint {
position: fixed;
top: auto;
bottom: calc(90px + var(--safe-bottom));
right: 18px;
max-width: calc(100vw - 36px);
}
.player-bar.mobile-expanded .mobile-full-player .mobile-expanded-queue {
display: block;
width: 100%;
@@ -5053,7 +5169,7 @@ button.user-stat:hover {
}
.volume-slider {
display: block;
display: flex;
width: 100%;
}