update: Playlist mode toggle und Mini player bar für Audiobücher
fix: Music starts → ABS pauses: ABS starts → Music pauses
This commit is contained in:
1 parent
88999d30ca
commit
8f126562e7
4 files changed
+100
-3
No files matched your search
+15
-1
@@ -435,6 +435,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ABS MINI BAR -->
|
||||||
|
<div id="abs-mini-bar" class="abs-mini-bar" onclick="absOpenPlayer()">
|
||||||
|
<div id="abs-mini-cover" class="abs-mini-cover">📚</div>
|
||||||
|
<div class="abs-mini-info">
|
||||||
|
<div id="abs-mini-title" class="abs-mini-title">—</div>
|
||||||
|
<div id="abs-mini-chapter" class="abs-mini-chapter"></div>
|
||||||
|
</div>
|
||||||
|
<button class="abs-mini-play" id="abs-mini-play" onclick="event.stopPropagation();absTogglePlay()">
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- PREVIEW BAR (iTunes 30s) -->
|
<!-- PREVIEW BAR (iTunes 30s) -->
|
||||||
<div class="preview-bar" id="preview-bar">
|
<div class="preview-bar" id="preview-bar">
|
||||||
<div class="player-art" id="preview-art" style="width:42px;height:42px;border-radius:6px"><div class="no-art" style="font-size:16px">♪</div></div>
|
<div class="player-art" id="preview-art" style="width:42px;height:42px;border-radius:6px"><div class="no-art" style="font-size:16px">♪</div></div>
|
||||||
@@ -586,7 +598,9 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
|
||||||
</button>
|
</button>
|
||||||
<span class="abs-player-label">Hörbuch</span>
|
<span class="abs-player-label">Hörbuch</span>
|
||||||
<div style="width:36px"></div>
|
<button class="abs-hbtn" id="abs-playlist-btn" onclick="absTogglePlaylistMode()" title="Kapitelliste fixieren">
|
||||||
|
<svg width="18" height="18" 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"/><circle cx="3" cy="6" r="1.2" fill="currentColor" stroke="none"/><circle cx="3" cy="12" r="1.2" fill="currentColor" stroke="none"/><circle cx="3" cy="18" r="1.2" fill="currentColor" stroke="none"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cover -->
|
<!-- Cover -->
|
||||||
|
|||||||
+66
@@ -185,6 +185,10 @@ function _updateChapter() {
|
|||||||
ap.chapterIdx = idx;
|
ap.chapterIdx = idx;
|
||||||
_renderChapterName();
|
_renderChapterName();
|
||||||
_renderChapterList();
|
_renderChapterList();
|
||||||
|
if (_playlistMode) {
|
||||||
|
const cl = document.getElementById('abs-chapter-list');
|
||||||
|
setTimeout(() => cl?.querySelector('.abs-ch-item.active')?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }), 30);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +207,7 @@ export async function absPlayItem(item) {
|
|||||||
const ap = S.absPlayer;
|
const ap = S.absPlayer;
|
||||||
const audio = _getAudio();
|
const audio = _getAudio();
|
||||||
|
|
||||||
|
if (!S.audio.paused) S.audio.pause(); // stop music if playing
|
||||||
if (ap._sessionId) { absCloseSession(ap._sessionId); ap._sessionId = null; }
|
if (ap._sessionId) { absCloseSession(ap._sessionId); ap._sessionId = null; }
|
||||||
_stopSync();
|
_stopSync();
|
||||||
audio.pause();
|
audio.pause();
|
||||||
@@ -255,6 +260,11 @@ export function absTogglePlay() {
|
|||||||
S.absPlayer.playing ? audio.pause() : audio.play().catch(() => {});
|
S.absPlayer.playing ? audio.pause() : audio.play().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function absInterrupt() {
|
||||||
|
if (!S.absPlayer.item || !S.absPlayer.playing) return;
|
||||||
|
_getAudio().pause(); // triggers the pause listener which calls _doSync()
|
||||||
|
}
|
||||||
|
|
||||||
export function absSeek(globalTime) {
|
export function absSeek(globalTime) {
|
||||||
const ap = S.absPlayer;
|
const ap = S.absPlayer;
|
||||||
globalTime = Math.max(0, Math.min(globalTime, ap.duration || Infinity));
|
globalTime = Math.max(0, Math.min(globalTime, ap.duration || Infinity));
|
||||||
@@ -334,6 +344,7 @@ export function absClosePlayer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function absToggleChapters() {
|
export function absToggleChapters() {
|
||||||
|
if (_playlistMode) return;
|
||||||
document.getElementById('abs-chapter-list')?.classList.toggle('open');
|
document.getElementById('abs-chapter-list')?.classList.toggle('open');
|
||||||
_renderChapterList();
|
_renderChapterList();
|
||||||
}
|
}
|
||||||
@@ -353,6 +364,30 @@ export function absSeekToChapter(idx) {
|
|||||||
if (ch) absSeek(ch.start);
|
if (ch) absSeek(ch.start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── PLAYLIST MODE ───
|
||||||
|
let _playlistMode = localStorage.getItem('ns_abs_pl_mode') === '1';
|
||||||
|
|
||||||
|
export function absTogglePlaylistMode() {
|
||||||
|
_playlistMode = !_playlistMode;
|
||||||
|
localStorage.setItem('ns_abs_pl_mode', _playlistMode ? '1' : '0');
|
||||||
|
_applyPlaylistMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _applyPlaylistMode() {
|
||||||
|
const overlay = document.getElementById('abs-player');
|
||||||
|
overlay?.classList.toggle('playlist-mode', _playlistMode);
|
||||||
|
document.getElementById('abs-playlist-btn')?.classList.toggle('active', _playlistMode);
|
||||||
|
const cl = document.getElementById('abs-chapter-list');
|
||||||
|
if (!cl) return;
|
||||||
|
if (_playlistMode) {
|
||||||
|
cl.classList.add('open');
|
||||||
|
_renderChapterList();
|
||||||
|
setTimeout(() => cl.querySelector('.abs-ch-item.active')?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }), 50);
|
||||||
|
} else {
|
||||||
|
cl.classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── PROGRESS SCRUB ───
|
// ─── PROGRESS SCRUB ───
|
||||||
let _scrubbing = false;
|
let _scrubbing = false;
|
||||||
export function absProgDown(e) { _scrubbing = true; _scrub(e); }
|
export function absProgDown(e) { _scrubbing = true; _scrub(e); }
|
||||||
@@ -367,6 +402,33 @@ function _scrub(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── RENDER HELPERS ───
|
// ─── RENDER HELPERS ───
|
||||||
|
function _renderMiniBar() {
|
||||||
|
const ap = S.absPlayer;
|
||||||
|
const bar = document.getElementById('abs-mini-bar');
|
||||||
|
if (!bar) return;
|
||||||
|
if (!ap.item) { bar.classList.remove('visible'); return; }
|
||||||
|
bar.classList.add('visible');
|
||||||
|
const meta = ap.item?.media?.metadata || {};
|
||||||
|
const cover = absCoverUrl(ap.item?.id);
|
||||||
|
const coverEl = document.getElementById('abs-mini-cover');
|
||||||
|
if (coverEl) {
|
||||||
|
coverEl.innerHTML = cover
|
||||||
|
? `<img src="${cover}" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:6px" onerror="this.style.display='none'">`
|
||||||
|
: '📚';
|
||||||
|
}
|
||||||
|
const titleEl = document.getElementById('abs-mini-title');
|
||||||
|
if (titleEl) titleEl.textContent = meta.title || 'Unbekannt';
|
||||||
|
const ch = ap.chapters[ap.chapterIdx];
|
||||||
|
const chEl = document.getElementById('abs-mini-chapter');
|
||||||
|
if (chEl) chEl.textContent = ch?.title || meta.authorName || '';
|
||||||
|
const playBtn = document.getElementById('abs-mini-play');
|
||||||
|
if (playBtn) {
|
||||||
|
playBtn.innerHTML = ap.playing
|
||||||
|
? `<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/></svg>`
|
||||||
|
: `<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _fmt(secs) {
|
function _fmt(secs) {
|
||||||
if (!isFinite(secs) || secs < 0) secs = 0;
|
if (!isFinite(secs) || secs < 0) secs = 0;
|
||||||
const h = Math.floor(secs / 3600);
|
const h = Math.floor(secs / 3600);
|
||||||
@@ -393,6 +455,8 @@ function _renderPlayer() {
|
|||||||
_renderControls();
|
_renderControls();
|
||||||
_renderChapterName();
|
_renderChapterName();
|
||||||
_renderSleepBtn();
|
_renderSleepBtn();
|
||||||
|
_renderMiniBar();
|
||||||
|
_applyPlaylistMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderProgress() {
|
function _renderProgress() {
|
||||||
@@ -417,12 +481,14 @@ function _renderControls() {
|
|||||||
: `<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
|
: `<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
|
||||||
const sv = document.getElementById('abs-speed-val');
|
const sv = document.getElementById('abs-speed-val');
|
||||||
if (sv) sv.textContent = `${ap.speed}×`;
|
if (sv) sv.textContent = `${ap.speed}×`;
|
||||||
|
_renderMiniBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderChapterName() {
|
function _renderChapterName() {
|
||||||
const ch = S.absPlayer.chapters[S.absPlayer.chapterIdx];
|
const ch = S.absPlayer.chapters[S.absPlayer.chapterIdx];
|
||||||
const el = document.getElementById('abs-chapter-name');
|
const el = document.getElementById('abs-chapter-name');
|
||||||
if (el) el.textContent = ch?.title || '';
|
if (el) el.textContent = ch?.title || '';
|
||||||
|
_renderMiniBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _renderChapterList() {
|
function _renderChapterList() {
|
||||||
|
|||||||
+3
-2
@@ -24,7 +24,7 @@ import {
|
|||||||
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
||||||
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
|
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter, absProgDown, absProgMove,
|
||||||
absProgUp, loadAbsBooks, absOpenBook, loadAbsBookDetail, absPlayChapter,
|
absProgUp, loadAbsBooks, absOpenBook, loadAbsBookDetail, absPlayChapter,
|
||||||
absLoadSettingsLibraries,
|
absLoadSettingsLibraries, absTogglePlaylistMode, absInterrupt,
|
||||||
} from './abs.js';
|
} from './abs.js';
|
||||||
import {
|
import {
|
||||||
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
openNowPlaying, closeNowPlaying, switchNpTab, npGoArtist, npGoAlbum,
|
||||||
@@ -126,6 +126,7 @@ window.addEventListener('load', () => {
|
|||||||
S.audio.addEventListener('timeupdate', updateProgress);
|
S.audio.addEventListener('timeupdate', updateProgress);
|
||||||
S.audio.addEventListener('ended', onEnded);
|
S.audio.addEventListener('ended', onEnded);
|
||||||
S.audio.addEventListener('play', () => {
|
S.audio.addEventListener('play', () => {
|
||||||
|
absInterrupt();
|
||||||
setPlayIcon(true);
|
setPlayIcon(true);
|
||||||
document.getElementById('player-art').classList.add('spinning');
|
document.getElementById('player-art').classList.add('spinning');
|
||||||
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
|
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
|
||||||
@@ -270,7 +271,7 @@ Object.assign(window, {
|
|||||||
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
absSetSpeed, absSleepIn, absOpenPlayer, absClosePlayer, absToggleChapters,
|
||||||
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter,
|
absToggleSpeedPicker, absToggleSleepPicker, absSeekToChapter,
|
||||||
absProgDown, absProgMove, absProgUp,
|
absProgDown, absProgMove, absProgUp,
|
||||||
absOpenBook, absPlayChapter, absConnectSettings, absSetLibrary, absDisconnect,
|
absOpenBook, absPlayChapter, absConnectSettings, absSetLibrary, absDisconnect, absTogglePlaylistMode,
|
||||||
absOnSearch: (val) => {
|
absOnSearch: (val) => {
|
||||||
clearTimeout(window._absSearchTimer);
|
clearTimeout(window._absSearchTimer);
|
||||||
window._absSearchTimer = setTimeout(() => {
|
window._absSearchTimer = setTimeout(() => {
|
||||||
|
|||||||
+16
@@ -951,3 +951,19 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
|
|||||||
.abs-cover-area{padding:8px 24px}
|
.abs-cover-area{padding:8px 24px}
|
||||||
.abs-cover-wrap{width:min(240px,calc(100vw - 48px))}
|
.abs-cover-wrap{width:min(240px,calc(100vw - 48px))}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── ABS MINI BAR ── */
|
||||||
|
.abs-mini-bar{position:fixed;bottom:var(--player-h);left:0;right:0;background:rgba(10,10,28,.96);backdrop-filter:blur(16px);border-top:1px solid rgba(130,80,255,.25);padding:9px 16px;display:flex;align-items:center;gap:12px;z-index:99;transform:translateY(100%);transition:transform .3s;cursor:pointer}
|
||||||
|
.abs-mini-bar.visible{transform:translateY(0)}
|
||||||
|
.abs-mini-cover{width:40px;height:40px;border-radius:7px;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;overflow:hidden}
|
||||||
|
.abs-mini-info{flex:1;min-width:0}
|
||||||
|
.abs-mini-title{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.abs-mini-chapter{font-size:11px;color:var(--text2);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.abs-mini-play{background:none;border:none;color:var(--text);cursor:pointer;padding:6px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||||
|
@media(max-width:768px){.abs-mini-bar{bottom:calc(var(--player-h) + 80px + env(safe-area-inset-bottom))}}
|
||||||
|
|
||||||
|
/* ── ABS PLAYLIST MODE ── */
|
||||||
|
.abs-hbtn.active{color:var(--accent)}
|
||||||
|
.abs-player-overlay.playlist-mode .abs-cover-area{flex:0 0 auto;padding:4px 40px}
|
||||||
|
.abs-player-overlay.playlist-mode .abs-cover-wrap{width:min(140px,calc(100vw - 80px))}
|
||||||
|
.abs-player-overlay.playlist-mode .abs-chapter-list.open{flex:1;max-height:none;overflow-y:auto}
|
||||||
Reference in new issue
Block a user