utils_MERGED.js

message, countdown, GetElementByText, etc.

Этот скрипт недоступен для установки пользователем. Он является библиотекой, которая подключается к другим скриптам мета-ключом // @require https://update.greatest.deepsurf.us/scripts/578557/1827723/utils_MERGEDjs.js

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ────────────────────── utils SYSTEM ──────────────────────
// May 17, 7:17 PM 2026
// 💡 What I learned from tampermonkey:
// - The local host script updates instantly, so there is no issue in that regard.
// - The issue is with Tampermonkey; it does not always update.
// - need to wait 10 seconds before it updates to the latest version
// - Need to manually click "update" in the external tab to immediately get the latest version
// - cache buster also works

// 💡 GM_xmlhttpRequest:
// - unstable. does not always work

let StayLoop      = true
let HasExecuted   = false
let OriginalTitle = false
let sleep         = (ms) => {return new Promise(resolve => setTimeout(resolve, ms))}

// function TestSnappy(){
//     // message("☑️ first - test snappy", "GUI_v1", "blue", 0, "y80", 17, 3000)
//     // message("⚠️ second - test snappy", "GUI_v1", "blue", 0, "y80", 17, 3000)
//     // alert('first - test snappy')
//     alert('second - test snappy')
// }    

function ConsoleLog(text){
    console.log(text)
}

function sys_StayLoopOffOn() {
    message("stop loop", "GUI_v1", "red", 0, "y80", 16, 3000)
    StayLoop = false

    setTimeout(() => {
        StayLoop = true
        message("STAY LOOP: true", "GUI_v1", "blue", 0, "y80", 16, 2000)
    }, 1000);
}

function sys_GetOriginalTitle(){
    if (HasExecuted) return
    OriginalTitle = document.title
    HasExecuted = true // don't get title ever again
}    

async function sys_SetWintitle(signal, data = '', ms = 2000) {
    // OriginalTitle = document.title; issue: it gets wrong title, coz "get original title" invokes immediately even before changing back to original title
    sys_GetOriginalTitle()  
    
    document.title = signal + " " + data
    ConsoleLog("success: wintitle set: " + signal)

    await sleep(ms) // 2 seconds

    document.title = OriginalTitle
}

// pinVSCODE
// sys_AddSVG(OpenHistory, "fixed", '2.7%',  '82.9%',  '<svg width="24px" height="24px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <rect width="48" height="48" fill="white" fill-opacity="0.01"></rect> <path d="M5.81824 6.72729V14H13.091" stroke="#0080ff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M4 24C4 35.0457 12.9543 44 24 44V44C35.0457 44 44 35.0457 44 24C44 12.9543 35.0457 4 24 4C16.598 4 10.1351 8.02111 6.67677 13.9981" stroke="#0080ff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M24.005 12L24.0038 24.0088L32.4832 32.4882" stroke="#0080ff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>')
function sys_AddSVG(callback, pos, top, left, SVGstring){
    let SVGbutton              = document.createElement('button')
    SVGbutton.style.position   = pos // 🅿️ Position (variable): fixed / absolute 
    SVGbutton.innerHTML        = SVGstring
    SVGbutton.style.border     = "none"
    SVGbutton.style.top        = top // '80%'
    SVGbutton.style.left       = left // '80%'
    SVGbutton.style.padding    = '0px'
    SVGbutton.style.background = "none"
    SVGbutton.style.zIndex     = '999'
    SVGbutton.addEventListener('click', () => {
        callback()
    })
    
    document.body.appendChild(SVGbutton)
}

