Skip to content

Commit 4e590e4

Browse files
committed
fix: autoTopList is now called properly when needed
1 parent d7b46e9 commit 4e590e4

1 file changed

Lines changed: 63 additions & 67 deletions

File tree

user script/yt-playlist-filter.user.js

Lines changed: 63 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name YouTube Save to Playlist filter
33
// @namespace fred.vatin.yt-playlists-filter
4-
// @version 1.1.5
4+
// @version 1.1.6
55
// @description Tap P key to open the “save to playlist” menu where your can type to filter
66
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
77
// @author Fred Vatin, Flemming Steffensen
@@ -31,14 +31,7 @@
3131
// 1. Click the "…" menu button where there is the "save to playlist" menu entry
3232
// 2. In the dev tool > inspector, search for the selector: yt-button-shape > button[aria-label]
3333
// It should be around the 7th match. Copy the [aria-label] value and add it here.
34-
const MoreActionsButtonText = [
35-
"More actions",
36-
"Autres actions",
37-
"Mehr Aktionen",
38-
"Más acciones",
39-
"Altre azioni",
40-
"Mais ações",
41-
];
34+
const MoreActionsButtonText = ["More actions", "Autres actions", "Mehr Aktionen", "Más acciones", "Altre azioni", "Mais ações"];
4235
// 3. search for the selector: #items > ytd-menu-service-item-renderer yt-formatted-string
4336
// copy the text content
4437
const SaveButtonText = ["Save", "Enregistrer", "Speichern", "Guardar", "Salva"];
@@ -61,7 +54,9 @@
6154
* ℹ GLOBAL DEFINITION
6255
===========================================================================*/
6356
let URL = window.location.href;
57+
let PLAYLISTS = null;
6458
console.log("URL (at first loading): ", URL);
59+
const selector_MoreActionSubMenuItems = "#items > ytd-menu-service-item-renderer yt-formatted-string";
6560
const selector_MenuType1 = "yt-contextual-sheet-layout";
6661
const selector_MenuType2 = "ytd-add-to-playlist-renderer.ytd-popup-container";
6762
const selector_HeaderType1 = "yt-panel-header-view-model";
@@ -182,7 +177,6 @@
182177
*/
183178
let items = 0;
184179
function callback_NewMenu(mutationsList, obsv_PlaylistContainer) {
185-
186180
for (const mutation of mutationsList) {
187181
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
188182
// Iterate only on newly added nodes
@@ -240,12 +234,12 @@
240234
console.log(`❌ header not detected in menu`);
241235
}
242236
}
243-
244237
} else {
245-
console.log(`❌ new node seems to match selectors_PlaylistsMenu but for some unknown reason it doesn’t match selector_MenuType1 or selector_MenuType2`, node);
238+
console.log(
239+
`❌ new node seems to match selectors_PlaylistsMenu but for some unknown reason it doesn’t match selector_MenuType1 or selector_MenuType2`,
240+
node,
241+
);
246242
}
247-
248-
249243
}
250244
}
251245
}
@@ -255,21 +249,20 @@
255249
}
256250

