Youtube Button Come Back

YouTube 功能增強:歷史記錄懸浮按鈕 + 播放清單懸浮按鈕 + 頻道頁面播放全部按鈕 (純 Icon Path 判定)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Youtube Button Come Back
// @namespace    http://tampermonkey.net/
// @version      4.5
// @description  YouTube 功能增強:歷史記錄懸浮按鈕 + 播放清單懸浮按鈕 + 頻道頁面播放全部按鈕 (純 Icon Path 判定)
// @author       AI
// @match        https://www.youtube.com/*
// @require      https://update.greasyfork.org/scripts/419640/1775486/onElementReady.js
// @grant        none
// @license      MIT
// ==/UserScript==
// = CONFIG: 全局設定 =
const CONFIG = {
    NAVIGATION_DELAY: 200,
    PLAY_ALL_ENABLED: true,
    FLOAT_BUTTONS_ENABLED: true,
    PLAYLIST_FLOAT_ENABLED: true
}
// = CONFIG: 播放全部功能 =
const PLAY_CONFIG = {
    ENABLED_PATHS: [{ type: 'regex', value: '^/@[^/]+/(videos|shorts|streams)$' }],
    BTN_TEXT: { all: 'Play all', popular: 'Play popular', oldest: 'Play oldest' },
    BTN_SPACING: '0.5em'
}
// = CONFIG: 按鈕通用設定 =
const BTN_CONFIG = {
    FLOAT_PATHS: [
        { type: 'exact', value: '/' },
        { type: 'startsWith', value: '/feed/subscriptions' },
        { type: 'startsWith', value: '/feed/history' }
    ],
    PLAYLIST_PATHS: [{ type: 'startsWith', value: '/playlist?list=' }],
    SIZE: 36,
    SPACING: 4,
    BG_OPACITY: 0.9,
    TOP_MARGIN: 6,
    FLOAT_LEFT_MARGIN: 12,
    PLAYLIST_LEFT_MARGIN: 160,
    MENU_WAIT_TIMEOUT: 1000,
    MENU_CHECK_INTERVAL: 30,
    TRANSITION_SPEED: '0.15s',
    BATCH_SIZE: 15,
    BATCH_DELAY: 20,
    SUBSEQUENT_DELAY: 100
}
// = 常量定義:圖示路徑 =
const ICON_PATHS = {
    WATCH_LATER: 'M12 1C5.925 1 1 5.925 1 12s4.925 11 11 11 11-4.925 11-11S18.075 1 12 1Zm0 2a9 9 0 110 18.001A9 9 0 0112 3Zm0 3a1 1 0 00-1 1v5.565l.485.292 3.33 2a1 1 0 001.03-1.714L13 11.435V7a1 1 0 00-1-1Z',
    ADD_TO_PLAYLIST: 'M2 2.864v6.277a.5.5 0 00.748.434L9 6.002 2.748 2.43A.5.5 0 002 2.864ZM21 5h-9a1 1 0 100 2h9a1 1 0 100-2Zm0 6H9a1 1 0 000 2h12a1 1 0 000-2Zm0 6H9a1 1 0 000 2h12a1 1 0 000-2Z',
    SAVE_TO_PLAYLIST: 'M19 2H5a2 2 0 00-2 2v16.887c0 1.266 1.382 2.048 2.469 1.399L12 18.366l6.531 3.919c1.087.652 2.469-.131 2.469-1.397V4a2 2 0 00-2-2ZM5 20.233V4h14v16.233l-6.485-3.89-.515-.309-.515.309L5 20.233Z',
    REMOVE: 'M19 3h-4V2a1 1 0 00-1-1h-4a1 1 0 00-1 1v1H5a2 2 0 00-2 2h18a2 2 0 00-2-2ZM6 19V7H4v12a4 4 0 004 4h8a4 4 0 004-4V7h-2v12a2 2 0 01-2 2H8a2 2 0 01-2-2Zm4-11a1 1 0 00-1 1v8a1 1 0 102 0V9a1 1 0 00-1-1Zm4 0a1 1 0 00-1 1v8a1 1 0 002 0V9a1 1 0 00-1-1Z'
}
// = 常量定義:選擇器與樣式 =
const STYLE_IDS = { PLAY: 'yt-play-all-style', CUSTOM_BTN: 'yt-custom-btn-style' }
const ATTRS = { PLAY_PAGE_INIT: 'data-play-all-init', PLAY_ELEM_ADDED: 'data-play-all-added', BTN_PROCESSED: 'data-btn-added' }
const CLASSES = { PLAY_CONTAINER: 'ytpa-container', PLAY_BTN: 'ytpa-btn', BTN_CONTAINER: 'ytcb-container', BTN: 'yt-custom-btn' }
const SELECTORS = {
    PLAY_TARGET: 'ytd-rich-item-renderer, ytd-playlist-video-renderer, ytd-video-renderer',
    FLOAT_TARGET: 'yt-lockup-view-model',
    HISTORY_SHORTS: 'ytm-shorts-lockup-view-model-v2',
    MENU_BTN: '.yt-spec-button-shape-next.yt-spec-button-shape-next--icon-button',
    PLAYLIST_VIDEO: 'ytd-playlist-video-renderer',
    PLAYLIST_MENU_BTN: 'ytd-menu-renderer button, .yt-icon-button',
    MENU_ITEM_BTN: 'yt-list-item-view-model button.ytButtonOrAnchorHost.ytButtonOrAnchorButton.yt-list-item-view-model__button-or-anchor, button.ytButtonOrAnchorHost.ytButtonOrAnchorButton.ytListItemViewModelButtonOrAnchor'
}
// = 運行狀態 =
const state = {
    playActive: false,
    btnActive: false,
    playlistActive: false,
    processedCount: 0,
    playlistCount: 0,
    timer: null,
    playlistTimer: null
}
// = 工具函數 =
const safeOn = (el, evt, fn) => el?.addEventListener(evt, fn)
function matchPathRules(path, rules) {
    return rules.some(rule => {
        if (rule.type === 'startsWith') return path.startsWith(rule.value)
        if (rule.type === 'includes') return path.includes(rule.value)
        if (rule.type === 'regex') return new RegExp(rule.value).test(path)
        if (rule.type === 'exact') return path === rule.value || path === rule.value + '/'
        return false
    })
}
function isPlayTargetPage() {
    if (!CONFIG.PLAY_ALL_ENABLED) return false
    return matchPathRules(location.pathname, PLAY_CONFIG.ENABLED_PATHS)
}
function isBtnTargetPage() {
    if (!CONFIG.FLOAT_BUTTONS_ENABLED) return false
    return matchPathRules(location.pathname, BTN_CONFIG.FLOAT_PATHS)
}
function isPlaylistTargetPage() {
    if (!CONFIG.PLAYLIST_FLOAT_ENABLED) return false
    return matchPathRules(location.pathname + location.search, BTN_CONFIG.PLAYLIST_PATHS)
}
function createSVGIcon(path) {
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
    svg.setAttribute('viewBox', '0 0 24 24')
    svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
    svg.setAttribute('fill', 'currentColor')
    const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path')
    pathEl.setAttribute('d', path)
    svg.appendChild(pathEl)
    return svg
}
// = 播放全部功能 =
function playCleanup() {
    state.playActive = false
    document.querySelectorAll(`.${CLASSES.PLAY_BTN}`).forEach(btn => btn.remove())
    document.querySelectorAll(`.${CLASSES.PLAY_CONTAINER}`).forEach(container => container.remove())
}
function playAddStyles() {
    if (document.getElementById(STYLE_IDS.PLAY)) return
    const style = document.createElement('style')
    style.id = STYLE_IDS.PLAY
    style.textContent = `
        .${CLASSES.PLAY_BTN}:hover { background: var(--yt-spec-additive-background-hover, rgba(128,128,128,0.2)) !important; }
        .${CLASSES.PLAY_CONTAINER} { display: flex !important; flex-wrap: wrap !important; align-items: center !important; margin: 8px 0 !important; }
    `
    document.head.appendChild(style)
}
function playCreateBtn(text, href) {
    const a = document.createElement('a')
    a.className = CLASSES.PLAY_BTN
    a.href = href
    a.textContent = text
    a.role = 'button'
    a.style.cssText = `display:inline-flex;align-items:center;justify-content:center;padding:0 0.8em;height:32px;border-radius:8px;font-size:14px;font-weight:500;text-decoration:none;cursor:pointer;background:var(--yt-spec-additive-background);color:var(--yt-spec-text-primary);margin-left:${PLAY_CONFIG.BTN_SPACING};user-select:none;`
    safeOn(a, 'click', e => { if (location.host === 'm.youtube.com') { e.preventDefault(); location.href = a.href } })
    return a
}
async function playGetChannelId() {
    let id = ''
    const extract = html => { const m = /var ytInitialData.+?[ "']channelId[ "']:[ "']?(UC[\w-]+)/.exec(html); return m?.[1] || '' }
    try {
        const link = document.querySelector('#content ytd-rich-item-renderer a')?.href
        if (link) { const res = await fetch(link); const html = await res.text(); id = extract(html) }
    } catch (_) { }
    if (!id) { const m = location.href.match(/youtube\.com\/channel\/(UC[\w-]+)/); if (m) id = m[1] }
    return id.replace('UC', '')
}
function playInsertButtons(channelId) {
    const isVideos = location.pathname.endsWith('/videos'), isShorts = location.pathname.endsWith('/shorts')
    const lists = isVideos ? ['UULF', 'UULP'] : isShorts ? ['UUSH', 'UUPS'] : ['UULV', 'UUPV']
    const [allList, popList] = lists
    let container = document.querySelector('ytd-feed-filter-chip-bar-renderer iron-selector#chips, ytm-feed-filter-chip-bar-renderer .chip-bar-contents, chip-bar-view-model.ytChipBarViewModelHost')
    if (!container) {
        const grid = document.querySelector('ytd-rich-grid-renderer, ytm-rich-grid-renderer, div.ytChipBarViewModelChipWrapper')
        grid?.insertAdjacentHTML('afterbegin', `<div class="${CLASSES.PLAY_CONTAINER}"></div>`)
        container = grid?.querySelector(`.${CLASSES.PLAY_CONTAINER}`)
    }
    if (!container) return
    container.querySelectorAll(`.${CLASSES.PLAY_BTN}`).forEach(b => b.remove())
    const base = `/playlist?list=`
    const btns = [
        playCreateBtn(PLAY_CONFIG.BTN_TEXT.all, `${base}${allList}${channelId}&playnext=1&sort=1`),
        playCreateBtn(PLAY_CONFIG.BTN_TEXT.popular, `${base}${popList}${channelId}&playnext=1`),
        playCreateBtn(PLAY_CONFIG.BTN_TEXT.oldest, `${base}${allList}${channelId}&playnext=1&sort=2`)
    ]
    btns.forEach(b => container.appendChild(b))
}
async function playActivate() {
    if (state.playActive || !CONFIG.PLAY_ALL_ENABLED) return
    state.playActive = true
    playAddStyles()
    if (document.body.hasAttribute(ATTRS.PLAY_PAGE_INIT)) {
        onElementReady(SELECTORS.PLAY_TARGET, { once: false }, async (el) => {
            if (!el.hasAttribute(ATTRS.PLAY_ELEM_ADDED)) {
                const channelId = await playGetChannelId()
                if (channelId) playInsertButtons(channelId)
                el.setAttribute(ATTRS.PLAY_ELEM_ADDED, 'true')
            }
        })
        return
    }
    document.body.setAttribute(ATTRS.PLAY_PAGE_INIT, 'true')
    onElementReady(SELECTORS.PLAY_TARGET, { once: false }, async (el) => {
        if (!el.hasAttribute(ATTRS.PLAY_ELEM_ADDED)) {
            const channelId = await playGetChannelId()
            if (channelId) playInsertButtons(channelId)
            el.setAttribute(ATTRS.PLAY_ELEM_ADDED, 'true')
        }
    })
    const hasTarget = document.querySelector(SELECTORS.PLAY_TARGET)
    if (hasTarget) { const channelId = await playGetChannelId(); if (channelId) playInsertButtons(channelId) }
}
// = 統一按鈕樣式 (歷史 + 播放清單共用) =
function btnCleanup() {
    state.btnActive = false
    state.playlistActive = false
    if (state.timer) { clearTimeout(state.timer); state.timer = null }
    if (state.playlistTimer) { clearTimeout(state.playlistTimer); state.playlistTimer = null }
    document.querySelectorAll(`.${CLASSES.BTN_CONTAINER}`).forEach(c => c.remove())
    document.querySelectorAll(`[${ATTRS.BTN_PROCESSED}]`).forEach(el => el.removeAttribute(ATTRS.BTN_PROCESSED))
    state.processedCount = 0
    state.playlistCount = 0
}
function btnAddStyles() {
    if (document.getElementById(STYLE_IDS.CUSTOM_BTN)) return
    const style = document.createElement('style')
    style.id = STYLE_IDS.CUSTOM_BTN
    const t = BTN_CONFIG.TRANSITION_SPEED
    const s = BTN_CONFIG.SIZE
    const o = BTN_CONFIG.BG_OPACITY
    style.textContent = `
        .${CLASSES.BTN_CONTAINER} {
            position: absolute !important;
            top: ${BTN_CONFIG.TOP_MARGIN}px !important;
            display: flex !important;
            flex-direction: row !important;
            align-items: center !important;
            gap: ${BTN_CONFIG.SPACING}px !important;
            z-index: 10000 !important;
            pointer-events: none !important;
            opacity: 0 !important;
            visibility: hidden !important;
            transition: opacity ${t} ease, visibility ${t} ease !important;
        }
        ${SELECTORS.FLOAT_TARGET}:hover .${CLASSES.BTN_CONTAINER},
        ${SELECTORS.HISTORY_SHORTS}:hover .${CLASSES.BTN_CONTAINER},
        ${SELECTORS.PLAYLIST_VIDEO}:hover .${CLASSES.BTN_CONTAINER} {
            opacity: 1 !important;
            visibility: visible !important;
        }
        .${CLASSES.BTN} {
            width: ${s}px !important;
            height: ${s}px !important;
            border-radius: 50% !important;
            border: none !important;
            background: rgba(28, 28, 28, ${o}) !important;
            color: #fff !important;
            cursor: pointer !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            transition: background-color ${t}, transform ${t} !important;
            pointer-events: auto !important;
            padding: 0 !important;
            flex-shrink: 0 !important;
        }
        .${CLASSES.BTN}:hover {
            background: rgba(255, 255, 255, 0.25) !important;
            transform: scale(1.15) !important;
        }
        .${CLASSES.BTN} svg {
            width: 20px !important;
            height: 20px !important;
            fill: currentColor !important;
        }
        ${SELECTORS.FLOAT_TARGET},
        ${SELECTORS.HISTORY_SHORTS},
        ${SELECTORS.PLAYLIST_VIDEO} {
            overflow: visible !important;
            position: relative !important;
        }
    `
    document.head.appendChild(style)
}
// = 通用選單點擊函數 =
async function clickMenuByIcon(menuButton, targetIconPath, itemSelector, isHistoryStyle = false) {
    menuButton.click()
    const startTime = Date.now()
    return new Promise((resolve) => {
        const checkInterval = setInterval(() => {
            const popupContainer = document.querySelector('ytd-popup-container')
            if (!popupContainer) {
                if (Date.now() - startTime > BTN_CONFIG.MENU_WAIT_TIMEOUT) {
                    clearInterval(checkInterval)
                    resolve(false)
                    return
                }
                return
            }
            const menuItems = popupContainer.querySelectorAll(itemSelector)
            for (const item of menuItems) {
                const svg = item.querySelector('svg path')
                if (svg && svg.getAttribute('d') === targetIconPath) {
                    clearInterval(checkInterval)
                    const actionBtn = isHistoryStyle ? item.querySelector(SELECTORS.MENU_ITEM_BTN) : item
                    if (actionBtn) {
                        actionBtn.click()
                        setTimeout(() => { popupContainer.click() }, 50)
                        resolve(true)
                        return
                    }
                }
            }
            if (Date.now() - startTime > BTN_CONFIG.MENU_WAIT_TIMEOUT) {
                clearInterval(checkInterval)
                resolve(false)
            }
        }, BTN_CONFIG.MENU_CHECK_INTERVAL)
    })
}
function findMenuButton(row) {
    let btn = row.querySelector('.shortsLockupViewModelHostOutsideMetadataMenu button')
    if (btn) return btn
    btn = row.querySelector('ytd-menu-renderer button, .yt-lockup-metadata-view-model__menu-button button')
    if (btn) return btn
    return row.querySelector(SELECTORS.MENU_BTN)
}
// = 創建按鈕函數 (統一風格) =
function createBtn(icon, title, onClick) {
    const btn = document.createElement('button')
    btn.className = CLASSES.BTN
    btn.title = title
    btn.appendChild(createSVGIcon(icon))
    btn.addEventListener('click', onClick)
    return btn
}
// = 處理 Shorts (四鍵) =
async function processShorts(row) {
    const menuButton = findMenuButton(row)
    if (!menuButton) return
    const container = document.createElement('div')
    container.className = CLASSES.BTN_CONTAINER
    container.style.left = `${BTN_CONFIG.FLOAT_LEFT_MARGIN}px`
    const btnConfig = [
        { icon: ICON_PATHS.ADD_TO_PLAYLIST, title: 'Add to Queue', path: ICON_PATHS.ADD_TO_PLAYLIST },
        { icon: ICON_PATHS.WATCH_LATER, title: 'Watch Later', path: ICON_PATHS.WATCH_LATER },
        { icon: ICON_PATHS.SAVE_TO_PLAYLIST, title: 'Save to Playlist', path: ICON_PATHS.SAVE_TO_PLAYLIST },
        { icon: ICON_PATHS.REMOVE, title: 'Remove', path: ICON_PATHS.REMOVE }
    ]
    for (const cfg of btnConfig) {
        const btn = createBtn(cfg.icon, cfg.title, async (e) => {
            e.stopPropagation(); e.preventDefault(); e.stopImmediatePropagation()
            const success = await clickMenuByIcon(menuButton, cfg.path, 'yt-list-item-view-model', true)
            if (success && cfg.path === ICON_PATHS.REMOVE) {
                row.style.opacity = 0.3; row.style.pointerEvents = 'none'
                setTimeout(() => { if (row.isConnected) row.style.display = 'none' }, 300)
            }
        })
        container.appendChild(btn)
    }
    row.appendChild(container)
    row.setAttribute(ATTRS.BTN_PROCESSED, 'true')
    state.processedCount++
}
// = 處理歷史一般影片 (單一移除鍵) =
async function processHistoryRegular(row) {
    const menuButton = findMenuButton(row)
    if (!menuButton) return
    const container = document.createElement('div')
    container.className = CLASSES.BTN_CONTAINER
    container.style.left = `${BTN_CONFIG.FLOAT_LEFT_MARGIN}px`
    const btn = createBtn(ICON_PATHS.REMOVE, 'Remove from History', async (e) => {
        e.stopPropagation(); e.preventDefault(); e.stopImmediatePropagation()
        const success = await clickMenuByIcon(menuButton, ICON_PATHS.REMOVE, 'yt-list-item-view-model', true)
        if (success) {
            row.style.opacity = 0.3; row.style.pointerEvents = 'none'
            setTimeout(() => { if (row.isConnected) row.style.display = 'none' }, 300)
        }
    })
    container.appendChild(btn)
    row.appendChild(container)
    row.setAttribute(ATTRS.BTN_PROCESSED, 'true')
    state.processedCount++
}
// = 處理播放清單 (兩鍵) =
async function processPlaylist(row) {
    const menuButton = row.querySelector(SELECTORS.PLAYLIST_MENU_BTN)
    if (!menuButton) return
    const container = document.createElement('div')
    container.className = CLASSES.BTN_CONTAINER
    container.style.left = `${BTN_CONFIG.PLAYLIST_LEFT_MARGIN}px`
    const isWatchLater = location.search.includes('list=WL')
    const btnConfig = [
        { icon: ICON_PATHS.ADD_TO_PLAYLIST, path: ICON_PATHS.ADD_TO_PLAYLIST, title: 'Add to Queue', shouldHideRow: false },
        { icon: ICON_PATHS.REMOVE, path: ICON_PATHS.REMOVE, title: isWatchLater ? 'Remove from Watch Later' : 'Remove from Playlist', shouldHideRow: true }
    ]
    for (const cfg of btnConfig) {
        const btn = createBtn(cfg.icon, cfg.title, async (e) => {
            e.stopPropagation(); e.preventDefault(); e.stopImmediatePropagation()
            const success = await clickMenuByIcon(menuButton, cfg.path, 'ytd-menu-service-item-renderer, ytd-menu-navigation-item-renderer', false)
            if (success && cfg.shouldHideRow) {
                row.style.opacity = 0.3; row.style.pointerEvents = 'none'
                setTimeout(() => { if (row.isConnected) row.style.display = 'none' }, 300)
            }
        })
        container.appendChild(btn)
    }
    row.appendChild(container)
    row.setAttribute(ATTRS.BTN_PROCESSED, 'true')
    state.playlistCount++
}
// = 批量處理 =
function processBatch(elements, batchSize, delay, processor, isPlaylist = false) {
    const activeState = isPlaylist ? state.playlistActive : state.btnActive
    if (!activeState) return
    const batch = elements.slice(0, batchSize)
    batch.forEach(el => processor(el))
    const remaining = elements.slice(batchSize)
    if (remaining.length > 0) {
        const timerKey = isPlaylist ? 'playlistTimer' : 'timer'
        state[timerKey] = setTimeout(() => { processBatch(remaining, batchSize, delay, processor, isPlaylist) }, delay)
    }
}
// = 激活懸浮按鈕 =
function btnActivate() {
    if (state.btnActive || !CONFIG.FLOAT_BUTTONS_ENABLED) return
    state.btnActive = true
    state.processedCount = 0
    btnAddStyles()
    const isHistory = location.pathname.startsWith('/feed/history')
    if (!isHistory) return
    const shortsExisting = document.querySelectorAll(`${SELECTORS.HISTORY_SHORTS}:not([${ATTRS.BTN_PROCESSED}])`)
    if (shortsExisting.length > 0) processBatch(Array.from(shortsExisting), BTN_CONFIG.BATCH_SIZE, BTN_CONFIG.BATCH_DELAY, processShorts)
    onElementReady(SELECTORS.HISTORY_SHORTS, { once: false }, (el) => {
        if (state.processedCount >= BTN_CONFIG.BATCH_SIZE) {
            state.timer = setTimeout(() => processShorts(el), BTN_CONFIG.SUBSEQUENT_DELAY)
        } else { processShorts(el) }
    })
    const videoExisting = document.querySelectorAll(`${SELECTORS.FLOAT_TARGET}:not([${ATTRS.BTN_PROCESSED}])`)
    if (videoExisting.length > 0) processBatch(Array.from(videoExisting), BTN_CONFIG.BATCH_SIZE, BTN_CONFIG.BATCH_DELAY, processHistoryRegular)
    onElementReady(SELECTORS.FLOAT_TARGET, { once: false }, (el) => {
        if (state.processedCount >= BTN_CONFIG.BATCH_SIZE) {
            state.timer = setTimeout(() => processHistoryRegular(el), BTN_CONFIG.SUBSEQUENT_DELAY)
        } else { processHistoryRegular(el) }
    })
}
// = 激活播放清單按鈕 =
function playlistActivate() {
    if (state.playlistActive || !CONFIG.PLAYLIST_FLOAT_ENABLED) return
    state.playlistActive = true
    state.playlistCount = 0
    btnAddStyles()
    const existingElements = document.querySelectorAll(SELECTORS.PLAYLIST_VIDEO + ':not([' + ATTRS.BTN_PROCESSED + '])')
    const existingArray = Array.from(existingElements)
    if (existingArray.length > 0) {
        processBatch(existingArray, BTN_CONFIG.BATCH_SIZE, BTN_CONFIG.BATCH_DELAY, processPlaylist, true)
    }
    onElementReady(SELECTORS.PLAYLIST_VIDEO, { once: false }, (el) => {
        if (state.playlistCount >= BTN_CONFIG.BATCH_SIZE) {
            state.playlistTimer = setTimeout(() => { processPlaylist(el) }, BTN_CONFIG.SUBSEQUENT_DELAY)
        } else { processPlaylist(el) }
    })
}
// = 主控制函數 =
function cleanupAll() {
    playCleanup()
    btnCleanup()
}
function activateFeatures() {
    if (isPlayTargetPage()) playActivate()
    if (isBtnTargetPage()) btnActivate()
    if (isPlaylistTargetPage()) playlistActivate()
}
function handleNavigation() {
    cleanupAll()
    setTimeout(activateFeatures, CONFIG.NAVIGATION_DELAY)
}
function setupNavigationListener() {
    document.addEventListener('yt-navigate-finish', handleNavigation)
    window.addEventListener('popstate', handleNavigation)
    window.addEventListener('hashchange', handleNavigation)
}
// = 主入口 =
function init() {
    setupNavigationListener()
    handleNavigation()
}
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init)
} else {
    init()
}