|
|
@@ -569,9 +569,6 @@
|
|
|
.now-playing-overlay {
|
|
|
height: calc(100vh - var(--player-h) - 52px) !important; /* compensate for mobile bar */
|
|
|
}
|
|
|
- .np-content { padding: 12px 16px 18px; gap: 10px; }
|
|
|
- .np-art-wrap { width: min(68vw, 300px, 38vh); }
|
|
|
- .np-title { font-size: 18px; }
|
|
|
}
|
|
|
.mobile-player-controls { display: none; }
|
|
|
.mobile-seek-row { display: none; width: 100%; align-items: center; gap: 6px; padding: 2px 0 8px; }
|
|
|
@@ -686,26 +683,46 @@
|
|
|
.main-content { position: relative; }
|
|
|
|
|
|
/* ── Now Playing full-screen overlay ─────────────────────────────── */
|
|
|
+ /* The centre of the overlay follows the Drive music player: a dark
|
|
|
+ stage under the blurred cover art, thin stroked icons, a white play
|
|
|
+ disc, a 4 px seek bar and a spectrum under the title. */
|
|
|
.now-playing-overlay {
|
|
|
+ --np-stage: #171c26;
|
|
|
+ --np-text: #e8ecf3;
|
|
|
+ --np-text2: #9aa4b5;
|
|
|
+ --np-text3: #6d7789;
|
|
|
+ --np-line: rgba(255,255,255,.08);
|
|
|
+ --np-hover: rgba(255,255,255,.12);
|
|
|
+ --np-accent: #5b93ff;
|
|
|
position: absolute; inset: 0;
|
|
|
- z-index: 85; overflow: hidden;
|
|
|
- background: var(--bg);
|
|
|
+ /* clip, not just hidden: the blurred backdrop reaches past the
|
|
|
+ edges and a hidden box can still be scrolled by focus changes */
|
|
|
+ z-index: 85; overflow: hidden; overflow: clip;
|
|
|
+ background: var(--np-stage);
|
|
|
+ color: var(--np-text);
|
|
|
height: calc(100vh - var(--player-h));
|
|
|
}
|
|
|
.now-playing-overlay .np-bg {
|
|
|
- position: absolute; inset: 0; z-index: 0;
|
|
|
+ position: absolute; inset: -60px; z-index: 0;
|
|
|
background-size: cover; background-position: center;
|
|
|
- filter: blur(48px) brightness(.45) saturate(1.3);
|
|
|
- transform: scale(1.35); opacity: .6; pointer-events: none;
|
|
|
+ filter: blur(70px) saturate(1.4);
|
|
|
+ transform: scale(1.1); opacity: .38; pointer-events: none;
|
|
|
}
|
|
|
- .now-playing-overlay::before {
|
|
|
- content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
|
|
|
- background: linear-gradient(180deg, rgba(17,17,20,.45) 0%, rgba(17,17,20,.82) 100%);
|
|
|
+ /* No cover art: the blue tones move to the matching light purples and
|
|
|
+ the backdrop becomes a soft purple glow instead of the blurred art */
|
|
|
+ .now-playing-overlay.np-noart {
|
|
|
+ --np-stage: #1d1728;
|
|
|
+ --np-accent: #b18cff;
|
|
|
+ }
|
|
|
+ .now-playing-overlay.np-noart .np-bg {
|
|
|
+ background-image:
|
|
|
+ radial-gradient(55% 45% at 28% 30%, #a855f7 0%, transparent 70%),
|
|
|
+ radial-gradient(50% 45% at 76% 74%, #6d28d9 0%, transparent 70%);
|
|
|
}
|
|
|
.np-content {
|
|
|
position: relative; z-index: 2;
|
|
|
height: calc(100% ); display: flex; flex-direction: column; align-items: center;
|
|
|
- padding: 14px 22px 42px; gap: 14px; overflow-y: auto;
|
|
|
+ padding: 14px 22px 42px; gap: 22px; overflow-y: auto;
|
|
|
}
|
|
|
.np-slide-left { animation: npSlideL .28s ease; }
|
|
|
.np-slide-right { animation: npSlideR .28s ease; }
|
|
|
@@ -715,12 +732,16 @@
|
|
|
width: 100%; display: flex; align-items: center; justify-content: space-between;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
- .np-header-title { font-size: 13px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text2); }
|
|
|
+ .np-header-title { font-size: 13px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--np-text2); }
|
|
|
+ .np-header .ctrl-btn { color: var(--np-text2); }
|
|
|
+ .np-header .ctrl-btn:hover { color: #fff; background: var(--np-hover); }
|
|
|
+ .np-header .ctrl-btn.active,
|
|
|
+ .np-header .ctrl-btn.active:hover { color: var(--np-accent); }
|
|
|
.np-art-wrap {
|
|
|
position: relative; margin-top: auto;
|
|
|
- width: min(74vw, 360px, 40vh); max-width: 100%; aspect-ratio: 1 / 1;
|
|
|
- border-radius: 14px; overflow: hidden;
|
|
|
- background: var(--bg3); box-shadow: 0 14px 50px rgba(0,0,0,.55);
|
|
|
+ max-width: 100%; aspect-ratio: 1 / 1;
|
|
|
+ border-radius: 20px; overflow: hidden;
|
|
|
+ background: #222936; box-shadow: 0 24px 60px rgba(0,0,0,.55);
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
.np-art { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
|
@@ -730,25 +751,119 @@
|
|
|
background: rgba(0,0,0,.5); backdrop-filter: blur(2px); color: #fff;
|
|
|
}
|
|
|
.np-art-loading i.icon { margin: 0; font-size: 34px; }
|
|
|
+ .np-meta { width: min(560px, 92%); text-align: center; flex-shrink: 0; }
|
|
|
.np-title {
|
|
|
- width: 100%; text-align: center; font-size: 20px; font-weight: 700; color: var(--text);
|
|
|
- line-height: 1.3; max-height: 2.6em; overflow: hidden; flex-shrink: 0;
|
|
|
- }
|
|
|
- .np-artist { width: 100%; text-align: center; font-size: 14px; color: var(--text2); margin-top: -8px; flex-shrink: 0; }
|
|
|
- .np-seek { width: 100%; max-width: 460px; display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
|
|
|
- .np-seek .seek-bar-wrap { flex: 1; }
|
|
|
+ font-size: 24px; font-weight: 700; letter-spacing: -.3px; line-height: 1.3; color: var(--np-text);
|
|
|
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+ /* Marquee for a title wider than the column (.scrolling is set from JS) */
|
|
|
+ .np-mq-copy { display: none; }
|
|
|
+ .np-title.scrolling {
|
|
|
+ text-overflow: clip; text-align: left;
|
|
|
+ -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
|
|
|
+ mask-image: linear-gradient(90deg, transparent 0, #000 16px, #000 calc(100% - 16px), transparent 100%);
|
|
|
+ }
|
|
|
+ .np-title.scrolling .np-mq {
|
|
|
+ display: inline-block; padding-left: 16px;
|
|
|
+ animation: npMarquee var(--np-mq-dur, 12s) linear infinite;
|
|
|
+ }
|
|
|
+ .np-title.scrolling .np-mq-text { padding-right: 56px; }
|
|
|
+ .np-title.scrolling .np-mq-copy { display: inline; }
|
|
|
+ /* Rest at the start of every pass, then slide one copy's width (text +
|
|
|
+ gap, measured in JS) left, where the second copy sits exactly where
|
|
|
+ the first one started */
|
|
|
+ @keyframes npMarquee { 0%, 15% { transform: translateX(0); } 100% { transform: translateX(var(--np-mq-shift, -50%)); } }
|
|
|
+ .np-artist {
|
|
|
+ margin-top: 6px; font-size: 14px; color: var(--np-text2);
|
|
|
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
|
+ }
|
|
|
+ .np-vis { width: min(560px, 92%); height: 64px; display: block; flex-shrink: 0; }
|
|
|
+ .np-seek { width: min(560px, 92%); flex-shrink: 0; }
|
|
|
+ .np-times {
|
|
|
+ display: flex; justify-content: space-between; margin-top: 2px;
|
|
|
+ font-size: 12px; color: var(--np-text2); font-variant-numeric: tabular-nums;
|
|
|
+ }
|
|
|
+ /* Seek bar: a 4 px track that thickens on hover, knob only while pointing */
|
|
|
+ .np-range {
|
|
|
+ --np-track: 4px;
|
|
|
+ -webkit-appearance: none; appearance: none;
|
|
|
+ display: block; width: 100%; height: 20px; margin: 0;
|
|
|
+ background: transparent; outline: none; cursor: pointer; touch-action: none;
|
|
|
+ }
|
|
|
+ .np-range:hover, .np-range:active { --np-track: 6px; }
|
|
|
+ .np-range::-webkit-slider-runnable-track {
|
|
|
+ height: var(--np-track); border-radius: 3px;
|
|
|
+ background: linear-gradient(to right, var(--np-accent) 0%, var(--np-accent) var(--progress, 0%), rgba(255,255,255,.22) var(--progress, 0%), rgba(255,255,255,.22) 100%);
|
|
|
+ }
|
|
|
+ .np-range::-webkit-slider-thumb {
|
|
|
+ -webkit-appearance: none; width: 14px; height: 14px; border: 0; border-radius: 50%;
|
|
|
+ margin-top: calc((var(--np-track) - 14px) / 2);
|
|
|
+ background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4);
|
|
|
+ transform: scale(0); transition: transform .12s;
|
|
|
+ }
|
|
|
+ .np-range:hover::-webkit-slider-thumb, .np-range:active::-webkit-slider-thumb { transform: scale(1); }
|
|
|
+ .np-range::-moz-range-track { height: var(--np-track); border-radius: 3px; background: rgba(255,255,255,.22); }
|
|
|
+ .np-range::-moz-range-progress { height: var(--np-track); border-radius: 3px; background: var(--np-accent); }
|
|
|
+ .np-range::-moz-range-thumb {
|
|
|
+ width: 14px; height: 14px; border: 0; border-radius: 50%;
|
|
|
+ background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.4);
|
|
|
+ transform: scale(0); transition: transform .12s;
|
|
|
+ }
|
|
|
+ .np-range:hover::-moz-range-thumb, .np-range:active::-moz-range-thumb { transform: scale(1); }
|
|
|
.np-controls {
|
|
|
- width: 100%; max-width: 460px; display: flex; align-items: center; justify-content: center;
|
|
|
- gap: 18px; margin-bottom: auto; flex-shrink: 0;
|
|
|
- }
|
|
|
- .np-controls .ctrl-btn { width: 46px; height: 46px; }
|
|
|
- .np-controls .ctrl-btn i.icon { font-size: 18px; }
|
|
|
- .np-controls .np-play { width: 60px; height: 60px; }
|
|
|
- .np-controls .np-play i.icon { font-size: 24px; }
|
|
|
- .np-hint { font-size: 11px; color: var(--text3); text-align: center; flex-shrink: 0; }
|
|
|
+ display: flex; align-items: center; justify-content: center;
|
|
|
+ gap: 14px; margin-bottom: auto; flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .np-btn {
|
|
|
+ width: 44px; height: 44px; border-radius: 22px; border: 0; padding: 0;
|
|
|
+ background: none; color: var(--np-text2); cursor: pointer;
|
|
|
+ display: inline-flex; align-items: center; justify-content: center;
|
|
|
+ flex: none; position: relative;
|
|
|
+ transition: color .12s, background .12s, transform .12s;
|
|
|
+ }
|
|
|
+ .np-btn:hover { color: #fff; background: var(--np-hover); }
|
|
|
+ .np-btn.on { color: var(--np-accent); }
|
|
|
+ .np-btn svg { width: 22px; height: 22px; display: block; }
|
|
|
+ .np-btn.np-play {
|
|
|
+ width: 66px; height: 66px; border-radius: 33px;
|
|
|
+ background: #fff; color: #111620; box-shadow: 0 10px 30px rgba(0,0,0,.35);
|
|
|
+ }
|
|
|
+ .np-btn.np-play:hover { background: #fff; color: #111620; transform: scale(1.04); }
|
|
|
+ .np-btn.np-play svg { width: 28px; height: 28px; }
|
|
|
+ .np-hint { font-size: 11px; color: var(--np-text3); text-align: center; flex-shrink: 0; }
|
|
|
/* Docked song-info section at the bottom of the overlay */
|
|
|
.np-info { width: 100%; max-width: 460px; flex-shrink: 0; padding-bottom: 8px; }
|
|
|
- .np-info .track-info-divider { margin: 6px 0 16px; }
|
|
|
+ .np-info .track-info-divider { margin: 6px 0 16px; background: var(--np-line); }
|
|
|
+ .np-info .track-info-meta { background: rgba(255,255,255,.05); border-color: var(--np-line); }
|
|
|
+ .np-info .track-meta-row { border-bottom-color: var(--np-line); }
|
|
|
+ .np-info .track-meta-label { color: var(--np-text3); }
|
|
|
+ .np-info .track-meta-value { color: var(--np-text); }
|
|
|
+ .np-info .track-meta-path { color: var(--np-text2); }
|
|
|
+ .np-info .track-action-btn { background: rgba(255,255,255,.05); border-color: var(--np-line); color: var(--np-text); }
|
|
|
+ .np-info .track-action-btn:hover { background: var(--np-hover); border-color: rgba(255,255,255,.16); color: #fff; }
|
|
|
+ .np-info .track-action-btn i,
|
|
|
+ .np-info .track-action-btn:hover i { color: var(--np-accent); }
|
|
|
+ /* The cover shrinks with the window height so the header, spectrum and
|
|
|
+ transport controls stay on one page. These follow the base rules
|
|
|
+ above so they win the cascade. */
|
|
|
+ .np-art-wrap { width: min(300px, 38vh, max(160px, calc(100vh - 560px))); }
|
|
|
+ @media (max-height: 760px) {
|
|
|
+ .np-content { gap: 12px; }
|
|
|
+ .np-art-wrap { width: min(300px, max(150px, calc(100vh - 476px))); }
|
|
|
+ .np-vis { height: 40px; }
|
|
|
+ }
|
|
|
+ @media (max-width: 768px) {
|
|
|
+ .np-content { padding: 12px 16px 18px; gap: 16px; }
|
|
|
+ .np-art-wrap { width: min(72vw, 300px, max(150px, calc(100vh - 528px))); }
|
|
|
+ .np-title { font-size: 20px; }
|
|
|
+ .np-vis { height: 48px; }
|
|
|
+ .np-controls { gap: 8px; }
|
|
|
+ }
|
|
|
+ @media (max-width: 768px) and (max-height: 760px) {
|
|
|
+ .np-content { gap: 12px; }
|
|
|
+ .np-art-wrap { width: min(72vw, 300px, max(140px, calc(100vh - 496px))); }
|
|
|
+ .np-vis { height: 40px; }
|
|
|
+ }
|
|
|
/* When Now Playing is open, hide the duplicated transport/seek controls elsewhere */
|
|
|
.np-open .player-controls,
|
|
|
.np-open .mobile-seek-row,
|
|
|
@@ -758,6 +873,7 @@
|
|
|
the overlay's now-redundant (and now-hidden-anyway) copies. */
|
|
|
#app.mobile-landscape.np-open .player-controls { display: flex !important; }
|
|
|
#app.mobile-landscape .np-seek,
|
|
|
+ #app.mobile-landscape .np-vis,
|
|
|
#app.mobile-landscape .np-controls,
|
|
|
#app.mobile-landscape .np-hint { display: none; }
|
|
|
/* Give the now-visible controls room on narrower landscape phones by
|
|
|
@@ -1434,12 +1550,13 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- ── NOW PLAYING OVERLAY ──────────────────────────────────────── -->
|
|
|
- <div x-show="showNowPlaying" class="now-playing-overlay"
|
|
|
+ <div x-show="showNowPlaying" class="now-playing-overlay" :class="{'np-noart': npNoArt}"
|
|
|
x-on:touchstart="npTouchStart($event)"
|
|
|
x-on:touchend="npTouchEnd($event)">
|
|
|
<!-- Blurred album art backdrop -->
|
|
|
+ <!-- Without cover art it falls back to a purple glow (.np-noart) -->
|
|
|
<div class="np-bg"
|
|
|
- :style="currentTrack ? ('background-image:url(\'' + getCoverUrl(currentTrack) + '\')') : ''"></div>
|
|
|
+ :style="currentTrack && !npNoArt ? ('background-image:url(\'' + getCoverUrl(currentTrack) + '\')') : ''"></div>
|
|
|
|
|
|
<div class="np-content" :class="{'np-slide-left': _npSwipeDir==='left', 'np-slide-right': _npSwipeDir==='right'}">
|
|
|
<!-- Header -->
|
|
|
@@ -1457,7 +1574,8 @@
|
|
|
<div class="np-art-wrap">
|
|
|
<img class="np-art" x-show="!privacyVisualizerMode"
|
|
|
:src="currentTrack ? getCoverUrl(currentTrack) : 'img/placeholder.png'"
|
|
|
- x-on:error="$event.target.src='img/placeholder.png'; $event.target.onerror=null"
|
|
|
+ x-on:error="npNoArt = true; $event.target.src='img/placeholder.png'; $event.target.onerror=null"
|
|
|
+ x-on:load="npNoArt = $event.target.src.indexOf('img/placeholder.png') !== -1"
|
|
|
alt="Cover">
|
|
|
<canvas id="np-viz-canvas" class="np-art-viz" x-show="privacyVisualizerMode" title="Album art hidden — Privacy Mode"></canvas>
|
|
|
<div class="np-art-loading" x-show="_fullBufferLoading">
|
|
|
@@ -1466,44 +1584,51 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- Title + artist -->
|
|
|
- <div class="np-title" x-text="currentTrack ? currentTrack.name : ''"></div>
|
|
|
- <div class="np-artist" x-text="currentTrack ? getArtistLabel(currentTrack) : ''"></div>
|
|
|
+ <div class="np-meta">
|
|
|
+ <!-- A title too long for one line scrolls (two copies loop seamlessly) -->
|
|
|
+ <div class="np-title" :title="currentTrack ? currentTrack.name : ''"><span class="np-mq"><span class="np-mq-text" x-text="currentTrack ? currentTrack.name : ''"></span><span class="np-mq-text np-mq-copy" aria-hidden="true" x-text="currentTrack ? currentTrack.name : ''"></span></span></div>
|
|
|
+ <div class="np-artist" x-text="currentTrack ? getArtistLabel(currentTrack) : ''"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Spectrum (needs Web Audio, so never on iOS / iPadOS) -->
|
|
|
+ <canvas id="np-spectrum" class="np-vis" x-show="spectrumSupported"></canvas>
|
|
|
|
|
|
<!-- Seek bar -->
|
|
|
<div class="np-seek">
|
|
|
- <span class="seek-time" x-text="formatTime(currentTime)"></span>
|
|
|
- <div class="seek-bar-wrap">
|
|
|
- <input type="range" class="seek-bar" min="0" :max="Math.floor(duration) || 100"
|
|
|
- :value="Math.floor(currentTime)"
|
|
|
- :style="'--progress:' + progressPercent() + '%'"
|
|
|
- x-on:mousedown="beginSeek()"
|
|
|
- x-on:touchstart.stop="beginSeek()"
|
|
|
- x-on:touchmove.stop="currentTime = parseFloat($event.target.value)"
|
|
|
- x-on:touchend.stop="endSeek($event.target.value)"
|
|
|
- x-on:touchcancel.stop="endSeek($event.target.value)"
|
|
|
- x-on:change="endSeek($event.target.value)"
|
|
|
- x-on:input="currentTime = parseFloat($event.target.value)">
|
|
|
+ <input type="range" class="np-range" min="0" :max="Math.floor(duration) || 100"
|
|
|
+ aria-label="Seek"
|
|
|
+ :value="Math.floor(currentTime)"
|
|
|
+ :style="'--progress:' + progressPercent() + '%'"
|
|
|
+ x-on:mousedown="beginSeek()"
|
|
|
+ x-on:touchstart.stop="beginSeek()"
|
|
|
+ x-on:touchmove.stop="currentTime = parseFloat($event.target.value)"
|
|
|
+ x-on:touchend.stop="endSeek($event.target.value)"
|
|
|
+ x-on:touchcancel.stop="endSeek($event.target.value)"
|
|
|
+ x-on:change="endSeek($event.target.value)"
|
|
|
+ x-on:input="currentTime = parseFloat($event.target.value)">
|
|
|
+ <div class="np-times">
|
|
|
+ <span x-text="formatTime(currentTime)"></span>
|
|
|
+ <span x-text="(_currentTrackTranscoded && !duration) ? '--:--' : formatTime(duration)"></span>
|
|
|
</div>
|
|
|
- <span class="seek-time right" x-text="(_currentTrackTranscoded && !duration) ? '--:--' : formatTime(duration)"></span>
|
|
|
</div>
|
|
|
|
|
|
<!-- Transport controls -->
|
|
|
<div class="np-controls">
|
|
|
- <button class="ctrl-btn" :class="{active: shuffle}" x-on:click="toggleShuffle()" title="Shuffle">
|
|
|
- <i class="ui random icon" style="margin:0;"></i>
|
|
|
+ <button class="np-btn" :class="{on: shuffle}" x-on:click="toggleShuffle()" title="Shuffle">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7h3.5c2 0 3.2 1 4.3 2.7l2.4 4.6c1.1 1.7 2.3 2.7 4.3 2.7H21"/><path d="M3 17h3.5c1.4 0 2.4-.5 3.2-1.4M14.3 8.4c.8-.9 1.8-1.4 3.2-1.4H21"/><path d="M18.5 4.5 21 7l-2.5 2.5M18.5 14.5 21 17l-2.5 2.5"/></svg>
|
|
|
</button>
|
|
|
- <button class="ctrl-btn" x-on:click="prevTrack()" title="Previous">
|
|
|
- <i class="ui step backward icon" style="margin:0;"></i>
|
|
|
+ <button class="np-btn" x-on:click="prevTrack()" title="Previous">
|
|
|
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 5h2v14H6zM19 5.5v13l-9.5-6.5z"/></svg>
|
|
|
</button>
|
|
|
- <button class="ctrl-btn play-btn np-play" x-on:click="togglePlay()" :title="isPlaying ? 'Pause' : 'Play'">
|
|
|
- <i :class="isPlaying ? 'pause' : 'play'" class="ui icon" style="margin:0;"></i>
|
|
|
+ <button class="np-btn np-play" x-on:click="togglePlay()" :title="isPlaying ? 'Pause' : 'Play'">
|
|
|
+ <svg x-show="!isPlaying" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 5.5v13l10-6.5z"/></svg>
|
|
|
+ <svg x-show="isPlaying" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6v12M15 6v12"/></svg>
|
|
|
</button>
|
|
|
- <button class="ctrl-btn" x-on:click="nextTrack()" title="Next">
|
|
|
- <i class="ui step forward icon" style="margin:0;"></i>
|
|
|
+ <button class="np-btn" x-on:click="nextTrack()" title="Next">
|
|
|
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 5h2v14h-2zM5 5.5v13l9.5-6.5z"/></svg>
|
|
|
</button>
|
|
|
- <button class="ctrl-btn" :class="{active: repeat !== 'none'}" x-on:click="cycleRepeat()" :title="repeatTitle()">
|
|
|
- <i :class="repeatIcon()" class="ui icon" style="margin:0;"></i>
|
|
|
- <span x-show="repeat==='one'" style="position:absolute;font-size:9px;font-weight:900;line-height:1;bottom:3px;right:3px;color:var(--accent);">1</span>
|
|
|
+ <button class="np-btn" :class="{on: repeat !== 'none'}" x-on:click="cycleRepeat()" :title="repeatTitle()">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 11V9a3 3 0 0 1 3-3h12"/><path d="M16 3l3 3-3 3"/><path d="M20 13v2a3 3 0 0 1-3 3H5"/><path d="M8 21l-3-3 3-3"/><text x-show="repeat === 'one'" x="12" y="14.6" text-anchor="middle" font-size="7.5" font-weight="700" fill="currentColor" stroke="none" font-family="Segoe UI,Arial,sans-serif">1</text></svg>
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
@@ -1654,6 +1779,24 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- Display subsection -->
|
|
|
+ <div class="settings-section">
|
|
|
+ <div class="settings-section-title">Display</div>
|
|
|
+ <div class="settings-section-desc">
|
|
|
+ How the Now Playing view shows the current track.
|
|
|
+ </div>
|
|
|
+ <div class="settings-toggle-row">
|
|
|
+ <div>
|
|
|
+ <div class="settings-toggle-label">Scroll Long Titles</div>
|
|
|
+ <div class="settings-toggle-desc" x-text="scrollLongTitles ? 'Active – a title too long for one line scrolls slowly in a loop' : 'Inactive – long titles are cut off with …'"></div>
|
|
|
+ </div>
|
|
|
+ <label class="toggle-switch">
|
|
|
+ <input type="checkbox" x-model="scrollLongTitles" x-on:change="saveScrollLongTitles()">
|
|
|
+ <span class="toggle-slider"></span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<!-- Mobile Volume subsection -->
|
|
|
<div class="settings-section">
|
|
|
<div class="settings-section-title">Mobile Volume</div>
|