fix: button focus ring, playlist reorder mode, edit modal mobile, backdrop-filter

- button:focus{outline:none} — removes the blue tap-highlight box in WebView
- Playlist reorder: replaced always-on drag handles with explicit 'Neu anordnen'
  mode. Handles only appear after tapping the button; a sticky Speichern/Abbrechen
  bar appears above the list. Touch drag is now fully supported. Context-menu
  (long-press) is blocked while reorder mode is active to prevent conflicts.
- styles.css: .pl-drag-handle always visible when pl-draggable (no hover needed)
- Edit modal footer: wrap buttons on small screens so Save doesn't overflow
- Edit modal overlay: remove backdrop-filter:blur — causes WebView compositor
  glitch where 3/4 of the modal disappears after returning from background

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-18 22:40:11 +02:00
1 parent 68bec67880
commit 9d8a5a22bb
4 files changed
+110 -34

No files matched your search

+8
View File
@@ -260,6 +260,7 @@
<button class="btn-play-all" id="btn-play-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>Abspielen</button> <button class="btn-play-all" id="btn-play-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>Abspielen</button>
<button class="btn-outline" id="btn-shuffle-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Shuffle</button> <button class="btn-outline" id="btn-shuffle-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Shuffle</button>
<button class="btn-outline" id="btn-edit-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Bearbeiten</button> <button class="btn-outline" id="btn-edit-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Bearbeiten</button>
<button class="btn-outline" id="btn-reorder-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>Neu anordnen</button>
</div> </div>
</div> </div>
</div> </div>
@@ -267,6 +268,13 @@
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input id="playlist-search" class="list-search" type="text" placeholder="In Playlist suchen…" oninput="filterPlaylist(this.value)"> <input id="playlist-search" class="list-search" type="text" placeholder="In Playlist suchen…" oninput="filterPlaylist(this.value)">
</div> </div>
<div class="pl-reorder-bar" id="pl-reorder-bar" style="display:none">
<span>Reihenfolge ändern</span>
<div style="display:flex;gap:8px">
<button class="edit-pl-btn-cancel" onclick="plReorderCancel()">Abbrechen</button>
<button class="edit-pl-btn-save" onclick="plReorderSave()">Speichern</button>
</div>
</div>
<div class="song-list" id="playlist-songs"></div> <div class="song-list" id="playlist-songs"></div>
</div> </div>
+4 -1
View File
@@ -16,6 +16,7 @@ import {
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum, onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio, searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel,
} from './views.js'; } from './views.js';
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js'; import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
import { import {
@@ -282,7 +283,9 @@ Object.assign(window, {
openSettings, closeSettings, saveSettings, confirmDisconnect, checkBackend, openSettings, closeSettings, saveSettings, confirmDisconnect, checkBackend,
openSidebar, closeSidebar, switchNavMode, showCtxMenu, openSidebar, closeSidebar, switchNavMode, showCtxMenu,
// Views // Views
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll, loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel,
loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
executeSpotifyDownload, executeSpotifyDownload,
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt, openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
showSongInfo, closeInfoModal, showSongInfo, closeInfoModal,
+87 -30
View File
@@ -557,11 +557,10 @@ export async function loadPlaylistDetail(pl) {
playQueue(shuffled, 0); playQueue(shuffled, 0);
}; };
sl.innerHTML = ''; sl.innerHTML = '';
sl.classList.add('pl-draggable');
const plSearch = document.getElementById('playlist-search'); const plSearch = document.getElementById('playlist-search');
if (plSearch) plSearch.value = ''; if (plSearch) plSearch.value = '';
songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true, noAlbumLink: true }))); songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true, noAlbumLink: true })));
_initPlaylistDragDrop(sl, songs, S._currentPlaylist); _setupReorderMode(sl, songs, S._currentPlaylist);
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; } } catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
} }
@@ -574,9 +573,28 @@ export function filterPlaylist(val) {
}); });
} }
function _initPlaylistDragDrop(container, songs, pl) { // ─── PLAYLIST REORDER MODE ───────────────────────────────────────────────
let _reorderCtx = null;
function _setupReorderMode(container, songs, pl) {
// Exit any previous reorder mode silently when loading a new playlist.
if (_reorderCtx) _exitReorderMode(false);
_reorderCtx = { container, songs, pl };
const btn = document.getElementById('btn-reorder-playlist');
if (btn) btn.onclick = plReorderEnter;
const bar = document.getElementById('pl-reorder-bar');
if (bar) bar.style.display = 'none';
}
export function plReorderEnter() {
if (!_reorderCtx) return;
const { container, songs } = _reorderCtx;
container.classList.add('pl-draggable');
// Block context-menu (long-press) on all items while reordering.
container.addEventListener('contextmenu', _blockCtxMenu, true);
let dragSrc = null; let dragSrc = null;
let saveTimer = null; let touchDragSrc = null;
Array.from(container.children).forEach((item, i) => { Array.from(container.children).forEach((item, i) => {
const handle = document.createElement('div'); const handle = document.createElement('div');
@@ -585,56 +603,95 @@ function _initPlaylistDragDrop(container, songs, pl) {
item.insertBefore(handle, item.firstChild); item.insertBefore(handle, item.firstChild);
item._songRef = songs[i]; item._songRef = songs[i];
// Track whether drag started from the handle (e.target in dragstart is always the item, not the handle) // ── Desktop drag ──
let fromHandle = false; let fromHandle = false;
handle.addEventListener('mousedown', () => { fromHandle = true; item.draggable = true; }); handle.addEventListener('mousedown', () => { fromHandle = true; item.draggable = true; });
item.addEventListener('dragstart', e => { item.addEventListener('dragstart', e => {
if (!fromHandle) { e.preventDefault(); item.draggable = false; return; } if (!fromHandle) { e.preventDefault(); item.draggable = false; return; }
fromHandle = false; fromHandle = false; dragSrc = item;
dragSrc = item;
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // required by Firefox e.dataTransfer.setData('text/plain', '');
setTimeout(() => item.classList.add('dragging'), 0); setTimeout(() => item.classList.add('dragging'), 0);
}); });
item.addEventListener('dragend', () => { item.addEventListener('dragend', () => {
fromHandle = false; fromHandle = false; item.draggable = false; dragSrc = null;
item.draggable = false;
dragSrc = null;
item.classList.remove('dragging'); item.classList.remove('dragging');
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
}); });
item.addEventListener('dragover', e => { item.addEventListener('dragover', e => {
e.preventDefault(); e.preventDefault(); e.dataTransfer.dropEffect = 'move';
e.dataTransfer.dropEffect = 'move';
if (item === dragSrc) return; if (item === dragSrc) return;
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
item.classList.add('drag-over'); item.classList.add('drag-over');
}); });
item.addEventListener('dragleave', e => { item.addEventListener('dragleave', e => { if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over'); });
if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over');
});
item.addEventListener('drop', e => { item.addEventListener('drop', e => {
e.preventDefault(); e.preventDefault(); e.stopPropagation();
e.stopPropagation();
if (!dragSrc || item === dragSrc) return; if (!dragSrc || item === dragSrc) return;
item.classList.remove('drag-over'); item.classList.remove('drag-over');
const allItems = Array.from(container.children); _doReorder(container, songs, dragSrc, item);
const fromIdx = allItems.indexOf(dragSrc); });
const toIdx = allItems.indexOf(item);
container.insertBefore(dragSrc, fromIdx < toIdx ? item.nextSibling : item); // ── Touch drag ──
handle.addEventListener('touchstart', e => {
e.preventDefault(); e.stopPropagation();
touchDragSrc = item; item.classList.add('dragging');
}, { passive: false });
handle.addEventListener('touchmove', e => {
if (!touchDragSrc) return;
e.preventDefault();
const t = e.touches[0];
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
const over = _itemAtPoint(container, t.clientX, t.clientY, touchDragSrc);
if (over) over.classList.add('drag-over');
}, { passive: false });
handle.addEventListener('touchend', e => {
if (!touchDragSrc) return;
const t = e.changedTouches[0];
touchDragSrc.classList.remove('dragging');
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
const over = _itemAtPoint(container, t.clientX, t.clientY, touchDragSrc);
if (over) _doReorder(container, songs, touchDragSrc, over);
touchDragSrc = null;
}, { passive: false });
});
const bar = document.getElementById('pl-reorder-bar');
if (bar) bar.style.display = 'flex';
}
function _blockCtxMenu(e) { e.preventDefault(); e.stopPropagation(); }
function _itemAtPoint(container, x, y, exclude) {
return (document.elementsFromPoint(x, y) || [])
.find(el => el.classList?.contains('song-item') && el !== exclude && container.contains(el)) || null;
}
function _doReorder(container, songs, src, target) {
const all = Array.from(container.children);
const from = all.indexOf(src), to = all.indexOf(target);
container.insertBefore(src, from < to ? target.nextSibling : target);
const newOrder = Array.from(container.children).map(el => el._songRef); const newOrder = Array.from(container.children).map(el => el._songRef);
songs.splice(0, songs.length, ...newOrder); songs.splice(0, songs.length, ...newOrder);
Array.from(container.children).forEach((el, idx) => { Array.from(container.children).forEach((el, i) => {
const n = el.querySelector('.song-num'); const n = el.querySelector('.song-num'); if (n) n.textContent = i + 1;
if (n) n.textContent = idx + 1;
});
clearTimeout(saveTimer);
saveTimer = setTimeout(() => _savePlaylistOrder(pl, songs), 800);
});
}); });
} }
export function plReorderSave() { _exitReorderMode(true); }
export function plReorderCancel() { _exitReorderMode(false); }
function _exitReorderMode(save) {
if (!_reorderCtx) return;
const { container, songs, pl } = _reorderCtx;
container.classList.remove('pl-draggable');
container.removeEventListener('contextmenu', _blockCtxMenu, true);
container.querySelectorAll('.pl-drag-handle').forEach(h => h.remove());
const bar = document.getElementById('pl-reorder-bar');
if (bar) bar.style.display = 'none';
if (save) _savePlaylistOrder(pl, songs);
}
async function _savePlaylistOrder(pl, songs) { async function _savePlaylistOrder(pl, songs) {
try { try {
const songIds = songs.map(s => `&songId=${s.id}`).join(''); const songIds = songs.map(s => `&songId=${s.id}`).join('');
+11 -3
View File
@@ -165,6 +165,8 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
.gtrack-actions{display:flex;align-items:center;gap:6px;opacity:0;transition:opacity .15s} .gtrack-actions{display:flex;align-items:center;gap:6px;opacity:0;transition:opacity .15s}
.gtrack-item:hover .gtrack-actions{opacity:1} .gtrack-item:hover .gtrack-actions{opacity:1}
button:focus{outline:none}
/* ── ICON BTN ── */ /* ── ICON BTN ── */
.icon-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:6px;border-radius:6px;display:flex;align-items:center;transition:color .15s,background .15s} .icon-btn{background:none;border:none;color:var(--text2);cursor:pointer;padding:6px;border-radius:6px;display:flex;align-items:center;transition:color .15s,background .15s}
.icon-btn:hover{color:var(--accent);background:var(--accent-dim)} .icon-btn:hover{color:var(--accent);background:var(--accent-dim)}
@@ -193,8 +195,9 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
/* ── PLAYLIST DRAG & DROP ── */ /* ── PLAYLIST DRAG & DROP ── */
.song-list.pl-draggable .song-item{grid-template-columns:18px 32px 44px 1fr auto} .song-list.pl-draggable .song-item{grid-template-columns:18px 32px 44px 1fr auto}
.pl-drag-handle{display:flex;align-items:center;justify-content:center;color:var(--text3);opacity:0;cursor:grab;transition:opacity .15s;touch-action:none} .pl-drag-handle{display:flex;align-items:center;justify-content:center;color:var(--text3);opacity:0;cursor:grab;transition:opacity .15s;touch-action:none}
.song-list.pl-draggable .song-item:hover .pl-drag-handle{opacity:1} .song-list.pl-draggable .pl-drag-handle{opacity:1}
.pl-drag-handle:active{cursor:grabbing} .pl-drag-handle:active{cursor:grabbing}
.pl-reorder-bar{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:var(--surface);border-radius:10px;margin-bottom:8px;font-size:13px;color:var(--text2);gap:10px}
.song-item.dragging{opacity:0.35} .song-item.dragging{opacity:0.35}
.song-item.drag-over{box-shadow:0 -2px 0 var(--accent)} .song-item.drag-over{box-shadow:0 -2px 0 var(--accent)}
@@ -389,7 +392,7 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
.pl-btns button{flex:1;padding:10px;border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;border:none} .pl-btns button{flex:1;padding:10px;border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;border:none}
/* ── EDIT PLAYLIST MODAL ── */ /* ── EDIT PLAYLIST MODAL ── */
.edit-pl-overlay{position:fixed;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(5px);z-index:500;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s} .edit-pl-overlay{position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:500;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
.edit-pl-overlay.open{opacity:1;pointer-events:all} .edit-pl-overlay.open{opacity:1;pointer-events:all}
.edit-pl-box{width:min(580px,calc(100vw - 24px));background:var(--surface);border:1px solid var(--border2);border-radius:18px;box-shadow:0 40px 80px rgba(0,0,0,.8);overflow:hidden} .edit-pl-box{width:min(580px,calc(100vw - 24px));background:var(--surface);border:1px solid var(--border2);border-radius:18px;box-shadow:0 40px 80px rgba(0,0,0,.8);overflow:hidden}
.edit-pl-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;font-size:18px;font-weight:700;border-bottom:1px solid var(--border)} .edit-pl-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;font-size:18px;font-weight:700;border-bottom:1px solid var(--border)}
@@ -422,7 +425,12 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
.confirm-title{font-size:17px;font-weight:700;margin-bottom:10px} .confirm-title{font-size:17px;font-weight:700;margin-bottom:10px}
.confirm-msg{font-size:14px;color:var(--text2);line-height:1.5;margin-bottom:24px} .confirm-msg{font-size:14px;color:var(--text2);line-height:1.5;margin-bottom:24px}
.confirm-footer{display:flex;justify-content:flex-end;gap:10px} .confirm-footer{display:flex;justify-content:flex-end;gap:10px}
@media(max-width:480px){.edit-pl-body{flex-direction:column;align-items:center}.edit-pl-cover-wrap{width:140px;height:140px}} @media(max-width:480px){
.edit-pl-body{flex-direction:column;align-items:center}
.edit-pl-cover-wrap{width:140px;height:140px}
.edit-pl-footer{flex-wrap:wrap;padding:12px 16px;gap:8px}
.edit-pl-footer>div{margin-left:auto}
}
/* ── SONG INFO MODAL ── */ /* ── SONG INFO MODAL ── */
.info-modal{position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:400;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s} .info-modal{position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:400;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}