// ─── GET POSTS ─────────────
function sys_GetTopChildrenDoAction(PlaylistContainerID, callback) {
    
    let PostsContainer = document.querySelector(PlaylistContainerID)

    if (PostsContainer) {
        // message("success: found PostsContainer", "GUI_v1", "blue", 0, "y80", 17, 3000)

        // ─── YOU SHOULD USE FOREACH, LOOKS CLEANER. ─────────────
        let TopChildrenArr = Array.from(PostsContainer.children)
        TopChildrenArr.forEach(callback)
        
        // ─── FOR LOOP ─────────────
        // let TopChildrenArr = PostsContainer.children // get top level/direct/immediate children/descendant
        // for (let index = 0; index < TopChildrenArr.length; index++) {
        //     callback(TopChildrenArr, index)
        // }
    } 
    
    else {
        // message("error: posts container not found", "GUI_v1", "red", 0, "y80", 17, 3000)
        ConsoleLog("❌ error: PostsContainer not found (sys_GetTopChildrenDoAction)")
    }
}

// ─── FOREEACH ─────────────
// function POST_ACTION(SingleTopChildObj, index, array) {
//     ConsoleLog(`───────── Post ${index + 1} ─────────`)
//     ConsoleLog(SingleTopChildObj.innerText)
// }

// ─── FOR LOOP ─────────────
// function POST_ACTION(TopChildrenArr, index) {
//     ConsoleLog(`───────── Post ${index + 1} ─────────`)
//     ConsoleLog(TopChildrenArr[index].innerText)
// }

async function sys_WaitTextToExist(text, MessageStatus="hide"){

    while (true) {

        if (document.body.innerText.includes(text)){

            if (MessageStatus == "showGUI") 
                message('☑️ success: found  ' + text + ' (sys_WaitTextToExist)', "GUI_v1", "blue", 0, "y80", 17, 3000) 

            ConsoleLog('☑️ success: found "' + text + '" (sys_WaitTextToExist)')
            break
        }

        if (MessageStatus == "showGUI")  
            message('⏳ waiting for "' + text + '" (sys_WaitTextToExist)', "GUI_v1", "green", 0, "y80", 17, 3000) 

        ConsoleLog('⏳ waiting for "' + text + '" (sys_WaitTextToExist)')
        await sleep(100)
    }
}

async function sys_WaitElementToExist(ElementID, label=null, MessageStatus="hide"){

    while (true) {

        let ElementObj = document.querySelector(ElementID)
        let DisplayName = label || ElementID  // use label if provided, else fallback to raw selector

        if (ElementObj){

            if (MessageStatus == "showGUI") 
                message('☑️ success: found "' + DisplayName + '" (sys_WaitElementToExist)', "GUI_v1", "blue", 0, "y80", 17, 3000) 
                // message('☑️ success: found "(' + DisplayName + ' - ' + ElementObj + ')" (sys_WaitElementToExist)', "GUI_v1", "blue", 0, "y80", 17, 3000) 
            
            ConsoleLog('☑️ success: found "' + DisplayName + '" (sys_WaitElementToExist)')
            // ConsoleLog('☑️ success: found "(' + DisplayName + ' - ' + ElementObj + ')" (sys_WaitElementToExist)')
            return ElementObj
        }

        if (MessageStatus == "showGUI")  
            message('⏳ waiting for "' + DisplayName + '" (sys_WaitElementToExist)', "GUI_v1", "black", 0, "y80", 17, 3000) 

        ConsoleLog('⏳ waiting for "' + DisplayName + '" (sys_WaitElementToExist)')
        await sleep(100)
    }
}

async function sys_WaitElementToDisappear(ElementID, MessageStatus="HideMessage"){

    while (true) {

        let ElementObj = document.querySelector(ElementID)

        // ─── ☑️ ELEMENT DISAPPEARED ─────────────
        if (!ElementObj){

            if (MessageStatus == "ShowMessage") {
                message('☑️ success: element gone', "GUI_v1", "blue", 0, "y80", 16, 3000) 
            }

            ConsoleLog('☑️ success: element gone (sys_WaitElementToDisappear)')
            return // <---
        }

        // ─── ⏳ WAITING ELEMENT TO DISAPPEAR ─────────────
        if (MessageStatus == "ShowMessage") {
            message('⏳ waiting element to disappear', "GUI_v1", "black", 0, "y80", 16, 3000) 
        }  

        ConsoleLog('⏳ waiting element to disappear (sys_WaitElementToExist)')
        await sleep(100)
    }
}

