Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 53 additions & 34 deletions mods/features/adblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,33 +268,25 @@ function dumpHelperTileStructure(tile, id) {
}
}

// Collapse a playlist row that no longer holds any tile.
// NOTE: do NOT try to hide or collapse .TXB27d rows here.
//
// Removing a helper TILE was never the missing piece — on-device logs show it
// already works (tile_scan reports removed:1/2 via the outerHTML substring
// match). What stays behind is the row element: the virtual list lays out one
// .TXB27d row per data-model entry, and that row keeps its height whether or
// not a tile is inside it. Those empty rows are the blank slots that push real
// content down, and scrolling back over one makes the list re-render its tile
// into it — which is exactly the reported "they appeared again".
// This was attempted (collapse the row left behind after removing a helper
// tile) and confirmed on-device not to work: rowsCollapsed reported 1 and 2 on
// real runs, yet the blank slots stayed exactly where they were and the one
// visible helper still sat in 4th position. yt-virtual-list positions rows by
// transform from its own layout math over its data model, so the slot a row
// occupies is reserved whether or not the row element is displayed.
//
// Only ever called on a row we've just emptied, so there is nothing left in it
// to hide by mistake. Inline style + class are lost if the list rebuilds the
// row from its data model; the MutationObserver re-runs the scan on that
// mutation and re-applies. The class is the one isHelperLikePlaylistNode
// already tested for but nothing ever set.
function collapseEmptyHelperRow(row) {
try {
if (!row) return false;
if (row.querySelectorAll('ytlr-tile-renderer, ytlr-grid-tile, ytlr-rich-item-renderer').length > 0) return false;
if (row.classList?.contains('tt-helper-soft-hidden')) return false;
row.classList?.add('tt-helper-soft-hidden');
row.style?.setProperty('display', 'none', 'important');
return true;
} catch (_) { return false; }
}


// It is also actively unsafe. The virtual list RECYCLES .TXB27d nodes — it
// repoints an existing row at different content rather than creating a new one
// — so any inline style we leave behind rides along to whatever video reuses
// that node. That exact pattern (opacity:0 on helper rows) was removed in
// 6a340fc after it produced black gaps and navigation hangs while scrolling.
// display:none would do the same, only worse: a real video silently missing.
//
// The data model is the only layer that would actually work, and it is out of
// reach: 654577e removed clearStaleHelpersFromVListData after confirming every
// Polymer API (set/splice/notifyPath/render) is undefined on Tizen 5.0.

function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') {
if (window.__ttRemovingHelperTiles) return { scannedTiles: 0, removed: 0, matchedIds: [] };
Expand All @@ -303,7 +295,7 @@ function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') {
const tiles = getPlaylistTileNodes();
if (!tiles.length) return { scannedTiles: 0, removed: 0, matchedIds: [] };
window.__ttRemovingHelperTiles = true;
let matchedTiles = 0, removed = 0, focusRedirected = 0, deferredNoContent = 0, rowsCollapsed = 0;
let matchedTiles = 0, removed = 0, focusRedirected = 0, deferredNoContent = 0;
const removedTiles = new Set(), matchedIds = new Set();
try {
for (const tile of tiles) {
Expand Down Expand Up @@ -343,9 +335,6 @@ function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') {
removedTiles.add(tile);
try { tile.remove(); } catch (_) {}
removed++;
// The row survives the tile and keeps its height — that leftover
// row is the blank slot. Collapse it now that it's empty.
if (collapseEmptyHelperRow(rowNode)) rowsCollapsed++;
}
} catch (_) { }
break;
Expand All @@ -357,8 +346,8 @@ function removeRetiredHelpersFromTiles(reason = 'playlist.helper.tile_scan') {
} finally {
window.__ttRemovingHelperTiles = false;
}
appendFileOnlyLog('playlist.helper.tile_scan', { reason, retiredCount: retiredIds.length, scannedTiles: tiles.length, removed, matchedTiles, focusRedirected, deferredNoContent, rowsCollapsed, matchedIds: Array.from(matchedIds) });
return { scannedTiles: tiles.length, removed, matchedIds: Array.from(matchedIds), matchedTiles, rowsCollapsed };
appendFileOnlyLog('playlist.helper.tile_scan', { reason, retiredCount: retiredIds.length, scannedTiles: tiles.length, removed, matchedTiles, focusRedirected, deferredNoContent, matchedIds: Array.from(matchedIds) });
return { scannedTiles: tiles.length, removed, matchedIds: Array.from(matchedIds), matchedTiles };
}

function ensurePlaylistHelperObserver() {
Expand Down Expand Up @@ -497,6 +486,31 @@ function clearKeepOneMarkers(items, label = 'continuation') {
return cleared;
}

// True while playlistBatchCollect.js is fetching (or has already fetched) the
// rest of this playlist on its own.
//
// When it is, the keep-one auto-load cascade below must stay out of the way.
// Timings from a 68-video all-watched playlist show why: our cascade drove
// YouTube through its own continuations at 17:53:01.780, .02.060 and .02.265 —
// three helpers inside half a second, one per response — while the background
// collector was still working and only reached prefetch_ready at 17:53:12.375,
// ten seconds later, with all 53 items nobody needed by then. Every one of
// those helpers is permanently stuck in the virtual list's data model, which is
// unreachable on Tizen 5.0 (654577e), so each is a blank slot for the rest of
// the visit.
//
// Standing down lets the prefetched batches arrive as ONE merged continuation.
// That response carries no further continuation token, so hasContinuation is
// false and the keep-one branch never runs for it — leaving exactly one helper
// (from the initial page load) instead of four, and that one gets retired and
// removed when the merged batch lands.
function isBatchCollectHandlingPlaylist() {
try {
if (!configRead('enablePlaylistBatchCollect')) return false;
return !!(window.__ttPrefetchStarted || window.__ttPrefetchedBatch);
} catch (_) { return false; }
}

function filterContinuationItems(items, pageName, hasContinuation = false, label = 'continuation') {
if (pageName === 'playlist' && !hasContinuation) {
clearPlaylistHelperVideoIdSet(label);
Expand Down Expand Up @@ -529,13 +543,18 @@ function filterContinuationItems(items, pageName, hasContinuation = false, label
// All items in this batch are watched — auto-fetch the next batch without waiting
// for the user to scroll. The continuation token is already stored at this point.
// This cascades through all-watched batches automatically until unwatched content appears.
schedulePlaylistAutoLoad(`${label}.keep-one`);
if (isBatchCollectHandlingPlaylist()) {
appendFileOnlyLog(`${label}.keep-one.autoload_skipped`, { reason: 'batch_collect_active' });
} else {
schedulePlaylistAutoLoad(`${label}.keep-one`);
}
return [fallbackItem];
}
if (pageName === 'playlist' && !hasContinuation && filteredItems.length === 0) showPlaylistAllHiddenNotice(`${label}.no_continuation_all_hidden`);
if (pageName === 'playlist' && hasContinuation && filteredItems.length === 0) {
appendFileOnlyLog(`${label}.empty_batch.autoload`, { pageName, originalCount: Array.isArray(items) ? items.length : 0 });
schedulePlaylistAutoLoad(`${label}.empty_batch`);
const collecting = isBatchCollectHandlingPlaylist();
appendFileOnlyLog(`${label}.empty_batch.autoload`, { pageName, originalCount: Array.isArray(items) ? items.length : 0, skipped: collecting });
if (!collecting) schedulePlaylistAutoLoad(`${label}.empty_batch`);
}
return filteredItems;
}
Expand Down