diff --git a/src/content.js b/src/content.js index d0d97db..d7c0cad 100644 --- a/src/content.js +++ b/src/content.js @@ -1,13 +1,17 @@ /** - * Chrome Extension Content Script - * Will run on the YouTube page. + * Chrome Extension Content Script. + * Will run on all YouTube page. + */ + +/** + * Main */ chrome.storage.sync.get( ['doHideShorts', 'doHideWatched', 'doFadeByLength', 'videoLengthMax', 'videoLengthMin'], ({ doHideShorts, doHideWatched, doFadeByLength, videoLengthMax, videoLengthMin }) => { /** - * General Options + * Filters videos */ // Hide watched videos (if the option is enabled) @@ -36,11 +40,8 @@ chrome.storage.sync.get( } }; - /** - * Video Length Filter - */ - - const filterVideos = () => { + // Fade videos based on their length (if the option is enabled) + const fadeVideosByLength = () => { if (doFadeByLength) { // Select all video elements on the YouTube page const videoElements = document.querySelectorAll('ytd-rich-item-renderer'); @@ -75,18 +76,19 @@ chrome.storage.sync.get( }; /** - * Initialize filters + * Run */ + // Initial run of the filters hideWatchedVideos(); hideShortsSections(); - filterVideos(); + fadeVideosByLength(); - // Re-apply the filter every 5 seconds to handle dynamic content loading on YouTube + // Re-apply the filter every x seconds to handle dynamic content loading on YouTube setInterval(() => { hideWatchedVideos(); hideShortsSections(); - filterVideos(); + fadeVideosByLength(); }, 3000); } ); diff --git a/src/contentCategorize.css b/src/contentCategorize.css index 13a774d..f722a9f 100644 --- a/src/contentCategorize.css +++ b/src/contentCategorize.css @@ -28,7 +28,7 @@ /* Category Filter Buttons */ .sptcl-channel-filter-container, -.sptcl-category-filter-container { +.sptcl-subscription-filter-container { display: flex; flex-direction: row; justify-content: flex-start; diff --git a/src/contentCategorize.js b/src/contentCategorize.js index bcc9d7c..271dafa 100644 --- a/src/contentCategorize.js +++ b/src/contentCategorize.js @@ -1,35 +1,47 @@ +/** + * Chrome Extension Content Script. + * Will run on subscriptions page and channel page only. + */ + /** * Constants */ + +// Youtube page and blocks selectors const YTB_SELECTOR_CHANNEL_PAGE = '.ytd-page-manager[page-subtype="subscriptions-channels"]'; const YTB_SELECTOR_CHANNEL_RENDERER = 'ytd-channel-renderer'; + const YTB_SELECTOR_SUBSCRIPTION_PAGE = '.ytd-page-manager[page-subtype="subscriptions"]'; const YTB_SELECTOR_SUBSCRIPTION_RENDERER = 'ytd-rich-item-renderer'; +// Youtube channel name selectors +const SELECTOR_CHANNEL_LINK = '#main-link.channel-link'; +const SELECTOR_CHANNEL_NAME_LINK = '.ytd-channel-name > a.yt-formatted-string'; + +// SimpleTube Dropdown category const CATEGORY_DD_DEFAULT = 'Category'; const CLASS_CATEGORY_SELECT = 'sptcl-category-select'; const CLASS_CATEGORY_OPTION = 'sptcl-category-option'; -const SELECTOR_CHANNEL_LINK = '#main-link.channel-link'; -const SELECTOR_CHANNEL_NAME_LINK = '.ytd-channel-name > a.yt-formatted-string'; - +// SimpleTube Filter const CATEGORY_ALL = 'All'; const CATEGORY_NOT_ASSIGNED = 'Not Assigned'; + const CLASS_FILTER_CONTAINER_CHANNEL = 'sptcl-channel-filter-container'; const CLASS_FILTER_CONTAINER_SUBSCRIPTION = 'sptcl-subscription-filter-container'; const CLASS_FILTER_BUTTON = 'sptcl-filter-button'; + const SELECTOR_FILTER_BUTTON = '.sptcl-filter-button'; /** - * Get the channel name from the channel block - * @param {HTMLElement} contentEl - The channel block element - * @param {boolean} isChannelPage - If the page is a channel page - * @returns {string} - The channel name + * Helper Functions */ -function getChannelName(contentEl, isChannelPage = false) { + +// Get the channel name from the channel block +function getChannelName(block, isChannelPage = false) { const channelLink = isChannelPage - ? contentEl.querySelector(SELECTOR_CHANNEL_LINK) - : contentEl.querySelector(SELECTOR_CHANNEL_NAME_LINK); + ? block.querySelector(SELECTOR_CHANNEL_LINK) + : block.querySelector(SELECTOR_CHANNEL_NAME_LINK); // get the channel name from the link href, remove the leasding slash, and trim the whitespace, and lowercase the channel name for consistency const channelNameArr = channelLink?.href?.split('/') || []; @@ -38,101 +50,80 @@ function getChannelName(contentEl, isChannelPage = false) { return channelName; } -/** - * Render the filter buttons for each category - * @param {HTMLElement} filterContainerEl - The container element to append the buttons - * @param {string[]} categories - The list of categories - * @returns {void} - */ -function renderButtonsFilters(filterContainerEl, categories) { - categories.forEach(({ id, name }) => { - const filterButtonEl = document.createElement('span'); - filterButtonEl.textContent = name; - filterButtonEl.setAttribute('data-category-id', id); - filterButtonEl.classList.add(CLASS_FILTER_BUTTON); +// Render filter buttons for each categories +function renderButtonsFilters(filterContainer, categoriesList) { + categoriesList.forEach(({ id, name }) => { + const filterButton = document.createElement('span'); + filterButton.textContent = name; + filterButton.setAttribute('data-category-id', id); + filterButton.classList.add(CLASS_FILTER_BUTTON); // Set the default filter to "All" when the page is loaded if (name === CATEGORY_ALL) { - filterButtonEl.setAttribute('data-active', 'true'); + filterButton.setAttribute('data-active', 'true'); } - filterButtonEl.addEventListener('click', () => { + filterButton.addEventListener('click', () => { // Add an attribute data-active="true" to the selected filter - filterContainerEl.querySelectorAll(SELECTOR_FILTER_BUTTON).forEach((btn) => { + filterContainer.querySelectorAll(SELECTOR_FILTER_BUTTON).forEach((btn) => { btn.removeAttribute('data-active'); }); - filterButtonEl.setAttribute('data-active', 'true'); + filterButton.setAttribute('data-active', 'true'); }); - filterContainerEl.appendChild(filterButtonEl); + filterContainer.appendChild(filterButton); }); } -/** - * Apply the selected filter to the content - * @param {HTMLElement[]} contentArr - The list of contents to filter - * @param {string} category - The selected category - * @param {Object} channelCategoryAssigned - The list of channels assigned to each category - * @param {boolean} forChannelPage - If the page is a channel page - * @returns {void} - */ -function applyFilterToContent(contentArr, categoryId, channelCategoryAssigned, forChannelPage) { +// Apply the selected filter to the content +function applyFilterToContent(contentList, selectedCategoryId, channelCategoryAssignTable, forChannelPage) { // Apply the selected filter to the channels - contentArr.forEach((contentEl) => { - const channelName = getChannelName(contentEl, forChannelPage); + contentList.forEach((block) => { + const channelName = getChannelName(block, forChannelPage); // If the category is "Not Assigned" - if (categoryId === CATEGORY_NOT_ASSIGNED) { + if (selectedCategoryId === CATEGORY_NOT_ASSIGNED) { // Show the content only if the channel is not assigned to any category - if (!channelCategoryAssigned[channelName]) { - contentEl.style.display = ''; + if (!channelCategoryAssignTable[channelName]) { + block.style.display = ''; } else { - contentEl.style.display = 'none'; + block.style.display = 'none'; } } else { // If the category is assigned to the channel, show the content - if (channelCategoryAssigned[channelName] === categoryId) { - contentEl.style.display = ''; + if (channelCategoryAssignTable[channelName] === selectedCategoryId) { + block.style.display = ''; } else { - contentEl.style.display = 'none'; + block.style.display = 'none'; } } }); } -/** - * Apply the default "All" filter - * @param {HTMLElement[]} contentArr - The list of contents to filter - * @returns {void} - */ -function applyDefaultFilter(contentArr) { - contentArr.forEach((contentEl) => { - contentEl.style.display = ''; +// Apply the default "All" filter to the content +function applyDefaultFilter(contentList) { + contentList.forEach((block) => { + block.style.display = ''; }); } -/** - * Observe changes in the subscriptions page and reapply filters - * @param {HTMLElement} subscriptionsPageContainer - The container element of the subscriptions page - * @param {Object} channelCategoryAssigned - The list of channels assigned to each category - * @returns {void} - */ -function observeSubscriptionsPage(subscriptionsPageContainer, channelCategoryAssigned) { +// Observe changes in the subscriptions page and reapply filters +function observeSubscriptionsPage(subscriptionsPageContainer, channelCategoryAssignTable) { const observer = new MutationObserver(() => { - const activeFilterButtonEl = subscriptionsPageContainer.querySelector( + const activeFilterButton = subscriptionsPageContainer.querySelector( `${SELECTOR_FILTER_BUTTON}[data-active="true"]` ); - if (activeFilterButtonEl) { - const contentArr = subscriptionsPageContainer.querySelectorAll(YTB_SELECTOR_SUBSCRIPTION_RENDERER); - const category = activeFilterButtonEl.getAttribute('data-category-id'); + if (activeFilterButton) { + const contentList = subscriptionsPageContainer.querySelectorAll(YTB_SELECTOR_SUBSCRIPTION_RENDERER); + const category = activeFilterButton.getAttribute('data-category-id'); const forChannelPage = false; if (category === CATEGORY_ALL) { - applyDefaultFilter(contentArr); + applyDefaultFilter(contentList); } else { - applyFilterToContent(contentArr, category, channelCategoryAssigned, forChannelPage); + applyFilterToContent(contentList, category, channelCategoryAssignTable, forChannelPage); } } }); @@ -140,6 +131,10 @@ function observeSubscriptionsPage(subscriptionsPageContainer, channelCategoryAss observer.observe(subscriptionsPageContainer, { childList: true, subtree: true }); } +/** + * Main + */ + chrome.storage.sync.get( ['doCategorizeSubscription', 'categories', 'channelCategoryAssigned'], ({ doCategorizeSubscription, categories, channelCategoryAssigned }) => { @@ -147,61 +142,62 @@ chrome.storage.sync.get( categories.sort((a, b) => a.name.localeCompare(b.name)); /** - * Channel Page: Dropdown for Category Assignment + * Channel Page */ + // Dropdown for Category Assignment const renderChannelsPageCategoryDropdown = () => { // Get all channel blocks - const channelBlockElArray = document.querySelectorAll(YTB_SELECTOR_CHANNEL_RENDERER); + const channelBlocks = document.querySelectorAll(YTB_SELECTOR_CHANNEL_RENDERER); // If the page doesn't have any channel, skip - if (!channelBlockElArray.length) return; + if (!channelBlocks.length) return; // Add a dropdown for each channel to select categories - channelBlockElArray.forEach((channelBlockEl) => { + channelBlocks.forEach((channelBlockEl) => { // Get DOM elements - const actionsContainerEl = channelBlockEl.querySelector('#buttons'); + const actionsContainer = channelBlockEl.querySelector('#buttons'); // If the dropdown already exists, skip - if (actionsContainerEl.querySelectorAll(`.${CLASS_CATEGORY_SELECT}`).length > 0) return; + if (actionsContainer.querySelectorAll(`.${CLASS_CATEGORY_SELECT}`).length > 0) return; // Get channel name const channelName = getChannelName(channelBlockEl, true); // Create the category dropdown - const selectEl = document.createElement('select'); - selectEl.classList.add(CLASS_CATEGORY_SELECT); + const dropdown = document.createElement('select'); + dropdown.classList.add(CLASS_CATEGORY_SELECT); // Create a default option - const defaultOptionEl = document.createElement('option'); - defaultOptionEl.text = CATEGORY_DD_DEFAULT; - defaultOptionEl.classList.add(CLASS_CATEGORY_OPTION); + const defaultOption = document.createElement('option'); + defaultOption.text = CATEGORY_DD_DEFAULT; + defaultOption.classList.add(CLASS_CATEGORY_OPTION); - selectEl.appendChild(defaultOptionEl); + dropdown.appendChild(defaultOption); // Create each categories as an option categories.forEach(({ id, name }) => { - const optionEl = document.createElement('option'); - optionEl.text = name; - optionEl.value = id; - optionEl.classList.add(CLASS_CATEGORY_OPTION); + const option = document.createElement('option'); + option.text = name; + option.value = id; + option.classList.add(CLASS_CATEGORY_OPTION); // Set the selected option if the category is already assigned if (channelCategoryAssigned[channelName] === id) { - optionEl.selected = true; + option.selected = true; } - selectEl.appendChild(optionEl); + dropdown.appendChild(option); }); // Add event listener to save the selected category - selectEl.addEventListener('change', () => { - if (selectEl.value === CATEGORY_DD_DEFAULT) { + dropdown.addEventListener('change', () => { + if (dropdown.value === CATEGORY_DD_DEFAULT) { // Remove the category from the assigned list if the default option is selected delete channelCategoryAssigned[channelName]; } else { // Save the selected category - channelCategoryAssigned[channelName] = selectEl.value; + channelCategoryAssigned[channelName] = dropdown.value; } // Save the updated assigned list to storage @@ -209,14 +205,11 @@ chrome.storage.sync.get( }); // Append the dropdown to the channel actions container - actionsContainerEl.appendChild(selectEl); + actionsContainer.appendChild(dropdown); }); }; - /** - * Channel Page: Channel Filtered by Category Buttons - */ - + // Filter Channels by Category Buttons const renderChannelsPageFilters = () => { // Get DOM elements const channelPageContainer = document.querySelector(YTB_SELECTOR_CHANNEL_PAGE); @@ -224,42 +217,43 @@ chrome.storage.sync.get( // If filters do not exist, do create them... if (!channelPageContainer.querySelectorAll(`.${CLASS_FILTER_CONTAINER_CHANNEL}`).length) { // Create the filters container - const filterContainerEl = document.createElement('div'); - filterContainerEl.classList.add(CLASS_FILTER_CONTAINER_CHANNEL); + const filterContainer = document.createElement('div'); + filterContainer.classList.add(CLASS_FILTER_CONTAINER_CHANNEL); // Create filter buttons - renderButtonsFilters(filterContainerEl, [ + renderButtonsFilters(filterContainer, [ { id: CATEGORY_ALL, name: CATEGORY_ALL }, ...categories, { id: CATEGORY_NOT_ASSIGNED, name: CATEGORY_NOT_ASSIGNED }, ]); // Append the filters to the primary container - channelPageContainer.prepend(filterContainerEl); + channelPageContainer.prepend(filterContainer); } // Attach filter onclick event - const filterButtonsArr = channelPageContainer.querySelectorAll(SELECTOR_FILTER_BUTTON); + const filterButtons = channelPageContainer.querySelectorAll(SELECTOR_FILTER_BUTTON); - filterButtonsArr.forEach((filterButtonEl) => { - filterButtonEl.addEventListener('click', () => { - const contentArr = channelPageContainer.querySelectorAll(YTB_SELECTOR_CHANNEL_RENDERER); - const category = filterButtonEl.getAttribute('data-category-id'); + filterButtons.forEach((filterButton) => { + filterButton.addEventListener('click', () => { + const contentList = channelPageContainer.querySelectorAll(YTB_SELECTOR_CHANNEL_RENDERER); + const categoryId = filterButton.getAttribute('data-category-id'); const forChannelPage = true; - if (category === CATEGORY_ALL) { - applyDefaultFilter(contentArr); + if (categoryId === CATEGORY_ALL) { + applyDefaultFilter(contentList); } else { - applyFilterToContent(contentArr, category, channelCategoryAssigned, forChannelPage); + applyFilterToContent(contentList, categoryId, channelCategoryAssigned, forChannelPage); } }); }); }; /** - * Subscriptions Page: Videos Filtered by Category Buttons + * Subscriptions Page */ + // Filter Subscriptions by Category Buttons const renderSubscriptionsPageFilters = () => { // Get DOM elements const subscriptionsPageContainer = document.querySelector(YTB_SELECTOR_SUBSCRIPTION_PAGE); @@ -267,29 +261,29 @@ chrome.storage.sync.get( // If filters do not exist, do create them... if (!subscriptionsPageContainer.querySelectorAll(`.${CLASS_FILTER_CONTAINER_SUBSCRIPTION}`).length) { // Create the filters container - const filterContainerEl = document.createElement('div'); - filterContainerEl.classList.add(CLASS_FILTER_CONTAINER_SUBSCRIPTION); + const filterContainer = document.createElement('div'); + filterContainer.classList.add(CLASS_FILTER_CONTAINER_SUBSCRIPTION); // Create filter buttons - renderButtonsFilters(filterContainerEl, [{ id: CATEGORY_ALL, name: CATEGORY_ALL }, ...categories]); + renderButtonsFilters(filterContainer, [{ id: CATEGORY_ALL, name: CATEGORY_ALL }, ...categories]); // Append the filters to the primary container - subscriptionsPageContainer.prepend(filterContainerEl); + subscriptionsPageContainer.prepend(filterContainer); } // Attach filter onclick event - const filterButtonsArr = subscriptionsPageContainer.querySelectorAll(SELECTOR_FILTER_BUTTON); + const filterButtons = subscriptionsPageContainer.querySelectorAll(SELECTOR_FILTER_BUTTON); - filterButtonsArr.forEach((filterButtonEl) => { - filterButtonEl.addEventListener('click', () => { - const contentArr = subscriptionsPageContainer.querySelectorAll(YTB_SELECTOR_SUBSCRIPTION_RENDERER); - const category = filterButtonEl.getAttribute('data-category-id'); + filterButtons.forEach((filterButton) => { + filterButton.addEventListener('click', () => { + const contentList = subscriptionsPageContainer.querySelectorAll(YTB_SELECTOR_SUBSCRIPTION_RENDERER); + const categoryId = filterButton.getAttribute('data-category-id'); const forChannelPage = false; - if (category === CATEGORY_ALL) { - applyDefaultFilter(contentArr); + if (categoryId === CATEGORY_ALL) { + applyDefaultFilter(contentList); } else { - applyFilterToContent(contentArr, category, channelCategoryAssigned, forChannelPage); + applyFilterToContent(contentList, categoryId, channelCategoryAssigned, forChannelPage); } }); }); @@ -299,10 +293,12 @@ chrome.storage.sync.get( }; /** - * Initialize filters + * Run */ + // Initial run of the filters if (doCategorizeSubscription) { + // Re-apply the filter every x seconds to handle dynamic content loading on YouTube setInterval(() => { if (window.location.pathname === '/feed/channels') { renderChannelsPageCategoryDropdown(); diff --git a/src/options.js b/src/options.js index bd54495..10e5f48 100644 --- a/src/options.js +++ b/src/options.js @@ -3,6 +3,10 @@ * Will run on the options page of the Chrome extension. */ +/** + * Helper Functions + */ + function renderAlertMessage(message, error = false) { const messageElement = document.getElementById('sptid-alert-message'); messageElement.classList.remove('sptcl-error', 'sptcl-success');