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
+112 -36

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-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-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>
@@ -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>
<input id="playlist-search" class="list-search" type="text" placeholder="In Playlist suchen…" oninput="filterPlaylist(this.value)">
</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>
+4 -1
View File
@@ -16,6 +16,7 @@ import {
onGlobalSearch, s_goArtist, s_goAlbum, playerBarGoArtist, playerBarGoAlbum,
searchSimilarArtist, searchAndPreview, loadItunesAlbum, expandArtistBio,
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel,
} from './views.js';
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll } from './spotify.js';
import {
@@ -282,7 +283,9 @@ Object.assign(window, {
openSettings, closeSettings, saveSettings, confirmDisconnect, checkBackend,
openSidebar, closeSidebar, switchNavMode, showCtxMenu,
// Views
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist, loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
plReorderEnter, plReorderSave, plReorderCancel,
loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
executeSpotifyDownload,
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
showSongInfo, closeInfoModal,
+89 -32
View File
@@ -557,11 +557,10 @@ export async function loadPlaylistDetail(pl) {
playQueue(shuffled, 0);
};
sl.innerHTML = '';
sl.classList.add('pl-draggable');
const plSearch = document.getElementById('playlist-search');
if (plSearch) plSearch.value = '';
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>`; }
}
@@ -574,9 +573,28 @@ export function filterPlaylist(val) {
});
}
function _initPlaylistDragDrop(container, songs, pl) {
let dragSrc = null;
let saveTimer = null;
// ─── 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 touchDragSrc = null;
Array.from(container.children).forEach((item, i) => {
const handle = document.createElement('div');
@@ -585,54 +603,93 @@ function _initPlaylistDragDrop(container, songs, pl) {
item.insertBefore(handle, item.firstChild);
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;
handle.addEventListener('mousedown', () => { fromHandle = true; item.draggable = true; });
item.addEventListener('dragstart', e => {
if (!fromHandle) { e.preventDefault(); item.draggable = false; return; }
fromHandle = false;
dragSrc = item;
fromHandle = false; dragSrc = item;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', ''); // required by Firefox
e.dataTransfer.setData('text/plain', '');
setTimeout(() => item.classList.add('dragging'), 0);
});
item.addEventListener('dragend', () => {
fromHandle = false;
item.draggable = false;
dragSrc = null;
fromHandle = false; item.draggable = false; dragSrc = null;
item.classList.remove('dragging');
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
});
item.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
e.preventDefault(); e.dataTransfer.dropEffect = 'move';
if (item === dragSrc) return;
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
item.classList.add('drag-over');
});
item.addEventListener('dragleave', e => {
if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over');
});
item.addEventListener('dragleave', e => { if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over'); });
item.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
e.preventDefault(); e.stopPropagation();
if (!dragSrc || item === dragSrc) return;
item.classList.remove('drag-over');
const allItems = Array.from(container.children);
const fromIdx = allItems.indexOf(dragSrc);
const toIdx = allItems.indexOf(item);
container.insertBefore(dragSrc, fromIdx < toIdx ? item.nextSibling : item);
const newOrder = Array.from(container.children).map(el => el._songRef);
songs.splice(0, songs.length, ...newOrder);
Array.from(container.children).forEach((el, idx) => {
const n = el.querySelector('.song-num');
if (n) n.textContent = idx + 1;
});
clearTimeout(saveTimer);
saveTimer = setTimeout(() => _savePlaylistOrder(pl, songs), 800);
_doReorder(container, songs, dragSrc, 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);
songs.splice(0, songs.length, ...newOrder);
Array.from(container.children).forEach((el, i) => {
const n = el.querySelector('.song-num'); if (n) n.textContent = i + 1;
});
}
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) {
+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-item:hover .gtrack-actions{opacity:1}
button:focus{outline:none}
/* ── 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:hover{color:var(--accent);background:var(--accent-dim)}
@@ -193,8 +195,9 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
/* ── PLAYLIST DRAG & DROP ── */
.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}
.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-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.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}
/* ── 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-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)}
@@ -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-msg{font-size:14px;color:var(--text2);line-height:1.5;margin-bottom:24px}
.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 ── */
.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}