utils_MERGED.js

message, countdown, GetElementByText, etc.

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://update.greasyfork.org/scripts/578557/1858458/utils_MERGEDjs.js

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ────────────────────── utils SYSTEM ──────────────────────
// June 23, 9:51 PM 2026
// 💡 This file contains helper and wrapper functions used by userscripts in various websites:
// - GetElementByText
// - CreateToggleButton
// - CreateTextButton
// - WaitElementToExist
// - WaitTextToExist
// - ConsoleLog
// - LogAndMessage
// - …and more

// 💡 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))}
let UrlObj        = location.href

// 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, msg="hide"){

    while (true) {

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

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

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

        if (msg == "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, msg="hide"){

    while (true) {

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

        if (ElementObj){

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

        if (msg == "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, msg="HideMessage"){

    while (true) {

        let ElementObj = document.querySelector(ElementID)

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

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

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

        // ─── ⏳ WAITING ELEMENT TO DISAPPEAR ─────────────
        if (msg == "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 ParentElementObj = sys_GetElementByText("Songs")
// ParentElementObj.click()
function sys_GetElementByText(text, msg="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 (msg == "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 (msg == "showGUI") 
            message('❌ error: not found "' + text + '" (sys_GetElementByText)', "GUI_v1", "red", 0, "y80", 17, 3000) 

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

// let Energy0button  = sys_CreateTextButton("0", "x1", "y1", "s15", "green", "white") ; ⚠️ double check the position
function sys_CreateTextButton(text, xpos, ypos, FontSize, BgColor, FontColor, pos = "absolute") {

    // ─── 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
    let PaddingHoriz = Math.round(FontSize * 0.4) // horizontal padding

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

    // ─── Apply styles ─────────────
    Object.assign(TextButton.style, {
        // position    : "absolute",
        position    : pos,
        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",
        zIndex      : "99999"
    });

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

function sys_CreateToggleButton(ConfigObj) {
    let ToggleVar = ConfigObj.initialState;
    
    let container = document.createElement('div')
    container.style.position       = 'fixed'
    container.style.top            = ConfigObj.location?.top  || '1.8%'
    container.style.left           = ConfigObj.location?.left || '80%'
    container.style.zIndex         = '9999'
    container.style.display        = 'flex'
    container.style.flexDirection  = 'column'
    container.style.alignItems     = 'center'
    container.style.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: 32px;
        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;
        overflow: hidden;
    `

    let onText = document.createElement('span')
    onText.textContent = 'ON'
    onText.style.cssText = `
        position: absolute;
        left: 4px;
        top: 45%;
        transform: translateY(-50%);
        font-size: 8px;
        font-weight: normal;
        color: white;
        font-family: 'Segoe UI', Arial, sans-serif;
        user-select: none;
        opacity: ${ToggleVar ? '1' : '0'};
        transition: opacity 0.2s;
    `

    let offText = document.createElement('span')
    offText.textContent = 'OFF'
    offText.style.cssText = `
        position: absolute;
        right: 3px;
        top: 45%;
        transform: translateY(-50%);
        font-size: 8px;
        font-weight: 700;
        color: #555;
        font-family: 'Segoe UI', Arial, sans-serif;
        user-select: none;
        opacity: ${ToggleVar ? '0' : '1'};
        transition: opacity 0.2s;
    `

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

    slider     .appendChild(onText)
    slider     .appendChild(offText)
    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(18px)'
            onText.style.opacity          = '1'
            offText.style.opacity         = '0'
        } 
        
        else {
            slider.style.backgroundColor  = '#ccc'
            SliderButton.style.transform  = 'translateX(0)'
            onText.style.opacity          = '0'
            offText.style.opacity         = '1'
        }
        
        localStorage.setItem(ConfigObj.storageKey, ToggleVar)
        ConfigObj.callback()
    })

    document.body.appendChild(container)
    
    return { 
        container,  
        slider, 
        SliderButton,
        checkbox,    
        GetToggleValue: () => ToggleVar,
        SetToggleValue: (NewToggleValue) => {
            ToggleVar = NewToggleValue
            checkbox.checked              = NewToggleValue
            slider.style.backgroundColor  = NewToggleValue ? '#2196F3' : '#ccc'
            SliderButton.style.transform  = NewToggleValue ? 'translateX(19px)' : 'translateX(0)'
            onText.style.opacity          = NewToggleValue ? '1' : '0'
            offText.style.opacity         = NewToggleValue ? '0' : '1'
            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)
    }
}

function sys_AddClassToTopChildrenOld(ContainerObj, ClassName){

    TopChildrenArr = Array.from(ContainerObj.children)

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

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

// ───────────── REVISE ─────────────
// 💡 example 💡
// G_TopChildPostArr = sys_GetTopChildren(G_ContainerPostObj, G_ContainerPostID, "TopChildPostsClass")
function sys_GetTopChildren(ContainerObj, ContainerID, ClassName, ShowMsg = false){
    ContainerObj = sys_GetContainerObj(ContainerObj, ContainerID, ShowMsg)
    sys_AddClassToTopChildren(ContainerObj, ClassName)
    return sys_GetTopChildrenByClass(ClassName)
}

function sys_GetContainerObj(ContainerObj, ContainerID, ShowMsg){

    ContainerObj = document.querySelector(ContainerID)

    if (!ContainerObj){
        LogAndMessage('❌ error: not found ContainerObj', true, ShowMsg, "red")
        return false // <---
    }

    else if (ContainerObj){
        LogAndMessage('✔️ success: found ContainerObj', true, ShowMsg)
        return ContainerObj
    }
}

function sys_AddClassToTopChildren(ContainerObj, ClassName){
    let TopChildrenArr = Array.from(ContainerObj.children)

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

function sys_GetTopChildrenByClass(ClassName){
    return document.querySelectorAll("." + ClassName) // NodeList
}

// 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)
//     }
// }

function sys_AddClassToArrayElements(elements_arr, TargetElementID, ClassNameToUse) {

    elements_arr.forEach(AddClassToElement)

    // ─── CALLBACK ─────────────
    function AddClassToElement(element) {

        let FoundTargetElement = element.querySelector(TargetElementID) // ✅ captures params via closure

        if (!FoundTargetElement) {
            return ExitLog('❌ error: not found FoundTargetElement')
        }

        else if (FoundTargetElement) {
            FoundTargetElement.classList.add(ClassNameToUse) // ✅ ClassNameToUse is in closure scope
        }
    }
}

function ExitLog(text){
    ConsoleLog(text)
    // return false // <--- not needed
}

// let svg_SortButton = '<svg width="30px" height="30px" fill="#0080ff" viewBox="0 0 24 24" id="sort-ascending" data-name="Flat Line" xmlns="http://www.w3.org/2000/svg" class="icon flat-line" stroke="#0080ff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><polyline id="primary" points="10 15 6 19 2 15" style="fill: none; stroke: #0080ff; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></polyline><path id="primary-2" data-name="primary" d="M6,19V4M20,16H15m5-5H13m7-5H10" style="fill: none; stroke: #0080ff; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></path></g></svg>'

// 💡 #1 example for: sys_AddClassToTopChildren
// function gfork_AddClassTopChildren(){
//     let G_ContainerPostObj = document.querySelector(G_ContainerPostID)

//     if (!G_ContainerPostObj){
//         LogAndMessage('error: not found G_ContainerPostObj', true, true, "red")
//     }

//     else if (G_ContainerPostObj){
//         G_TopChildPostArr = sys_AddClassToTopChildren(G_ContainerPostObj, "TopChildPostsClass")
//         LogAndMessage('success: added TopChildPostsClass', true, true)
//     }
// }

// 💡 #2 example for: sys_AddClassToTopChildren
// let TopChildrenArr = sys_AddClassToTopChildren(ContainerElement, "TopChildSongsClass")

// 💡 example for: sys_GetTopChildren
// function gfork_GetTopChildren(){
//     G_TopChildPostArr = sys_GetTopChildren(G_ContainerPostObj, G_ContainerPostID, "TopChildPostsClass")
// }

// function sys_GetTopChildren(ContainerObj, ContainerID, ClassName, msg = true){

//     ContainerObj = document.querySelector(ContainerID)

//     if (!ContainerObj){
//         LogAndMessage('❌ error: not found ContainerObj', true, msg, "red")
//         return // <---
//     }

//     else if (ContainerObj){
//         let NodeList = AddClassAndReturnNodeList()
//         LogAndMessage('✔️ success: added ContainerObj', true, msg)
//         return NodeList
//     }

//     function AddClassAndReturnNodeList(){
//         let TopChildrenArr = Array.from(ContainerObj.children)

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

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


// ────────────────────── utils MESSAGE ──────────────────────

// for bridge script test
// alert('first line')
// alert('second line')

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
}

function LogAndMessage(text, LogState = false, MessageState = false, MessageColor = "blue"){
    if (LogState)
        ConsoleLog(text)

    if (MessageState)
        message(text, "GUI_v1", MessageColor, 0, "y80", 17, 3000)
}

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');
    
    LogAndMessage("☑️ success: " + PlaylistPositionStr + " playlist sorted", true, false)
}

function YTM_SortPlaylists(a, b) {
    const LAST = ["Liked Music", "Episodes for Later"];

    let aLast = LAST.includes(a.title);
    let bLast = LAST.includes(b.title);
    if (aLast && bLast) return LAST.indexOf(a.title) - LAST.indexOf(b.title);
    if (aLast) return 1;
    if (bLast) return -1;

    let numRegex = /^(\d+)\.\s+/;
    let aMatch   = a.title.match(numRegex);
    let bMatch   = b.title.match(numRegex);
    
    if (aMatch && bMatch) {
        let aNum = parseInt(aMatch[1]);
        let bNum = parseInt(bMatch[1]);
        if (aNum !== bNum) return aNum - bNum;
    }
    
    if (aMatch && !bMatch) return -1;
    if (!aMatch && bMatch) return 1;

    // ── Helpers ────────────────────────────────────────────
    function getSpeedRank(title) {
        let t = title.toLowerCase();
        if (/\bslow\b/.test(t))         return 0;
        if (/\bmid\b/.test(t))          return 1;
        if (/\b(fast|super)\b/.test(t)) return 2;
        return -1;
    }

    function getBaseName(title) {
        return title
            .replace(/\b(slow|mid|fast|super)\b/gi, '')
            .replace(/[\/\s]+/g, ' ')
            .trim();
    }

    // ── 1. Compare base names first ────────────────────────
    let aBase = getBaseName(a.title);
    let bBase = getBaseName(b.title);
    let baseCompare = aBase.localeCompare(bBase);
    if (baseCompare !== 0) return baseCompare;

    // ── 2. Same base name → sort by speed ─────────────────
    let aRank = getSpeedRank(a.title);
    let bRank = getSpeedRank(b.title);
    if (aRank !== bRank) return aRank - bRank;

    // ── 3. Fallback ────────────────────────────────────────
    return a.title.localeCompare(b.title);
}

// function YTM_SortPlaylists(a, b) {
//     let numRegex = /^(\d+)\.\s+/;
//     let aMatch   = a.title.match(numRegex);
//     let bMatch   = b.title.match(numRegex);
    
//     if (aMatch && bMatch) {
//         let aNum = parseInt(aMatch[1]);
//         let bNum = parseInt(bMatch[1]);
//         if (aNum !== bNum) return aNum - bNum;
//     }
    
//     if (aMatch && !bMatch) return -1;
//     if (!aMatch && bMatch) return 1;

//     // ── Helpers ────────────────────────────────────────────
//     function getSpeedRank(title) {
//         let t = title.toLowerCase();
//         if (/\bslow\b/.test(t))         return 0;
//         if (/\bmid\b/.test(t))          return 1;
//         if (/\b(fast|super)\b/.test(t)) return 2;
//         return -1;
//     }

//     function getBaseName(title) {
//         return title
//             .replace(/\b(slow|mid|fast|super)\b/gi, '')
//             .replace(/[\/\s]+/g, ' ')
//             .trim();
//     }

//     // ── 1. Compare base names first ────────────────────────
//     let aBase = getBaseName(a.title);
//     let bBase = getBaseName(b.title);
//     let baseCompare = aBase.localeCompare(bBase);
//     if (baseCompare !== 0) return baseCompare;

//     // ── 2. Same base name → sort by speed ─────────────────
//     let aRank = getSpeedRank(a.title);
//     let bRank = getSpeedRank(b.title);
//     if (aRank !== bRank) return aRank - bRank;

//     // ── 3. Fallback ────────────────────────────────────────
//     return a.title.localeCompare(b.title);
// }

// put this playlist titles at the end: "Liked Music" and "Episodes for Later"

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