diff --git a/js&css/web-accessible/core.js b/js&css/web-accessible/core.js index cec0cbe63..52126a44b 100644 --- a/js&css/web-accessible/core.js +++ b/js&css/web-accessible/core.js @@ -35,6 +35,7 @@ var ImprovedTube = { channel_home_page_postfix: new RegExp('\/(featured)?\/?$'), thumbnail_quality: new RegExp('(default\.jpg|mqdefault\.jpg|hqdefault\.jpg|hq720\.jpg|sddefault\.jpg|maxresdefault\.jpg)+'), video_id: new RegExp('[?&]v=([^&]+)'), + video_time: new RegExp('[?&](?:t|start)=([^&]+)'), playlist_id: new RegExp('[?&]list=([^&]+)'), channel_link: new RegExp('https:\/\/www.youtube.com\/@|((channel|user|c)\/)') }, diff --git a/js&css/web-accessible/functions.js b/js&css/web-accessible/functions.js index 73311f96b..04a9c79ae 100644 --- a/js&css/web-accessible/functions.js +++ b/js&css/web-accessible/functions.js @@ -95,6 +95,7 @@ ImprovedTube.ytElementsHandler = function (node) { this.playlistReverse(); } } + this.playlistPopupUpdate(); } else if (name === 'YTD-GUIDE-SECTION-RENDERER') { if (!this.elements.sidebar_section) { this.elements.sidebar_section = node; @@ -120,7 +121,9 @@ ImprovedTube.ytElementsHandler = function (node) { if(document.documentElement.dataset.pageType === 'video'){ this.hideDetailButton(node.$['flexible-item-buttons'].children); } - } else if (name === 'YTD-SUBSCRIBE-BUTTON-RENDERER') { + } else if (name === 'YTD-PLAYLIST-HEADER-RENDERER' || (name === 'YTD-MENU-RENDERER' && node.classList.contains('ytd-playlist-panel-renderer'))) { + this.playlistPopupUpdate(); + } else if (name === 'YTD-SUBSCRIBE-BUTTON-RENDERER') { if (node.className.indexOf('ytd-c4-tabbed-header-renderer') !== -1) { ImprovedTube.blacklist('channel', node); } diff --git a/js&css/web-accessible/init.js b/js&css/web-accessible/init.js index ea9ed349a..d68a3096f 100644 --- a/js&css/web-accessible/init.js +++ b/js&css/web-accessible/init.js @@ -34,6 +34,9 @@ ImprovedTube.observer = new MutationObserver(function (mutationList) { for (var j = 0, k = mutation.addedNodes.length; j < k; j++) { ImprovedTube.childHandler(mutation.addedNodes[j]); } + for (const node of mutation.removedNodes){ + if(node.nodeName === 'BUTTON' && node.id === 'it-popup-playlist-button') ImprovedTube.playlistPopupUpdate(); + } } } }).observe(document.documentElement, { @@ -105,6 +108,7 @@ document.addEventListener('yt-page-data-updated', function (event) { ImprovedTube.playlistShuffle(); ImprovedTube.playlistReverse(); } + ImprovedTube.playlistPopupUpdate(); }); window.addEventListener('load', function () { diff --git a/js&css/web-accessible/www.youtube.com/playlist.js b/js&css/web-accessible/www.youtube.com/playlist.js index ccd9492d0..efaff86db 100644 --- a/js&css/web-accessible/www.youtube.com/playlist.js +++ b/js&css/web-accessible/www.youtube.com/playlist.js @@ -129,4 +129,94 @@ ImprovedTube.playlistShuffle = function () { { button.click(); } }, 5000); } +}; + +/*------------------------------------------------------------------------------ +4.5.5 POPUP +------------------------------------------------------------------------------*/ +/** + * ## Creates a playlist popup button (with ID `it-popup-playlist-button`) + * - used by/in {@linkcode ImprovedTube.playlistPopupUpdate} + * - checks {@linkcode ImprovedTube.storage.player_autoplay} if to autoplay the popuped playlist/video + * - checks {@linkcode ImprovedTube.elements.player} to get video ID and current time, if available, otherwise starts first video of playlist + * - popup has video players width/height or window (inner) width/height when video player is not available + * - the button has the playlist ID as `list` in its dataset and reads from it to open the popup + * @param {string | null} playlistID - the playlist ID or `null` + * @param {boolean} [altButtonStyle] - [optional] changes styling of the playlist popup button - `true` for minplayer and playlist panel and `false` for the playlist page - default `false` + * @param {boolean} [checkVideo] - [optional] if `true` checks the {@linkcode ImprovedTube.elements.player} to get the video ID, time, and size, if available, otherwise starts first video of playlist - default `false` (starts first video of playlist) + * @returns {HTMLButtonElement | null} the playlist popup button to insert into the DOM or `null` if the {@linkcode playlistID} is `null` + */ +ImprovedTube.playlistPopupCreateButton = function (playlistID, altButtonStyle, checkVideo) { + "use strict"; + if (playlistID == null) return null; + const button = document.createElement('button'), + svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'), + path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + + button.id = 'it-popup-playlist-button'; + button.className = `yt-spec-button-shape-next yt-spec-button-shape-next--${(altButtonStyle ?? false) ? 'text' : 'tonal'} yt-spec-button-shape-next--overlay yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-button style-scope ytd-playlist-header-renderer`; + button.title = 'Popup playlist'; + button.dataset.list = playlistID; + button.style.opacity = '0.8'; + button.addEventListener( + 'click', + (checkVideo ?? false) ? function (event) { + "use strict"; + const videoURL = ImprovedTube.elements.player?.getVideoUrl(); + if (videoURL != null && ImprovedTube.regex.video_id.test(videoURL)) { + ImprovedTube.elements.player.pauseVideo(); + window.open(`${location.protocol}//www.youtube.com/embed/${videoURL.match(ImprovedTube.regex.video_id)[1]}?autoplay=${(ImprovedTube.storage.player_autoplay ?? true) ? '1' : '0'}&start=${videoURL.match(ImprovedTube.regex.video_time)?.[1] ?? '0'}&list=${this.dataset.list}`, '_blank', `directories=no,toolbar=no,location=no,menubar=no,status=no,titlebar=no,scrollbars=no,resizable=no,width=${ImprovedTube.elements.player.offsetWidth ?? innerWidth},height=${ImprovedTube.elements.player.offsetHeight ?? innerHeight}`); + //! If the video is not in the playlist or not within the first 200 entries, then it automatically selects the first video in the list. + //! But this is okay since this button is mainly for the playlist, not the video (see the video popup button in player.js). + } else window.open(`${location.protocol}//www.youtube.com/embed/videoseries?autoplay=${(ImprovedTube.storage.player_autoplay ?? true) ? '1' : '0'}&list=${this.dataset.list}`, '_blank', `directories=no,toolbar=no,location=no,menubar=no,status=no,titlebar=no,scrollbars=no,resizable=no,width=${innerWidth},height=${innerHeight}`); + } : function (event) { + "use strict"; + window.open(`${location.protocol}//www.youtube.com/embed/videoseries?autoplay=${(ImprovedTube.storage.player_autoplay ?? true) ? '1' : '0'}&list=${this.dataset.list}`, '_blank', `directories=no,toolbar=no,location=no,menubar=no,status=no,titlebar=no,scrollbars=no,resizable=no,width=${innerWidth},height=${innerHeight}`); + }, + true + ); + + svg.style.width = '24px'; + svg.style.height = '24px'; + svg.style.pointerEvents = 'none'; + svg.style.fill = 'currentColor'; + svg.setAttribute('viewBox', '0 0 24 24'); + path.setAttribute('d', 'M19 7h-8v6h8V7zm2-4H3C2 3 1 4 1 5v14c0 1 1 2 2 2h18c1 0 2-1 2-2V5c0-1-1-2-2-2zm0 16H3V5h18v14z'); + + svg.append(path); + button.append(svg); + + return button; +}; +/** + * ## Adds a playlist popup button to each playlist panel found or update the links of existing popup buttons + * - buttons will be added on the playlist page (next to the share button), in the playlist panel (after the loop and shuffle buttons), and/or the mini playlist section of the mini player (after the loop and shuffle buttons) + * - uses {@linkcode ImprovedTube.playlistPopupCreateButton} to create each button + * - saves each button in {@linkcode ImprovedTube.elements.buttons} as `it-popup-playlist-button-playlist`, `it-popup-playlist-button-mini`, and `it-popup-playlist-button-panel` + * - called from {@linkcode ImprovedTube.ytElementsHandler} and {@linkcode ImprovedTube.hrefObserver} when DOM changes (somewhat related to playlist renderers) + */ +ImprovedTube.playlistPopupUpdate = function () { + "use strict"; + if (!(this.storage.playlist_popup ?? false)) return; + + const playlistID = location.search.match(this.regex.playlist_id)?.[1], + playlistIDMini = this.elements.player?.getPlaylistId?.(); + + if (!document.contains(this.elements.buttons['it-popup-playlist-button-playlist'])) { + const playlistShareButton = document.body.querySelector('ytd-app>div#content>ytd-page-manager>ytd-browse>ytd-playlist-header-renderer ytd-button-renderer.ytd-playlist-header-renderer:has(button[title])'); + if (playlistShareButton == null) this.elements.buttons['it-popup-playlist-button-playlist'] = null; + else playlistShareButton.insertAdjacentElement('afterend', this.elements.buttons['it-popup-playlist-button-playlist'] = this.playlistPopupCreateButton(playlistID)); + } else if (playlistID != null && this.elements.buttons['it-popup-playlist-button-playlist'].dataset.list !== playlistID) this.elements.buttons['it-popup-playlist-button-playlist'].dataset.list = playlistID; + + if (!document.contains(this.elements.buttons['it-popup-playlist-button-mini'])) { + const miniItemButtons = document.body.querySelector('ytd-app>ytd-miniplayer ytd-playlist-panel-renderer div#top-level-buttons-computed'); + if (miniItemButtons == null) this.elements.buttons['it-popup-playlist-button-mini'] = null; + else miniItemButtons.appendChild(this.elements.buttons['it-popup-playlist-button-mini'] = this.playlistPopupCreateButton(playlistIDMini, true, true)); + } else if (playlistIDMini != null && this.elements.buttons['it-popup-playlist-button-mini'].dataset.list !== playlistIDMini) this.elements.buttons['it-popup-playlist-button-mini'].dataset.list = playlistIDMini; + + if (!document.contains(this.elements.buttons['it-popup-playlist-button-panel'])) { + const panelItemButtons = document.body.querySelector('ytd-app>div#content>ytd-page-manager>ytd-watch-flexy ytd-playlist-panel-renderer div#top-level-buttons-computed'); + if (panelItemButtons == null) this.elements.buttons['it-popup-playlist-button-panel'] = null; + else panelItemButtons.appendChild(this.elements.buttons['it-popup-playlist-button-panel'] = this.playlistPopupCreateButton(playlistID, true, true)); + } else if (playlistID != null && this.elements.buttons['it-popup-playlist-button-panel'].dataset.list !== playlistID) this.elements.buttons['it-popup-playlist-button-panel'].dataset.list = playlistID; }; \ No newline at end of file diff --git a/menu/skeleton-parts/playlist.js b/menu/skeleton-parts/playlist.js index a7b6ea188..6aba3bf24 100644 --- a/menu/skeleton-parts/playlist.js +++ b/menu/skeleton-parts/playlist.js @@ -38,6 +38,10 @@ extension.skeleton.main.layers.section.playlist = { playlist_shuffle: { component: 'switch', text: 'shuffle' + }, + playlist_popup: { + component: 'switch', + text: 'popupPlayer' } } }