Bulk-select a channel's videos into a playlist; last card no longer hidden under the bottom bars
- Channel pages join the select-mode views: tap Select in the filter row, pick videos (or Select all — respects the filter), then + Queue / + Playlist. Delete is hidden there (nothing to delete on a channel). - --bottom-chrome (the fixed bottom nav + mini player height) is now measured whenever the mini player shows/hides and on resize/rotation, not only while the section rail scrolls, and the list pane pads its end by it, so the last card scrolls fully clear with or without the mini player.
This commit is contained in:
@@ -7283,6 +7283,23 @@ function updateProgress() {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Mini now-playing bar
|
// Mini now-playing bar
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
// Height of the fixed bars at the bottom (bottom nav, mini player) as CSS
|
||||||
|
// --bottom-chrome, so lists can scroll their last card clear of them. Measured,
|
||||||
|
// never guessed: the mini player comes and goes, and phones differ.
|
||||||
|
function updateBottomChrome() {
|
||||||
|
let occupied = 0;
|
||||||
|
for (const id of ['bottomNav', 'miniBar']) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el || el.classList.contains('hidden')) continue;
|
||||||
|
const box = el.getBoundingClientRect();
|
||||||
|
if (!box.height || getComputedStyle(el).position !== 'fixed') continue;
|
||||||
|
if (box.top > window.innerHeight * 0.5) occupied = Math.max(occupied, window.innerHeight - box.top);
|
||||||
|
}
|
||||||
|
document.documentElement.style.setProperty('--bottom-chrome', Math.round(occupied) + 'px');
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', () => requestAnimationFrame(updateBottomChrome));
|
||||||
|
window.addEventListener('orientationchange', () => setTimeout(updateBottomChrome, 300));
|
||||||
|
|
||||||
// Home while playing: the Search tab brings back the home screen (playlist
|
// Home while playing: the Search tab brings back the home screen (playlist
|
||||||
// board / hero) over the player pane, using the same look as "nothing playing"
|
// board / hero) over the player pane, using the same look as "nothing playing"
|
||||||
// (.empty). The media keeps playing — it is only made invisible, never removed
|
// (.empty). The media keeps playing — it is only made invisible, never removed
|
||||||
@@ -7314,6 +7331,7 @@ function showMiniBar() {
|
|||||||
miniCh.classList.toggle('link', !!channelKeyOf(current.meta));
|
miniCh.classList.toggle('link', !!channelKeyOf(current.meta));
|
||||||
$('miniBar').classList.remove('hidden');
|
$('miniBar').classList.remove('hidden');
|
||||||
updateMiniBar();
|
updateMiniBar();
|
||||||
|
requestAnimationFrame(updateBottomChrome);
|
||||||
}
|
}
|
||||||
function updateMiniBar() {
|
function updateMiniBar() {
|
||||||
if (!current || !current.meta) return;
|
if (!current || !current.meta) return;
|
||||||
@@ -7326,6 +7344,7 @@ function updateMiniBar() {
|
|||||||
}
|
}
|
||||||
function hideMiniBar() {
|
function hideMiniBar() {
|
||||||
$('miniBar').classList.add('hidden');
|
$('miniBar').classList.add('hidden');
|
||||||
|
requestAnimationFrame(updateBottomChrome);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -8150,6 +8169,8 @@ function renderList() {
|
|||||||
$('listFilterBar').classList.toggle('hidden', !showFilter);
|
$('listFilterBar').classList.toggle('hidden', !showFilter);
|
||||||
$('batchBar').classList.toggle('hidden', !selectMode);
|
$('batchBar').classList.toggle('hidden', !selectMode);
|
||||||
if (selectMode) $('batchCount').textContent = `${selectedIds.size} selected`;
|
if (selectMode) $('batchCount').textContent = `${selectedIds.size} selected`;
|
||||||
|
// A channel's videos can be queued or added to a playlist, not deleted.
|
||||||
|
$('batchDeleteBtn').classList.toggle('hidden', view.type === 'channel');
|
||||||
|
|
||||||
// Build the list, applying filter if active
|
// Build the list, applying filter if active
|
||||||
let list = currentList();
|
let list = currentList();
|
||||||
@@ -8274,7 +8295,7 @@ function renderList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select button for batch-operable views
|
// Select button for batch-operable views
|
||||||
const batchViews = ['history', 'playlist', 'queue', 'smart'];
|
const batchViews = ['history', 'playlist', 'queue', 'smart', 'channel'];
|
||||||
if (batchViews.includes(view.type) && list.length) {
|
if (batchViews.includes(view.type) && list.length) {
|
||||||
const selBtn = listIconBtn('select', selectMode ? 'Stop selecting' : 'Select items', toggleSelectMode);
|
const selBtn = listIconBtn('select', selectMode ? 'Stop selecting' : 'Select items', toggleSelectMode);
|
||||||
selBtn.classList.toggle('pinned', selectMode);
|
selBtn.classList.toggle('pinned', selectMode);
|
||||||
@@ -10509,6 +10530,17 @@ document.querySelectorAll('.chip').forEach((c) => {
|
|||||||
|
|
||||||
// Batch bar actions
|
// Batch bar actions
|
||||||
$('batchCancelBtn').addEventListener('click', () => { exitSelectMode(); renderList(); });
|
$('batchCancelBtn').addEventListener('click', () => { exitSelectMode(); renderList(); });
|
||||||
|
// Select all / none of what the list shows (the filter applies).
|
||||||
|
$('batchAllBtn').addEventListener('click', () => {
|
||||||
|
let list = currentList();
|
||||||
|
if (listFilter) list = list.filter((v) => `${v.title || ''} ${v.channel || ''}`.toLowerCase().includes(listFilter));
|
||||||
|
const ids = list.map((v) => v.id);
|
||||||
|
const all = ids.length && ids.every((id) => selectedIds.has(id));
|
||||||
|
ids.forEach((id) => { if (all) selectedIds.delete(id); else selectedIds.add(id); });
|
||||||
|
$('batchCount').textContent = `${selectedIds.size} selected`;
|
||||||
|
document.querySelectorAll('#cards .card[data-id]').forEach((c) => c.classList.toggle('selected', selectedIds.has(c.dataset.id)));
|
||||||
|
$('batchAllBtn').textContent = all ? 'Select all' : 'Select none';
|
||||||
|
});
|
||||||
$('batchQueueBtn').addEventListener('click', () => {
|
$('batchQueueBtn').addEventListener('click', () => {
|
||||||
const all = currentList();
|
const all = currentList();
|
||||||
selectedIds.forEach((id) => {
|
selectedIds.forEach((id) => {
|
||||||
@@ -11158,6 +11190,7 @@ function exitSelectMode() {
|
|||||||
const c = $('cards');
|
const c = $('cards');
|
||||||
if (c) c.classList.remove('select-mode');
|
if (c) c.classList.remove('select-mode');
|
||||||
$('batchBar').classList.add('hidden');
|
$('batchBar').classList.add('hidden');
|
||||||
|
$('batchAllBtn').textContent = 'Select all';
|
||||||
}
|
}
|
||||||
function toggleSelectMode() {
|
function toggleSelectMode() {
|
||||||
selectMode = !selectMode;
|
selectMode = !selectMode;
|
||||||
@@ -11166,6 +11199,7 @@ function toggleSelectMode() {
|
|||||||
if (c) c.classList.toggle('select-mode', selectMode);
|
if (c) c.classList.toggle('select-mode', selectMode);
|
||||||
$('batchBar').classList.toggle('hidden', !selectMode);
|
$('batchBar').classList.toggle('hidden', !selectMode);
|
||||||
$('batchCount').textContent = '0 selected';
|
$('batchCount').textContent = '0 selected';
|
||||||
|
$('batchAllBtn').textContent = 'Select all';
|
||||||
renderList();
|
renderList();
|
||||||
}
|
}
|
||||||
function toggleSelectCard(id) {
|
function toggleSelectCard(id) {
|
||||||
@@ -11777,6 +11811,7 @@ async function boot() {
|
|||||||
}
|
}
|
||||||
data.playlists.forEach(preloadPlaylist);
|
data.playlists.forEach(preloadPlaylist);
|
||||||
preloadPinnedPlaylists();
|
preloadPinnedPlaylists();
|
||||||
|
updateBottomChrome();
|
||||||
// Continue saves that were interrupted (closed app, dropped connection).
|
// Continue saves that were interrupted (closed app, dropped connection).
|
||||||
if (WEB) SaveQueue.start();
|
if (WEB) SaveQueue.start();
|
||||||
// Not awaited — artwork backfill must never delay first paint.
|
// Not awaited — artwork backfill must never delay first paint.
|
||||||
|
|||||||
@@ -376,6 +376,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="batchBar" class="batch-bar hidden">
|
<div id="batchBar" class="batch-bar hidden">
|
||||||
<span id="batchCount" class="batch-count">0 selected</span>
|
<span id="batchCount" class="batch-count">0 selected</span>
|
||||||
|
<button id="batchAllBtn" class="batch-btn">Select all</button>
|
||||||
<button id="batchQueueBtn" class="batch-btn">+ Queue</button>
|
<button id="batchQueueBtn" class="batch-btn">+ Queue</button>
|
||||||
<button id="batchAddPlaylistBtn" class="batch-btn">+ Playlist</button>
|
<button id="batchAddPlaylistBtn" class="batch-btn">+ Playlist</button>
|
||||||
<button id="batchDeleteBtn" class="batch-btn batch-danger">Delete</button>
|
<button id="batchDeleteBtn" class="batch-btn batch-danger">Delete</button>
|
||||||
|
|||||||
@@ -4306,3 +4306,7 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t
|
|||||||
|
|
||||||
/* iPhone/iPad: no double-tap page zoom (scrolling unaffected) */
|
/* iPhone/iPad: no double-tap page zoom (scrolling unaffected) */
|
||||||
html.ios-nozoom, html.ios-nozoom body { touch-action: manipulation; }
|
html.ios-nozoom, html.ios-nozoom body { touch-action: manipulation; }
|
||||||
|
|
||||||
|
/* The last card of a list scrolls clear of the bottom nav + mini player
|
||||||
|
(--bottom-chrome is measured in app.js updateBottomChrome). */
|
||||||
|
#listPane { padding-bottom: calc(var(--bottom-chrome, 0px) + 32px) !important; }
|
||||||
|
|||||||
Reference in New Issue
Block a user