message, countdown, GetElementByText, etc.
Hindi dapat direktang i-install ang script na ito. Ito ay isang library para sa iba pang mga script na isasama sa meta directive. // @require https://update.greasyfork.org/scripts/578557/1853216/utils_MERGEDjs.js
// ────────────────────── utils SYSTEM ──────────────────────
// June 17, 6:52 AM 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))}
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", "x15.8", "y86.3", "s15", "green", "white")
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 : "9999"
});
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() // does not run on button creation. only when toggle is clicked.
// })
// 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_CreateToggleButton(ConfigObj) {
// // ConfigObj = { initialState, storageKey, label, position }
// 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: 44px;
// 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;
// `
// // ─── ON label (left side, visible when ON) ───────────────────────
// let onText = document.createElement('span')
// onText.textContent = 'ON'
// onText.style.cssText = `
// position: absolute;
// left: 5px;
// top: 45%;
// transform: translateY(-50%);
// font-size: 6px;
// font-weight: 700;
// color: white;
// font-family: 'Segoe UI', Arial, sans-serif;
// user-select: none;
// opacity: ${ToggleVar ? '1' : '0'};
// transition: opacity 0.2s;
// `
// // ─── OFF label (right side, visible when OFF) ─────────────────────
// let offText = document.createElement('span')
// offText.textContent = 'OFF'
// offText.style.cssText = `
// position: absolute;
// right: 4px;
// top: 45%;
// transform: translateY(-50%);
// font-size: 6px;
// 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;
// content: "";
// height: 7px;
// width: 7px;
// left: 3px;
// bottom: 3px;
// background-color: black;
// border-radius: 50%;
// transition: 0.3s;
// transform: ${ToggleVar ? 'translateX(28px)' : '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(28px)'
// 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(28px)' : 'translateX(0)'
// onText.style.opacity = NewToggleValue ? '1' : '0'
// offText.style.opacity = NewToggleValue ? '0' : '1'
// localStorage.setItem(ConfigObj.storageKey, NewToggleValue)
// }
// }
// }
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
}
// 💡 #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 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