// use case:
// let ParentElementOfText = sys_GetElementByText("Songs")
// ParentElementOfText.click()
function sys_GetElementByText(text, MessageStatus="hide"){

    // ⚠️ CASE SENSITIVE: GetElementByText("Private") and GetElementByText("private") are not the same
    
    let AllElements_arr     = Array.from(document.querySelectorAll("*"))
    let ParentElementOfText = AllElements_arr.find(GetElementByText)

    function GetElementByText(CurrentElement){

        let CurrentElementChildNodes_arr = Array.from(CurrentElement.childNodes)
        
        if (CurrentElementChildNodes_arr.some(FindTargetTextNode)){
            return true
        }
            
        function FindTargetTextNode(CurrentChildNode) {
            // if current element child node is a text node, and that text node is the 'text' variable (eg. Songs),
            // return that element (return true)
            if (CurrentChildNode.nodeType === Node.TEXT_NODE && CurrentChildNode.textContent == text)
                return true
        }
    }

    if (ParentElementOfText) {

        if (MessageStatus == "showGUI")  
            message('☑️ success: found "' + text + '" (sys_GetElementByText)', "GUI_v1", "blue", 0, "y80", 17, 3000) 

        ConsoleLog('☑️ success: found "' + text + '" (sys_GetElementByText)')
        // ConsoleLog('☑️ ParentElementOfText: ' + ParentElementOfText + ' (sys_GetElementByText)')
        return ParentElementOfText
    } 
    
    else if (!ParentElementOfText) {

        if (MessageStatus == "showGUI") 
            message('❌ error: not found "' + text + '" (sys_GetElementByText)', "GUI_v1", "red", 0, "y80", 17, 3000) 

        ConsoleLog('❌ error: not found "' + text + '" (sys_GetElementByText)')
        return false
    }
}

function sys_CreateTextButton(text, xpos, ypos, FontSize, BgColor, FontColor) {

    // ─── Parse prefixed args ─────────────
    xpos     = parseFloat(xpos.slice(1))     // "x50" → 50
    ypos     = parseFloat(ypos.slice(1))     // "y20" → 20
    FontSize = parseFloat(FontSize.slice(1)) // "s14" → 14

    // ─── Derived sizing (auto-adjust padding & margin based on font size) ─────────────
    let PaddingVert  = Math.round(FontSize * 0.3) // vertical padding
    let PaddingHoriz = Math.round(FontSize * 0.7) // horizontal padding

    // ─── Create element ─────────────
    let TextButton = document.createElement("button");
    TextButton.textContent = text;

    // ─── Apply styles ─────────────
    Object.assign(TextButton.style, {
        position    : "absolute",
        left        : `${xpos}%`,
        top         : `${ypos}%`,
        transform   : "translate(-50%, -50%)",   // center on the x/y point
        fontSize    : `${FontSize}px`,
        padding     : `${PaddingVert}px ${PaddingHoriz}px`,

        // Sensible defaults (override as needed)
        cursor      : "pointer",
        border      : "none",
        borderRadius: "3px",
        background  : BgColor,
        color       : FontColor,
        fontFamily  : "consolas",
        lineHeight  : "1",
        whiteSpace  : "nowrap",   // prevent wrapping that would break the size math
        boxSizing   : "border-box",
    });

    document.body.appendChild(TextButton)
    return TextButton;
}