257251
/**
258-
* MutationObserver callback called when a new playlists menu is added to DOM (detected by callback_NewMenu)
259-
*
260-
* Iterates over the provided MutationRecord list and, for each mutation (added nodes),
261-
* check if the required menu elements are there.
262-
* If yes, it adds the input filter.
263-
*
264-
* @param {MutationRecord[]} mutationsList - Array of MutationRecord objects provided by the observer.
265-
* @param {MutationObserver} obsv_MenuAvailable - The MutationObserver instance that invoked this callback (not used)
266-
* @returns {void}
267-
* @callback MutationCallback
268-
* @listens {MutationEvent} childList - Listens for changes in child elements
269-
*
270-
*/
252+
* MutationObserver callback called when a new playlists menu is added to DOM (detected by callback_NewMenu)
253+
*
254+
* Iterates over the provided MutationRecord list and, for each mutation (added nodes),
255+
* check if the required menu elements are there.
256+
* If yes, it adds the input filter.
257+
*
258+
* @param {MutationRecord[]} mutationsList - Array of MutationRecord objects provided by the observer.
259+
* @param {MutationObserver} obsv_MenuAvailable - The MutationObserver instance that invoked this callback (not used)
260+
* @returns {void}
261+
* @callback MutationCallback
262+
* @listens {MutationEvent} childList - Listens for changes in child elements
263+
*
264+
*/
271265
function callback_MenuAvailable(mutationsList, obsv_MenuAvailable) {
272-
273266
let header = null;
274267
let list = null;
275268

@@ -354,13 +347,17 @@
354347
for (const mutation of mutationsList) {
355348
const AriaHidden = mutation.target.ariaHidden;
356349

357-
// console.log(
358-
// `mutation type: ${mutation.type}, AriaHidden: ${AriaHidden}, attribute changed: ${mutation.attributeName}`,
359-
// mutation.target
360-
// );
350+
console.log(
351+
`callback_MenuOpen triggered. mutation type: ${mutation.type}, AriaHidden: ${AriaHidden}, attribute changed: ${mutation.attributeName}`,
352+
mutation.target
353+
);
361354

362355
if (!AriaHidden) {
363356
setFocus(InputId);
357+
if (PLAYLISTS) {
358+
console.log(`Use PLAYLISTS`, PLAYLISTS);
359+
autoTopList(PLAYLISTS);
360+
}
364361
}
365362
}
366363
}
@@ -416,10 +413,7 @@
416413
* isPlayer('https://youtu.be/dQw4w9WgXcQ');
417414
*/
418415
function isPlayer(url) {
419-
if (
420-
url.startsWith("https://www.youtube.com/watch?v=") ||
421-
url.startsWith("https://www.youtube.com/live/")
422-
) {
416+
if (url.startsWith("https://www.youtube.com/watch?v=") || url.startsWith("https://www.youtube.com/live/")) {
423417
console.log("✅ Player detected !");
424418
return true;
425419
} else {
@@ -440,19 +434,21 @@
440434
function addFilterInput(elements = {}) {
441435
const { list = null, header = null } = elements;
442436

443-
autoTopList(list);
437+
PLAYLISTS = list;
438+
439+
autoTopList(PLAYLISTS);
444440

445441
const existingFilterInput = document.getElementById(InputId);
446442

447443
if (!existingFilterInput) {
448-
449444
// handle MenuType1 vs MenuType2
450445
const isType2 = header.matches(selector_HeaderType2);
451446
let title = null;
452447
let selector_OpenMenuParent = null;
453448

454449
if (isType2) {
455450
title = header?.querySelector('span[role="text"]');
451+
title.style.marginRight = "20px";
456452
selector_OpenMenuParent = selector_OpenMenuParentType2;
457453
} else {
458454
title = header?.firstElementChild;
@@ -557,34 +553,34 @@
557553
let selector_ListItems = null;
558554

559555
if (isType2) {
560-
selector_ListItems = selector_ListItemsType2
556+
selector_ListItems = selector_ListItemsType2;
561557
selector_SelItems = selector_SelItemsType2;
562558
} else {
563-
selector_ListItems = selector_ListItemsType1
559+
selector_ListItems = selector_ListItemsType1;
564560
selector_SelItems = selector_SelItemsType1;
565561
}
566562

567563
// Collect all items
568564
listItems = Array.from(playlists.querySelectorAll(selector_ListItems));
569-
console.log("✅ autoTopList(playlist), listItems number: ", listItems.length);
565+
console.log("✅ autoTopList(playlists), listItems number: ", listItems.length);
570566

571567
// Separates the selected ones from the rest
572568
const selected = listItems.filter((item) => item.querySelector(selector_SelItems));
573569
const unselected = listItems.filter((item) => !item.querySelector(selector_SelItems));
574570

575571
if (selected.length > 0) {
576-
console.log(`✅ autoTopList(playlist), this video belongs to ${selected.length} playlist(s)`);
572+
console.log(`✅ autoTopList(playlists), this video belongs to ${selected.length} playlist(s)`);
577573
// Reinsert in the desired order
578574
[...selected, ...unselected].forEach((item) => {
579575
playlists.appendChild(item);
580576
});
581577

582-
console.log(`✅ autoTopList(playlist), playlists have been sorted.`);
578+
console.log(`✅ autoTopList(playlists), playlists have been sorted.`);
583579

584580
const sortedItems = Array.from(playlists.querySelectorAll(selector_ListItems));
585-
console.log("✅ autoTopList(playlist), sortedItems number: ", sortedItems.length);
581+
console.log("✅ autoTopList(playlists), sortedItems number: ", sortedItems.length);
586582
} else {
587-
console.log("❌ autoTopList(playlist): this video doesn’t belong to any existing playlist. Not sorting needed.");
583+
console.log("❌ autoTopList(playlists): this video doesn’t belong to any existing playlist. Not sorting needed.");
588584
}
589585
}
590586

@@ -595,7 +591,7 @@
595591
*
596592
* @param {string} el - The ID of the input element to focus.
597593
*/
598-
function setFocus(el) {
594+
async function setFocus(el) {
599595
const input = document.getElementById(el);
600596
if (input) {
601597
// reset filter
@@ -612,9 +608,8 @@
612608
const timeout = 100;
613609
console.log(`✅ "input" found. Set focus! Timeout = ${timeout}`);
614610
// delay required to set the focus
615-
setTimeout(() => {
616-
input.focus();
617-
}, timeout);
611+
await sleep(timeout);
612+
input.focus();
618613
} else {
619614
console.log(`❌ "input" not found. Focus not set!`);
620615
}
@@ -630,8 +625,7 @@
630625
*
631626
* @returns {void}
632627
*/
633-
function openSaveToPlaylistDialog() {
634-
628+
async function openSaveToPlaylistDialog() {
635629
let directSaveButton = null;
636630

637631
for (const text of DirectSaveButtonText) {
@@ -646,9 +640,7 @@
646640
console.log("✅ openSaveToPlaylistDialog(), direct save button found. Click it", directSaveButton);
647641
directSaveButton.click();
648642
} else {
649-
console.log(
650-
"❌ openSaveToPlaylistDialog(), direct save button NOT found. Search for More Actions menu"
651-
);
643+
console.log("❌ openSaveToPlaylistDialog(), direct save button NOT found. Search for More Actions menu");
652644

653645
let moreActionsButton = null;
654646
for (const text of MoreActionsButtonText) {
@@ -662,19 +654,14 @@
662654
if (moreActionsButton) {
663655
console.log("✅ openSaveToPlaylistDialog(), More Actions menu found. Click it");
664656
moreActionsButton.click();
665-
setTimeout(() => {
666-
const submenuItems = document.querySelectorAll(
667-
"#items > ytd-menu-service-item-renderer yt-formatted-string"
668-
);
669-
let found = false;
670-
submenuItems.forEach((item) => {
671-
if (SaveButtonText.includes(item.textContent.trim())) {
672-
item.click();
673-
console.log("✅ Click on Save item:", item);
674-
found = true;
675-
}
676-
});
677-
}, 250);
657+
await sleep(250);
658+
const submenuItems = document.querySelectorAll(selector_MoreActionSubMenuItems);
659+
submenuItems.forEach((item) => {
660+
if (SaveButtonText.includes(item.textContent.trim())) {
661+
item.click();
662+
console.log("✅ Click on Save item:", item);
663+
}
664+
});
678665
} else {
679666
console.error("❌ Neither a direct 'Save' button nor a 'More actions' button was found.");
680667
}
@@ -729,4 +716,13 @@
729716
IS_KEYPRESS_LISTENER_ACTIVE = false;
730717
}
731718
}
719+
720+
/**
721+
* Pauses execution for a specified duration.
722+
* @param {number} ms - The number of milliseconds to sleep.
723+
* @returns {Promise<void>} A promise that resolves after the specified delay.
724+
*/
725+
function sleep(ms) {
726+
return new Promise((resolve) => setTimeout(resolve, ms));
727+
}
732728
})();

0 commit comments

Comments
 (0)