diff --git a/mods/config.js b/mods/config.js index acf67848..0ce421e8 100644 --- a/mods/config.js +++ b/mods/config.js @@ -39,6 +39,7 @@ const defaultConfig = { enableHideWatchedVideos: false, hideWatchedVideosThreshold: 80, hideWatchedVideosPages: [], + hiddenLibraryTabIds: [], enableHideEndScreenCards: false, enableYouThereRenderer: true, lastAnnouncementCheck: 0, diff --git a/mods/features/adblock.js b/mods/features/adblock.js index d9959264..acaf7e5e 100644 --- a/mods/features/adblock.js +++ b/mods/features/adblock.js @@ -4,6 +4,7 @@ import resolveCommand from '../resolveCommand.js'; import { timelyAction, longPressData, MenuServiceItemRenderer, ShelfRenderer, TileRenderer, ButtonRenderer } from '../ui/ytUI.js'; import { PatchSettings } from '../ui/customYTSettings.js'; import { t } from 'i18next'; +import { applyLibraryTabHiding } from './libraryTabHider.js'; /** * This is a minimal reimplementation of the following uBlock Origin rule: @@ -17,6 +18,8 @@ import { t } from 'i18next'; const origParse = JSON.parse; JSON.parse = function () { const r = origParse.apply(this, arguments); + + try { const adBlockEnabled = configRead('enableAdBlock'); const signinReminderEnabled = configRead('enableSigninReminder'); @@ -49,6 +52,11 @@ JSON.parse = function () { } } + const hiddenLibraryTabIds = configRead('hiddenLibraryTabIds'); + if (Array.isArray(hiddenLibraryTabIds) && hiddenLibraryTabIds.length > 0) { + applyLibraryTabHiding(r, hiddenLibraryTabIds); + } + // Drop "masthead" ad from home screen if ( r?.contents?.tvBrowseRenderer?.content?.tvSurfaceContentRenderer?.content @@ -172,9 +180,9 @@ JSON.parse = function () { } } /* - + Chapters are disabled due to the API removing description data which was used to generate chapters - + if (r?.contents?.singleColumnWatchNextResults?.results?.results?.contents && configRead('enableChapters')) { const chapterData = Chapters(r); r.frameworkUpdates.entityBatchUpdate.mutations.push(chapterData); @@ -251,6 +259,10 @@ JSON.parse = function () { } } + } catch (_) { + // Keep response unchanged if parsing hook hits an edge-case. + } + return r; }; @@ -411,4 +423,4 @@ function hideVideo(items) { const percentWatched = (progressBar.percentDurationWatched || 0); return percentWatched <= configRead('hideWatchedVideosThreshold'); }); -} \ No newline at end of file +} diff --git a/mods/features/libraryTabHider.js b/mods/features/libraryTabHider.js new file mode 100644 index 00000000..12daabea --- /dev/null +++ b/mods/features/libraryTabHider.js @@ -0,0 +1,62 @@ +const getHiddenLibraryTabIds = (configured) => { + if (!Array.isArray(configured) || configured.length === 0) return new Set(); + return new Set(configured.map((id) => String(id || '').toLowerCase()).filter(Boolean)); +}; + +const matchesHiddenId = (value, hiddenIds) => { + const id = String(value || '').toLowerCase(); + if (!id) return false; + + for (const hiddenId of hiddenIds) { + if (id === hiddenId || id.includes(hiddenId)) return true; + } + + return false; +}; + +const shouldHideTabItem = (item, hiddenIds) => { + if (!item || typeof item !== 'object') return false; + + return matchesHiddenId(item?.tileRenderer?.contentId, hiddenIds) + || matchesHiddenId(item?.tabRenderer?.content?.tvSurfaceContentRenderer?.content?.gridRenderer?.items?.[0]?.tileRenderer?.contentId, hiddenIds) + || matchesHiddenId(item?.tabRenderer?.endpoint?.browseEndpoint?.browseId, hiddenIds) + || matchesHiddenId(item?.navigationEndpoint?.browseEndpoint?.browseId, hiddenIds) + || matchesHiddenId(item?.browseEndpoint?.browseId, hiddenIds); +}; + +const pruneLibraryTabs = (node, hiddenIds) => { + if (!node || typeof node !== 'object') return; + + if (Array.isArray(node?.horizontalListRenderer?.items)) { + node.horizontalListRenderer.items = node.horizontalListRenderer.items.filter((item) => !shouldHideTabItem(item, hiddenIds)); + } + + if (Array.isArray(node?.continuationContents?.horizontalListContinuation?.items)) { + node.continuationContents.horizontalListContinuation.items = + node.continuationContents.horizontalListContinuation.items.filter((item) => !shouldHideTabItem(item, hiddenIds)); + } + + if (Array.isArray(node?.tvSecondaryNavSectionRenderer?.tabs)) { + node.tvSecondaryNavSectionRenderer.tabs = node.tvSecondaryNavSectionRenderer.tabs.filter((tab) => !shouldHideTabItem(tab, hiddenIds)); + } + + for (const key of Object.keys(node)) { + const value = node[key]; + if (value && typeof value === 'object') { + if (Array.isArray(value)) { + for (const entry of value) { + if (entry && typeof entry === 'object') pruneLibraryTabs(entry, hiddenIds); + } + } else { + pruneLibraryTabs(value, hiddenIds); + } + } + } +}; + +export const applyLibraryTabHiding = (response, configuredHiddenIds) => { + const hiddenIds = getHiddenLibraryTabIds(configuredHiddenIds); + if (hiddenIds.size === 0) return; + + pruneLibraryTabs(response, hiddenIds); +}; diff --git a/mods/ui/settings.js b/mods/ui/settings.js index dfab4504..ba531669 100644 --- a/mods/ui/settings.js +++ b/mods/ui/settings.js @@ -511,6 +511,22 @@ export default function modernUI(update, parameters) { } ] }, + { + name: 'Library Tabs Buttons to Hide', + icon: 'TAB_LIBRARY', + value: null, + arrayToEdit: 'hiddenLibraryTabIds', + menuId: 'tt-hidden-library-tabs', + options: [ + { name: 'Music', value: 'femusic_last_played' }, + { name: 'Movies & Shows', value: 'festorefront' }, + { name: 'Podcasts', value: 'fecollection_podcasts' }, + { name: 'My Videos', value: 'femy_videos' }, + { name: 'History', value: 'fehistory' }, + { name: 'Watch Later', value: 'femy_youtube' }, + { name: 'Playlists', value: 'feplaylist_aggregation' } + ] + }, { name: t('settings.options.uiSettings.options.screenDimming.title'), icon: 'EYE_OFF',