// pinVSCODE
function sys_CreateToggleButton(ConfigObj) {
    // ConfigObj = { initialState, storageKey, label, position }
    let ToggleVar = ConfigObj.initialState;
    
    let container = document.createElement('div')
    container.style.position       = 'fixed' // 🅿️ Position (single)
    container.style.top            = ConfigObj.location?.top  || '1.8%'
    container.style.left           = ConfigObj.location?.left || '80%'
    // container.style.zIndex         = '999' // ⚠️ does not show for youtube
    container.style.zIndex         = '9999'
    container.style.display        = 'flex'
    container.style.flexDirection  = 'column'
    container.style.alignItems     = 'center'
    container.style.gap            = '4px'

    // container.style.cssText = `
    //     position: fixed;
    //     top: ${ConfigObj.position?.top || '1.8%'};
    //     left: ${ConfigObj.position?.left || '80%'};
    //     z-index: 9999;
    //     display: flex;
    //     flex-direction: column;
    //     align-items: center;
    //     gap: 4px;
    // `

    let labelText = document.createElement('span')
    labelText.textContent = ConfigObj.label || 'toggle'
    labelText.style.cssText = `
        font-size: 10px;
        font-weight: 500;
        color: white;
        user-select: none;
        font-family: 'Segoe UI', Arial, sans-serif;
        text-shadow:
            -1px -1px 0 black,
            1px -1px 0 black,
            -1px  1px 0 black,
            1px  1px 0 black;
    `


    let switchLabel = document.createElement('label')
    switchLabel.id = ConfigObj.id
    switchLabel.style.cssText = `
        position: relative;
        display: inline-block;
        width: 25px;
        height: 13px;
        cursor: pointer;
    `

    let checkbox = document.createElement('input')
    checkbox.type = 'checkbox'
    checkbox.checked = ToggleVar
    checkbox.style.cssText = `
        opacity: 0;
        width: 0;
        height: 0;
    `

    let slider = document.createElement('span')
    slider.style.cssText = `
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: ${ToggleVar ? '#2196F3' : '#ccc'};
        border-radius: 26px;
        transition: 0.3s;
        outline: 1px solid black;
    `

    let SliderButton = document.createElement('span')
    SliderButton.style.cssText = `
        position: absolute;
        content: "";
        height: 7px;
        width: 7px;
        left: 3px;
        bottom: 3px;
        background-color: black;
        border-radius: 50%;
        transition: 0.3s;
        transform: ${ToggleVar ? 'translateX(11px)' : 'translateX(0)'};
    `

    slider     .appendChild(SliderButton)
    switchLabel.appendChild(checkbox)
    switchLabel.appendChild(slider)
    container  .appendChild(labelText)
    container  .appendChild(switchLabel)

    checkbox.addEventListener('click', function(){

        ToggleVar = !ToggleVar
        
        if (ToggleVar) {
            slider.style.backgroundColor = '#2196F3'
            SliderButton.style.transform = 'translateX(11px)'
        } 
        
        else if (!ToggleVar) {
            slider.style.backgroundColor = '#ccc'
            SliderButton.style.transform = 'translateX(0)'
        }
        
        // ─── Save with unique key ─────────────
        localStorage.setItem(ConfigObj.storageKey, ToggleVar)
        // stores ToggleVar boolean value into string version. ie, true = "true", false = "false"
        
        // ─── RUN CALLBACK AFTER TOGGLE CLICKED (NO MATTER THE STATE) ─────────────
        ConfigObj.callback()
    })

    document.body.appendChild(container)
    
    return { 
        container,  
        slider, 
        SliderButton,
        checkbox,    
        GetToggleValue: () => ToggleVar,
        SetToggleValue: (NewToggleValue) => { // useful. lets you programmatically set the toggle's state aside from clicking the toggle button.
            ToggleVar = NewToggleValue
            checkbox.checked = NewToggleValue
            slider.style.backgroundColor = NewToggleValue ? '#2196F3' : '#ccc'
            SliderButton.style.transform = NewToggleValue ? 'translateX(11px)' : 'translateX(0)'
            localStorage.setItem(ConfigObj.storageKey, NewToggleValue)
        }
    }
}    

