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:
Jonathan Sykes
2026-10-03 02:21:55 +08:00
parent 2dfce8a2fb
commit 8084411f6a
3 changed files with 41 additions and 1 deletions

View File

@@ -7283,6 +7283,23 @@ function updateProgress() {
// ============================================================================
// 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
// 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
@@ -7314,6 +7331,7 @@ function showMiniBar() {
miniCh.classList.toggle('link', !!channelKeyOf(current.meta));
$('miniBar').classList.remove('hidden');
updateMiniBar();
requestAnimationFrame(updateBottomChrome);
}
function updateMiniBar() {
if (!current || !current.meta) return;
@@ -7326,6 +7344,7 @@ function updateMiniBar() {
}
function hideMiniBar() {
$('miniBar').classList.add('hidden');
requestAnimationFrame(updateBottomChrome);
}
// ============================================================================
@@ -8150,6 +8169,8 @@ function renderList() {
$('listFilterBar').classList.toggle('hidden', !showFilter);
$('batchBar').classList.toggle('hidden', !selectMode);
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
let list = currentList();
@@ -8274,7 +8295,7 @@ function renderList() {
}
// 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) {
const selBtn = listIconBtn('select', selectMode ? 'Stop selecting' : 'Select items', toggleSelectMode);
selBtn.classList.toggle('pinned', selectMode);
@@ -10509,6 +10530,17 @@ document.querySelectorAll('.chip').forEach((c) => {
// Batch bar actions
$('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', () => {
const all = currentList();
selectedIds.forEach((id) => {
@@ -11158,6 +11190,7 @@ function exitSelectMode() {
const c = $('cards');
if (c) c.classList.remove('select-mode');
$('batchBar').classList.add('hidden');
$('batchAllBtn').textContent = 'Select all';
}
function toggleSelectMode() {
selectMode = !selectMode;
@@ -11166,6 +11199,7 @@ function toggleSelectMode() {
if (c) c.classList.toggle('select-mode', selectMode);
$('batchBar').classList.toggle('hidden', !selectMode);
$('batchCount').textContent = '0 selected';
$('batchAllBtn').textContent = 'Select all';
renderList();
}
function toggleSelectCard(id) {
@@ -11777,6 +11811,7 @@ async function boot() {
}
data.playlists.forEach(preloadPlaylist);
preloadPinnedPlaylists();
updateBottomChrome();
// Continue saves that were interrupted (closed app, dropped connection).
if (WEB) SaveQueue.start();
// Not awaited — artwork backfill must never delay first paint.

View File

@@ -376,6 +376,7 @@
</div>
<div id="batchBar" class="batch-bar hidden">
<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="batchAddPlaylistBtn" class="batch-btn">+ Playlist</button>
<button id="batchDeleteBtn" class="batch-btn batch-danger">Delete</button>

View File

@@ -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) */
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; }