PLAYER: reworked player panel and fulscreen
Build and Publish / Build and Publish Docker Image (push) Successful in 3m1s

This commit is contained in:
2026-05-29 02:56:14 +03:00
parent de7626a6a9
commit 97c82b4ba2
4 changed files with 103 additions and 25 deletions
Generated
+1 -1
View File
@@ -1418,7 +1418,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "furumusic"
version = "0.2.7"
version = "0.2.8"
dependencies = [
"anyhow",
"async-trait",
+41 -16
View File
@@ -170,10 +170,14 @@ document.addEventListener('alpine:init', () => {
playerDragOffset: 0,
playerCloseOffset: 0,
_playerDragStartY: 0,
_playerDragStartX: 0,
_playerDragTracking: false,
_playerDragMode: null,
_playerDragPointerId: null,
_playerDragElement: null,
_playerDragMove: null,
_playerDragEnd: null,
_playerSuppressClickUntil: 0,
toggleLibrary() {
this.libraryOpen = !this.libraryOpen;
if (this.libraryOpen) Alpine.store('user').menuOpen = false;
@@ -202,31 +206,37 @@ document.addEventListener('alpine:init', () => {
playerDragStyle() {
return `--mobile-player-drag:${this.playerDragOffset}px; --mobile-player-close-drag:${this.playerCloseOffset}px;`;
},
handlePlayerClick(event) {
if (Date.now() <= this._playerSuppressClickUntil) {
event.preventDefault();
event.stopPropagation();
this._playerSuppressClickUntil = 0;
}
},
startPlayerDrag(event, force = false) {
if (!this.isMobilePlayer() || !Alpine.store('player').currentTrack) return;
if (event.button && event.button !== 0) return;
const target = event.target;
const isInteractive = target.closest('button, input, select, textarea, a, .volume-slider, .progress-bar, .device-popover, .mobile-expanded-queue');
const isDragBlocked = target.closest('input, select, textarea, .volume-slider, .device-popover, .mobile-expanded-queue');
if (!force) {
if (this.playerExpanded) {
const isCloseHandle = target.closest('.player-now-playing');
const scroller = event.currentTarget?.classList?.contains('player-bar') ? event.currentTarget : null;
if (!isCloseHandle || isInteractive || (scroller && scroller.scrollTop > 4)) return;
} else if (isInteractive) {
if (!isCloseHandle || isDragBlocked || (scroller && scroller.scrollTop > 4)) return;
} else if (isDragBlocked) {
return;
}
}
event.preventDefault();
if (this.playerDragging) this.endPlayerDrag({ type: 'pointercancel' });
this.playerDragging = true;
if (this._playerDragTracking) this.endPlayerDrag({ type: 'pointercancel' });
this.playerDragging = false;
this._playerDragTracking = true;
this._playerDragMode = this.playerExpanded ? 'close' : 'open';
this._playerDragStartY = event.clientY;
this._playerDragStartX = event.clientX;
this._playerDragPointerId = event.pointerId;
this._playerDragElement = event.currentTarget;
this.playerDragOffset = 0;
this.playerCloseOffset = 0;
try {
this._playerDragElement?.setPointerCapture?.(event.pointerId);
} catch (_) {}
this._playerDragMove = e => this.movePlayerDrag(e);
this._playerDragEnd = e => this.endPlayerDrag(e);
window.addEventListener('pointermove', this._playerDragMove, { passive: false });
@@ -234,11 +244,22 @@ document.addEventListener('alpine:init', () => {
window.addEventListener('pointercancel', this._playerDragEnd, { passive: false });
},
movePlayerDrag(event) {
if (!this.playerDragging) return;
if (!this._playerDragTracking) return;
const delta = this._playerDragStartY - event.clientY;
if (Math.abs(delta) > 6) event.preventDefault();
if (this.playerExpanded) {
this.playerCloseOffset = Math.max(0, Math.min(180, -delta));
const absDelta = Math.abs(delta);
if (!this.playerDragging) {
const horizontalDelta = Math.abs(event.clientX - this._playerDragStartX);
const wantsOpen = this._playerDragMode === 'open' && delta > 0;
const wantsClose = this._playerDragMode === 'close' && delta < 0;
if (absDelta < 8 || absDelta < horizontalDelta * 1.15 || (!wantsOpen && !wantsClose)) return;
this.playerDragging = true;
try {
this._playerDragElement?.setPointerCapture?.(this._playerDragPointerId);
} catch (_) {}
}
event.preventDefault();
if (this._playerDragMode === 'close') {
this.playerCloseOffset = Math.max(0, Math.min(window.innerHeight, -delta));
} else {
const max = Math.max(0, window.innerHeight - 132);
this.playerDragOffset = Math.max(0, Math.min(max, delta));
@@ -246,15 +267,17 @@ document.addEventListener('alpine:init', () => {
},
endPlayerDrag(event) {
const openThreshold = Math.min(180, Math.max(90, window.innerHeight * 0.18));
const closeThreshold = 110;
const closeThreshold = Math.min(110, Math.max(64, window.innerHeight * 0.1));
const wasCancelled = event?.type === 'pointercancel';
if (this.playerExpanded) {
const wasDragging = this.playerDragging;
if (wasDragging) this._playerSuppressClickUntil = Date.now() + 450;
if (this._playerDragMode === 'close') {
if (!wasCancelled && this.playerCloseOffset > closeThreshold) this.closePlayerFullscreen();
else {
this.playerCloseOffset = 0;
this.playerDragging = false;
}
} else if (!wasCancelled && this.playerDragOffset > openThreshold) {
} else if (this._playerDragMode === 'open' && !wasCancelled && this.playerDragOffset > openThreshold) {
this.openPlayerFullscreen();
} else {
this.playerDragOffset = 0;
@@ -270,6 +293,8 @@ document.addEventListener('alpine:init', () => {
window.removeEventListener('pointerup', this._playerDragEnd);
window.removeEventListener('pointercancel', this._playerDragEnd);
}
this._playerDragTracking = false;
this._playerDragMode = null;
this._playerDragPointerId = null;
this._playerDragElement = null;
this._playerDragMove = null;
+4 -2
View File
@@ -946,6 +946,7 @@
<div class="player-bar"
:class="{ 'mobile-expanded': $store.mobile.playerExpanded, 'mobile-dragging': $store.mobile.playerDragging }"
:style="$store.mobile.playerDragStyle()"
@click.capture="$store.mobile.handlePlayerClick($event)"
@pointerdown="$store.mobile.startPlayerDrag($event)">
<button class="mobile-player-collapse-btn" type="button" @click.stop="$store.mobile.closePlayerFullscreen()" title="{{ t.player_close }}" aria-label="{{ t.player_close }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">
@@ -956,8 +957,7 @@
<template x-if="$store.player.currentTrack">
<div style="display:flex;align-items:center;gap:12px;overflow:hidden">
<div class="player-cover"
@click.stop="$store.mobile.openPlayerFullscreen()"
@pointerdown.stop="$store.mobile.startPlayerDrag($event, true)">
@click.stop="$store.mobile.openPlayerFullscreen()">
<template x-if="$store.player.currentTrack.cover_url">
<img :src="$store.player.currentTrack.cover_url" :alt="$store.player.currentTrack.title">
</template>
@@ -1030,6 +1030,8 @@
<div class="progress-bar-thumb"></div>
</div>
</div>
<div class="player-progress-strip-times"
x-text="'-' + formatTime(Math.max(0, $store.player.duration - $store.player.currentTime)) + ' / ' + formatTime($store.player.duration)"></div>
<span class="player-time" x-text="formatTime($store.player.duration)"></span>
</div>
<div class="player-version-chip">v{{ t.app_version() }}</div>
+57 -6
View File
@@ -1261,6 +1261,10 @@ button.user-stat:hover {
.player-time { font-size: 11px; color: var(--text-subdued); min-width: 40px; text-align: center; }
.player-progress-strip-times {
display: none;
}
.progress-bar {
flex: 1;
height: 4px;
@@ -3642,14 +3646,13 @@ button.user-stat:hover {
.player-bar {
position: relative;
grid-template-columns: auto minmax(0, 1fr);
grid-template-rows: 62px 58px 24px;
grid-template-rows: 62px 58px;
grid-template-areas:
"now now"
"buttons actions"
"timeline timeline";
"buttons actions";
gap: 4px 10px;
align-items: center;
padding: 7px 12px calc(9px + var(--safe-bottom));
padding: 34px 12px calc(9px + var(--safe-bottom));
touch-action: none;
user-select: none;
}
@@ -3742,13 +3745,61 @@ button.user-stat:hover {
}
.player-timeline {
grid-area: timeline;
max-width: none;
gap: 5px;
align-self: center;
padding-right: 58px;
}
.player-bar:not(.mobile-expanded) .player-timeline {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 21px;
gap: 0;
padding-right: 0;
z-index: 1;
}
.player-bar:not(.mobile-expanded) .player-time {
display: none;
}
.player-bar:not(.mobile-expanded) .progress-bar,
.player-bar:not(.mobile-expanded) .progress-bar:hover {
width: 100%;
height: 21px;
border-radius: 0;
background: rgba(29, 185, 84, 0.18);
}
.player-bar:not(.mobile-expanded) .progress-bar-fill {
border-radius: 0;
background: var(--accent);
}
.player-bar:not(.mobile-expanded) .progress-bar-thumb {
display: none;
}
.player-bar:not(.mobile-expanded) .player-progress-strip-times {
position: absolute;
top: 22px;
right: 10px;
height: 10px;
display: flex;
align-items: center;
padding: 0;
color: var(--text-subdued);
font-size: 9px;
font-weight: 700;
line-height: 1;
pointer-events: none;
text-shadow: none;
white-space: nowrap;
}
.player-version-chip {
display: block;
position: absolute;
@@ -4603,7 +4654,7 @@ button.user-stat:hover {
gap: 8px;
padding-left: 10px;
padding-right: 10px;
grid-template-rows: 60px 58px 24px;
grid-template-rows: 60px 58px;
}
.player-track-title { font-size: 12px; }