function sys_AddClassToElement(ContainerObj, ClassName){

    if (!ContainerObj){
        ConsoleLog('❌ error: not found ContainerObj (sys_AddClassToElement)')
        return // <---
    }

    else if (ContainerObj){
        ContainerObj.classList.add(ClassName)
    }
}

// let TopChildrenArr = sys_AddClassForTopChildren(ContainerElement, "TopChildSongsClass")

function sys_AddClassToTopChildren(ContainerObj, ClassName){

    TopChildrenArr = Array.from(ContainerObj.children)

    TopChildrenArr.forEach((TopChild) => {
        TopChild.classList.add(ClassName)
    })

    return document.querySelectorAll("." + ClassName)
}

// sys_AddClassToArrayElements(global_TopChildren_arr, ".title-column", "TitleColumn_AddClass")

function sys_AddClassToArrayElements(elements_arr, TargetElementID, ClassNameToUse){

    elements_arr.forEach(sys_AddClassToArrayElements)

    function sys_AddClassToArrayElements(element){

        let FoundTargetElement = element.querySelector(TargetElementID)

        if (!FoundTargetElement) {
            ConsoleLog('❌ error: not found FoundTargetElement')
            return // <---
        }

        // ─── ADD CLASS ─────────────
        FoundTargetElement.classList.add(ClassNameToUse)
    }
}



// ────────────────────── utils MESSAGE ──────────────────────
let hour = 3600000

function message(text, GUI, color, extra_xpos, ypos, fontsize, time){
    // MessageInstance.message("hello", "GUI_v1", "green", 0, "y80", 16, 3000)
    MessageInstance.message(text, GUI, color, extra_xpos, ypos, fontsize, time)
}

function HideMessage(GUI){
    message("hide GUI", GUI, "green", 0, "y200", 16, 100) // y200 = vertically hidden
}

class DYNAMIC_MESSAGE {
    constructor() {
        this.MessageElementsDict = {}; // Store references to active MessageInstance elements
        this.FadeTimersDict      = {}; // Store references to fade timers
    }

    message(text, MessageCategory, bgColor = 'green', extra_xpos = 0, ypos = "y10", fontSize = 10, duration = 2000) {
        this.hideMessage(MessageCategory);
        
        let MessageElement = document.createElement('div');
        MessageElement.innerText = text;

        // ─── base styles ─────────────
        Object.assign(MessageElement.style, {
            position:        'fixed', // 🅿️ Position (single)
            zIndex:          '999',
            paddingTop:      '8px',
            paddingBottom:   '8px',
            paddingLeft:     '11px',
            paddingRight:    '11px',
            borderRadius:    '4px',
            color:           'white',
            fontFamily:      'Consolas',
            fontSize:        `${fontSize}px`,
            backgroundColor: bgColor,
            boxShadow:       '0 2px 10px rgba(0, 0, 0, 0.2)',
            transition:      'opacity 1s ease-in-out',
            opacity:         '1',
            whiteSpace:      'nowrap',
        })

        // ▬▬▬ Y POSITION ▬▬▬▬▬▬▬▬▬▬▬▬▬
        let IntegerYposPercent = parseInt(ypos.replace(/[^0-9]/g, ''), 10)
        MessageElement.style.top = `${IntegerYposPercent}%`

        // ▬▬▬ PASS 1: show offscreen to measure width ▬▬▬▬▬▬▬▬▬▬▬▬▬
        MessageElement.style.left       = '0px'
        MessageElement.style.visibility = 'hidden'
        document.body.appendChild(MessageElement) // append inside body
        // document.documentElement.appendChild(MessageElement) // append in HTML ⚠️ error: does not work

        // ▬▬▬ PASS 2: calculate centered position then show ▬▬▬▬▬▬▬▬▬▬▬▬▬
        // Element.getBoundingClientRect() method returns a DOMRect object that provides information 
        // about the size of an element and its position relative to the viewport
        let RectObjWithDimension = MessageElement.getBoundingClientRect() // ⚠️ key method
        let centeredX            = (window.innerWidth - RectObjWithDimension.width) / 2
        let extra_xpos_px        = extra_xpos * (window.innerWidth / 100)

        MessageElement.style.left       = `${centeredX + extra_xpos_px}px`
        MessageElement.style.visibility = 'visible'

        this.MessageElementsDict[MessageCategory] = MessageElement;

        // ─── fade out ─────────────
        let fadeDelay     = 1000;
        let fadeStartTime = duration - fadeDelay;

        if (this.FadeTimersDict[MessageCategory]) clearTimeout(this.FadeTimersDict[MessageCategory])

        this.FadeTimersDict[MessageCategory] = setTimeout(() => {
            if (this.MessageElementsDict[MessageCategory] === MessageElement) {
                MessageElement.style.opacity = '0'
                setTimeout(() => {
                    if (this.MessageElementsDict[MessageCategory] === MessageElement) this.hideMessage(MessageCategory)
                }, fadeDelay)
            }
        }, fadeStartTime)

        return MessageElement
    }
    
    // Hide/remove a specific MessageInstance
    hideMessage(MessageCategory) {
        if (this.MessageElementsDict[MessageCategory]) {
            document.body.removeChild(this.MessageElementsDict[MessageCategory]);
            delete this.MessageElementsDict[MessageCategory];
            
            // Clear any pending fade timers
            if (this.FadeTimersDict[MessageCategory]) {
                clearTimeout(this.FadeTimersDict[MessageCategory]);
                delete this.FadeTimersDict[MessageCategory];
            }
        }
    }
    
    // Hide all active messages
    hideAllMessages() {
        for (let MessageCategory in this.MessageElementsDict) {
            if (this.MessageElementsDict.hasOwnProperty(MessageCategory)) {
                this.hideMessage(MessageCategory);
            }
        }
    }
}

// Create a global instance of the MessageInstance system
let MessageInstance = new DYNAMIC_MESSAGE();

// ────────────────────── utils COUNTDOWN ──────────────────────
function countdown(count, text = '', xpos = 0, ypos = "y75", fontSize = 17, color = "black", ms_display = "ms") {
    if (ms_display === "ms") 
        return countdown_ms(count, text, xpos, ypos, fontSize, color)
    else if (ms_display === "noms") 
        return countdown_noms(count, text, xpos, ypos, fontSize, color)
}

function countdown_ms(count, text = '', xpos = 0, ypos = "y75", fontSize = 17, color = "black") {
    let SKIP_GUI_THRESHOLD = 0.2

    // skip GUI, just sleep
    if (count <= SKIP_GUI_THRESHOLD) {
        return new Promise(resolve => setTimeout(resolve, count * 1000))
    }

    // show GUI once — duration slightly longer so it doesn't vanish before reaching 0
    message(text + count.toFixed(1), "countdown_GUI", color, xpos, ypos, fontSize, count * 1000 + 200)

    let startTime = Date.now()

    return new Promise(resolve => {
        let interval = setInterval(() => {
            let elapsed   = (Date.now() - startTime) / 1000
            let remaining = count - elapsed

            if (remaining <= 0) {
                clearInterval(interval)
                HideMessage("countdown_GUI")
                resolve()
                return
            }

            // update text in-place — no new GUI element
            let el = MessageInstance.MessageElementsDict["countdown_GUI"]
            if (el) el.innerText = text + remaining.toFixed(1)

        }, 100)
    })
}

function countdown_noms(count, text = '', xpos = 0, ypos = "y75", fontSize = 17, color = "black") {
    message(text + count, "countdown_GUI", color, xpos, ypos, fontSize, count * 1000 + 200)

    let remaining = count
    return new Promise(resolve => {
        let interval = setInterval(() => {
            remaining--

            let el = MessageInstance.MessageElementsDict["countdown_GUI"]
            if (el) el.innerText = text + remaining

            if (remaining <= 0) {
                clearInterval(interval)
                setTimeout(() => {
                    HideMessage("countdown_GUI")
                    resolve()
                }, 1000)   // show "0" for 1s — same as AHK version
            }
        }, 1000)
    })
}

//                       /▬▬▬▬▬▬▬▬▬\
// ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ NOT USING ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬


// ────────────────────── utils YT MUSIC ──────────────────────

async function YTM_SortAnyPlaylist(PlaylistPositionStr, PlaylistContainerID, TitleElementID){

    let PlaylistContainerObj = await sys_WaitElementToExist(PlaylistContainerID)

    // Get all playlist items (first level children)
    let TopChildren_arr = Array.from(PlaylistContainerObj.children) // need this parent elem for DOM change

    // Create an array of objects with the DOM element and its title
    let PlaylistObjects_arr = [];
    TopChildren_arr.forEach(PushElementsToPlaylistObjects)

    function PushElementsToPlaylistObjects(SingleTopChildObj){
        // let TitleElementObj = item.querySelector('#title') // this works as well
        let TitleElementObj = SingleTopChildObj.querySelector(TitleElementID) // this is more explicit

        if (TitleElementObj) {
            PlaylistObjects_arr.push({
                element: SingleTopChildObj,
                title: TitleElementObj.textContent.trim()
            })
        }
    }
    
    // ─── check if already sorted ─────────────
    let currentOrder = [...PlaylistObjects_arr];
    let sortedOrder  = [...PlaylistObjects_arr].sort(YTM_SortPlaylists);
    
    let IsAlreadySorted = currentOrder.every((item, index) => 
        item.title === sortedOrder[index].title
    );

    if (IsAlreadySorted) {
        ConsoleLog("👍 " + PlaylistPositionStr + " playlist already sorted. Will not sort.")
        return // <---
    }
    
    // Sort the playlist objects
    PlaylistObjects_arr.sort(YTM_SortPlaylists);
    
    // Create a document fragment to hold the sorted items
    let fragment = document.createDocumentFragment();
    
    // Move all playlist items to the fragment in sorted order
    PlaylistObjects_arr.forEach(obj => {
        fragment.appendChild(obj.element);
    });
    
    // Clear the container and append the sorted items
    while (PlaylistContainerObj.firstChild) {
        PlaylistContainerObj.removeChild(PlaylistContainerObj.firstChild);
    }

    PlaylistContainerObj.appendChild(fragment);
    
    // Create a list of numbered titles for display
    let numberedTitles = PlaylistObjects_arr.map((obj, index) => `${index + 1}. ${obj.title}`);
    let titlesText = numberedTitles.join('\n');
    
    ConsoleLog("☑️ success: " + PlaylistPositionStr + " playlist sorted")
    // message(☑️ PlaylistPositionStr + " playlist sorted", "GUI_v1", "blue", 0, "y93", 16, 3000)
}

// Sort function to handle numerical prefixes
function YTM_SortPlaylists(a, b) {
    // Extract any leading number from each title
    let numRegex = /^(\d+)\.\s+/;
    let aMatch   = a.title.match(numRegex);
    let bMatch   = b.title.match(numRegex);
    
    // If both have numerical prefixes
    if (aMatch && bMatch) {
        let aNum = parseInt(aMatch[1]);
        let bNum = parseInt(bMatch[1]);
        if (aNum !== bNum) {
            return aNum - bNum;
        }
    }
    
    // If only one has a numerical prefix
    if (aMatch && !bMatch) return -1;
    if (!aMatch && bMatch) return 1;
    
    // Alphabetical sorting
    return a.title.localeCompare(b.title);
}

//                       /▬▬▬▬▬▬▬▬▬\
// ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ NOT USING ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