Youtube Button Come Back

Add delete, add, or play buttons to the following pages: History, Playlists, and Channel pages. However, YouTube constantly makes pointless UI changes instead of focusing on actual work. On the History page, locating Shorts buttons is difficult; you must switch to the Shorts filter to find them.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         Youtube Button Come Back
// @namespace    http://tampermonkey.net/
// @version      5.3
// @description  Add delete, add, or play buttons to the following pages: History, Playlists, and Channel pages. However, YouTube constantly makes pointless UI changes instead of focusing on actual work. On the History page, locating Shorts buttons is difficult; you must switch to the Shorts filter to find them.
// @description:zh-TW 在以下頁面添加刪除或添加、播放按鈕:歷史記錄、播放清單、頻道頁面。但Youtube不做正經事,成天亂調純搞事。歷史頁面shorts較難抓取按鈕,需切換至shorts類型
// @author       AI
// @match        https://www.youtube.com/*
// @require      https://update.greasyfork.org/scripts/419640/1775486/onElementReady.js
// @grant        none
// @license      MIT
// ==/UserScript==

// = CONFIG: 全局設定 / Global Settings =
const CONFIG = {
    NAVIGATION_DELAY: 200,              // = 頁面導航後延遲執行 (ms) / Delay after navigation (ms)
    PLAY_ALL_ENABLED: true,             // = 啟用「播放全部」功能 / Enable Play All feature
    FLOAT_BUTTONS_ENABLED: true,        // = 啟用歷史記錄懸浮按鈕 / Enable history float buttons
    PLAYLIST_FLOAT_ENABLED: true        // = 啟用播放清單懸浮按鈕 / Enable playlist float buttons
}

// = CONFIG: 播放全部功能設定 / Play All Feature Settings =
const PLAY_CONFIG = {
    ENABLED_PATHS: [{ type: 'regex', value: '^/@[^/]+/(videos|shorts|streams)$' }],  // = 啟用的頁面路徑規則 / Enabled page path patterns
    BTN_TEXT: { all: 'Play all', popular: 'Play popular', oldest: 'Play oldest' },    // = 按鈕顯示文字 / Button labels
    BTN_SPACING: '0.5em'                      // = 按鈕間距 / Button spacing
}

// = CONFIG: 懸浮按鈕通用設定 / Float Button Common Settings =
const BTN_CONFIG = {
    FLOAT_PATHS: [                              // = 顯示歷史懸浮按鈕的頁面路徑 / Paths for history float buttons
        { type: 'exact', value: '/' },
        { type: 'startsWith', value: '/feed/subscriptions' },
        { type: 'startsWith', value: '/feed/history' }
    ],
    PLAYLIST_PATHS: [{ type: 'startsWith', value: '/playlist?list=' }],  // = 播放清單頁面路徑 / Playlist page path
    SIZE: 36,                                   // = 按鈕尺寸 (px) / Button size in pixels
    SPACING: 4,                                 // = 按鈕間距 (px) / Button spacing in pixels
    BG_OPACITY: 0.9,                            // = 背景透明度 / Background opacity
    FLOAT_LEFT_MARGIN: 170,                     // = 歷史頁按鈕水平位移 (px) / Horizontal offset for history buttons
    FLOAT_TOP_MARGIN: 5,                        // = 歷史頁按鈕垂直位移 (px) / Vertical offset for history buttons
    PLAYLIST_LEFT_MARGIN: 160,                  // = 播放清單按鈕左邊距 (px) / Left margin for playlist buttons
    MENU_WAIT_TIMEOUT: 3000,                    // = 選單載入等待超時 (ms) - 已延長以適應 Shorts / Menu load timeout in ms - extended for Shorts
    MENU_CHECK_INTERVAL: 80,                    // = 選單檢查間隔 (ms) - 已放緩以減少負擔 / Menu check interval in ms - slowed to reduce load
    MENU_RETRY_DELAY: 150,                      // = 選單按鈕查找重試延遲 (ms) - 新增 / Menu button retry delay in ms - new
    MENU_MAX_RETRIES: 3,                        // = 選單按鈕查找最大重試次數 - 新增 / Max retries for menu button lookup - new
    TRANSITION_SPEED: '0.15s',                  // = CSS 過渡動畫速度 / CSS transition duration
    BATCH_SIZE: 15,                             // = 批量處理元素數量 / Batch processing size
    BATCH_DELAY: 20,                            // = 批次間延遲 (ms) / Delay between batches in ms
    SUBSEQUENT_DELAY: 100                       // = 後續單元素處理延遲 (ms) / Delay for subsequent single element processing
}

// = 常量:SVG 圖示路徑定義 / Constants: SVG Icon Path Definitions =
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',  // = 稍後觀看圖示 / Watch Later icon
    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',  // = 加入播放佇列圖示 / Add to Queue icon
    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',  // = 儲存至播放清單圖示 / Save to Playlist icon
    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'  // = 移除/刪除圖示 / Remove icon
}

// = 常量:樣式 ID 與自訂屬性定義 / Constants: Style IDs and Custom Attribute Definitions =
const STYLE_IDS = { PLAY: 'yt-play-all-style', CUSTOM_BTN: 'yt-custom-btn-style' }  // = 動態注入的 <style> 標籤 ID / IDs for dynamically injected <style> tags
const ATTRS = { PLAY_PAGE_INIT: 'data-play-all-init', PLAY_ELEM_ADDED: 'data-play-all-added', BTN_PROCESSED: 'data-btn-added' }  // = 用於標記元素處理狀態的 data 屬性 / Data attributes for tracking element processing state
const CLASSES = { PLAY_CONTAINER: 'ytpa-container', PLAY_BTN: 'ytpa-btn', BTN_CONTAINER: 'ytcb-container', BTN: 'yt-custom-btn' }  // = 腳本生成的 CSS 類別名稱 / CSS class names generated by the script

// = 常量:DOM 選擇器定義 (MENU_BTN 已修正) / Constants: DOM Selector Definitions (MENU_BTN fixed) =
const SELECTORS = {
    PLAY_TARGET: 'ytd-rich-item-renderer, ytd-playlist-video-renderer, ytd-video-renderer',  // = 播放全部功能監聽的目標元素 / Target elements for Play All feature
    FLOAT_TARGET: 'yt-lockup-view-model',                      // = 歷史記錄一般影片卡片選擇器 / Selector for history regular video cards
    HISTORY_SHORTS: 'ytd-video-renderer[lockup="true"]',       // = 歷史記錄 Shorts 卡片選擇器 (需帶 lockup 屬性) / Selector for history Shorts cards (requires lockup attribute)
    MENU_BTN: 'ytd-menu-renderer yt-icon-button#button.dropdown-trigger>button.style-scope.yt-icon-button,ytd-menu-renderer button,div.ytLockupMetadataViewModelMenuButton button',  // = 三點選單按鈕選擇器 (已修正:優先匹配 Shorts 內層真實 button 元素,避免觸發 yt-icon-button 自帶導航) / Three-dot menu button selector (fixed: prioritize inner real button for Shorts to avoid yt-icon-button navigation)
    PLAYLIST_VIDEO: 'ytd-playlist-video-renderer',             // = 播放清單內影片項目選擇器 / Selector for playlist video items
    PLAYLIST_MENU_BTN: 'ytd-menu-renderer button, .yt-icon-button',  // = 播放清單內選單按鈕選擇器 / Menu button selector within playlists
    MENU_ITEM_BTN: 'ytd-menu-service-item-renderer tp-yt-paper-item,ytd-menu-navigation-item-renderer tp-yt-paper-item,yt-list-item-view-model button.ytButtonOrAnchorHost.ytButtonOrAnchorButton.yt-list-item-view-model__button-or-anchor, button.ytButtonOrAnchorHost.ytButtonOrAnchorButton.ytListItemViewModelButtonOrAnchor'  // = 選單內可點擊項目按鈕選擇器 / Clickable menu item button selector
}

// = 運行狀態管理物件 / Runtime State Management Object =
const state = {
    playActive: false,          // = 播放全部功能是否已激活 / Play All feature activation status
    btnActive: false,           // = 歷史懸浮按鈕是否已激活 / History float buttons activation status
    playlistActive: false,      // = 播放清單懸浮按鈕是否已激活 / Playlist float buttons activation status
    processedCount: 0,          // = 已處理的歷史項目計數 / Count of processed history items
    playlistCount: 0,           // = 已處理的播放清單項目計數 / Count of processed playlist items
    timer: null,                // = 歷史按鈕批量處理計時器 / Timer for history button batch processing
    playlistTimer: null         // = 播放清單按鈕批量處理計時器 / Timer for playlist button batch processing
}

// = 工具函數:安全綁定事件監聽 / Utility: Safe Event Listener Binding =
const safeOn = (el, evt, fn) => el?.addEventListener(evt, fn)  // = 若元素存在則添加事件監聽器 / Add event listener if element exists

// = 工具函數:路徑規則匹配 / Utility: Path Pattern Matching =
function matchPathRules(path, rules) {  // = 檢查當前路徑是否符合任一規則 / Check if current path matches any rule
    return rules.some(rule => {
        if (rule.type === 'startsWith') return path.startsWith(rule.value)  // = 前綴匹配 / Prefix match
        if (rule.type === 'includes') return path.includes(rule.value)      // = 包含匹配 / Contains match
        if (rule.type === 'regex') return new RegExp(rule.value).test(path) // = 正則表達式匹配 / Regex match
        if (rule.type === 'exact') return path === rule.value || path === rule.value + '/'  // = 精確匹配 / Exact match
        return false
    })
}

// = 頁面判斷:是否為播放全部目標頁面 / Page Check: Is Play All Target Page =
function isPlayTargetPage() {  // = 根據 CONFIG 與 PLAY_CONFIG 判斷是否啟用播放全部功能 / Determine if Play All feature should be enabled based on config
    if (!CONFIG.PLAY_ALL_ENABLED) return false
    return matchPathRules(location.pathname, PLAY_CONFIG.ENABLED_PATHS)
}

// = 頁面判斷:是否為歷史懸浮按鈕目標頁面 / Page Check: Is History Float Button Target Page =
function isBtnTargetPage() {  // = 根據 CONFIG 與 BTN_CONFIG 判斷是否啟用歷史懸浮按鈕 / Determine if history float buttons should be enabled
    if (!CONFIG.FLOAT_BUTTONS_ENABLED) return false
    return matchPathRules(location.pathname, BTN_CONFIG.FLOAT_PATHS)
}

// = 頁面判斷:是否為播放清單懸浮按鈕目標頁面 / Page Check: Is Playlist Float Button Target Page =
function isPlaylistTargetPage() {  // = 根據 CONFIG 與 BTN_CONFIG 判斷是否啟用播放清單懸浮按鈕 / Determine if playlist float buttons should be enabled
    if (!CONFIG.PLAYLIST_FLOAT_ENABLED) return false
    return matchPathRules(location.pathname + location.search, BTN_CONFIG.PLAYLIST_PATHS)
}

// = 工具函數:創建 SVG 圖示元素 / Utility: Create SVG Icon Element =
function createSVGIcon(path) {  // = 根據 path 字符串生成 SVG <path> 元素 / Generate SVG <path> element from path string
    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
}

// = 工具函數:延遲執行 / Utility: Delay Execution =
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms))  // = 返回 Promise 的延遲函數 / Delay function returning Promise

// ============ 播放全部功能模組 / Play All Feature Module ============

// = 清理:移除播放全部相關元素與狀態 / Cleanup: Remove Play All Elements and Reset State =
function playCleanup() {  // = 重置 playActive 狀態並移除所有動態生成的按鈕與容器 / Reset playActive state and remove all dynamically generated buttons and containers
    state.playActive = false
    document.querySelectorAll(`.${CLASSES.PLAY_BTN}`).forEach(btn => btn.remove())
    document.querySelectorAll(`.${CLASSES.PLAY_CONTAINER}`).forEach(container => container.remove())
}

// = 樣式注入:播放全部按鈕樣式 / Style Injection: Play All Button Styles =
function playAddStyles() {  // = 動態插入 <style> 標籤,定義按鈕容器與懸浮效果 / Dynamically inject <style> tag defining button container and hover effects
    if (document.getElementById(STYLE_IDS.PLAY)) return
    const style = document.createElement('style')
    style.id = STYLE_IDS.PLAY
    style.textContent = `
        .${CLASSES.PLAY_BTN} { background: #000000 !important; color: #ffffff !important; transition: background-color 0.15s ease, color 0.15s ease !important; }
        .${CLASSES.PLAY_BTN}:hover { background: #ffffff !important; color: #000000 !important; }
        .${CLASSES.PLAY_CONTAINER} { display: flex !important; flex-wrap: wrap !important; align-items: center !important; margin: 8px 0 !important; }
    `
    document.head.appendChild(style)
}

// = 創建:播放全部功能按鈕元素 / Create: Play All Feature Button Element =
function playCreateBtn(text, href) {  // = 生成帶有樣式的 <a> 按鈕,支援點擊跳轉與行動版相容 / Generate styled <a> button with click handling and mobile compatibility
    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;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
}

// = 非同步:獲取頻道 ID / Async: Fetch Channel ID =
async function playGetChannelId() {  // = 透過頁面內容或 URL 提取 channelId,用於生成播放清單連結 / Extract channelId from page content or URL for generating playlist links
    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', '')
}

// = 插入:播放全部按鈕至頁面 / Insert: Play All Buttons into Page =
function playInsertButtons(channelId) {  // = 根據當前頁面類型 (videos/shorts/streams) 插入三個播放按鈕 / Insert three play buttons based on current page type (videos/shorts/streams)
    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))
}

// = 激活:播放全部功能主流程 / Activate: Play All Feature Main Flow =
async function playActivate() {  // = 初始化樣式、監聽目標元素、提取頻道 ID 並插入按鈕 / Initialize styles, observe target elements, extract channel ID, and insert buttons
    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) }
}

// ============ 懸浮按鈕通用模組 / Float Button Common Module ============

// = 清理:移除所有懸浮按鈕相關元素與狀態 / Cleanup: Remove All Float Button Elements and Reset State =
function btnCleanup() {  // = 重置 btnActive/playlistActive 狀態、清除計時器、移除按鈕容器與處理標記 / Reset activation states, clear timers, remove button containers and processed markers
    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
}

// = 樣式注入:懸浮按鈕通用樣式 / Style Injection: Float Button Common Styles =
function btnAddStyles() {  // = 動態插入 <style> 標籤,定義按鈕容器定位、懸浮顯示邏輯與按鈕樣式 / Dynamically inject <style> tag defining container positioning, hover display logic, and button styles
    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.FLOAT_TOP_MARGIN}px !important;
            left: ${BTN_CONFIG.FLOAT_LEFT_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)
}

// = 核心邏輯:點擊選單內指定圖示項目 / Core Logic: Click Menu Item by Icon Path =
async function clickMenuByIcon(menuButton, targetIconPath, itemSelector, isHistoryStyle = false) {  // = 打開選單後輪詢查找匹配 icon path 的項目並點擊 / Open menu then poll for item matching icon path and click it
    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('ytd-menu-service-item-renderer, ytd-menu-navigation-item-renderer, yt-list-item-view-model')
            for (const item of menuItems) {
                const svg = item.querySelector('svg path')
                if (svg && svg.getAttribute('d') === targetIconPath) {
                    clearInterval(checkInterval)
                    let actionBtn = null
                    if (item.tagName.toLowerCase().includes('menu-service-item') || item.tagName.toLowerCase().includes('menu-navigation-item')) {
                        actionBtn = item.querySelector('tp-yt-paper-item')
                    } else {
                        actionBtn = 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)
    })
}

// = 工具函數:查找三點選單按鈕 (已修正 Shorts 導航問題) / Utility: Find Three-Dot Menu Button (Shorts navigation fixed) =
async function findMenuButton(row, isShorts = false) {  // = 嘗試多種選擇器定位影片卡片內的選單觸發按鈕,已修正 Shorts 選擇器避免點擊到帶導航的 yt-icon-button / Try multiple selectors to locate menu trigger button, fixed Shorts selector to avoid clicking navigation-triggering yt-icon-button
    if (isShorts) await delay(BTN_CONFIG.MENU_RETRY_DELAY)

    for (let attempt = 0; attempt <= BTN_CONFIG.MENU_MAX_RETRIES; attempt++) {
        // = 優先:Shorts 專用 - 直接選擇內層真實 button 元素,避開外層會觸發導航的 yt-icon-button / Priority: Shorts specific - select inner real button to avoid outer yt-icon-button that triggers navigation =
        let btn = row.querySelector('ytd-menu-renderer yt-icon-button#button.dropdown-trigger>button.style-scope.yt-icon-button')
        if (btn) return btn
        // = 次選:用戶提供的通用選擇器 / Fallback: User-provided generic selector =
        btn = row.querySelector(SELECTORS.MENU_BTN)
        if (btn) return btn
        // = 備援:其他常見選單按鈕結構 / Backup: Other common menu button structures =
        btn = row.querySelector('ytd-menu-renderer button, .yt-lockup-metadata-view-model__menu-button button, .yt-spec-button-shape-next--icon-button')
        if (btn) return btn
        if (attempt < BTN_CONFIG.MENU_MAX_RETRIES) {
            await delay(BTN_CONFIG.MENU_RETRY_DELAY)
        }
    }
    return null
}

// = 創建:統一風格的懸浮按鈕 / Create: Uniform Style Float Button =
function createBtn(icon, title, onClick) {  // = 生成帶有 SVG 圖示、title 提示與點擊事件的圓形按鈕 / Generate circular button with SVG icon, title tooltip, and click handler
    const btn = document.createElement('button')
    btn.className = CLASSES.BTN
    btn.title = title
    btn.appendChild(createSVGIcon(icon))
    btn.addEventListener('click', onClick)
    return btn
}

// ============ 歷史記錄處理模組 (一般影片 + Shorts 共用單一刪除鍵) / History Processing Module (Regular + Shorts - Single Remove Button) ============

// = 處理:歷史影片項目 (一般/Shorts 共用) / Process: History Video Item (Regular/Shorts Shared) =
async function processHistoryItem(row) {  // = 為歷史頁面中的影片卡片添加「移除」懸浮按鈕,適用於一般影片與 Shorts / Add "Remove" float button to history video cards, works for both regular videos and Shorts
    const isShorts = row.matches(SELECTORS.HISTORY_SHORTS)
    const menuButton = await findMenuButton(row, isShorts)
    if (!menuButton) return
    const container = document.createElement('div')
    container.className = CLASSES.BTN_CONTAINER
    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++
}

// ============ 播放清單處理模組 / Playlist Processing Module ============

// = 處理:播放清單內影片 (兩按鈕) / Process: Playlist Video Item (2 Buttons) =
async function processPlaylist(row) {  // = 為播放清單影片添加「加入佇列」與「移除」兩個懸浮按鈕 / Add "Add to Queue" and "Remove" float buttons to playlist video items
    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++
}

// ============ 批量處理工具 / Batch Processing Utility ============

// = 批量處理元素 / Process Elements in Batches =
function processBatch(elements, batchSize, delay, processor, isPlaylist = false) {  // = 分批次處理元素以避免卡頓,支援歷史與播放清單兩種模式 / Process elements in batches to avoid lag, supporting both history and playlist modes
    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)
    }
}

// ============ 功能激活入口 / Feature Activation Entry Points ============

// = 激活:歷史記錄懸浮按鈕 (一般影片 + Shorts) / Activate: History Float Buttons (Regular + Shorts) =
function btnActivate() {  // = 初始化樣式並監聽歷史頁面中的一般影片與 Shorts 卡片,兩者皆添加單一移除按鈕 / Initialize styles and observe both regular videos and Shorts on history page, add single remove button to both
    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 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, processHistoryItem)
    onElementReady(SELECTORS.FLOAT_TARGET, { once: false }, (el) => {
        if (state.processedCount >= BTN_CONFIG.BATCH_SIZE) {
            state.timer = setTimeout(() => processHistoryItem(el), BTN_CONFIG.SUBSEQUENT_DELAY)
        } else { processHistoryItem(el) }
    })
    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, processHistoryItem)
    onElementReady(SELECTORS.HISTORY_SHORTS, { once: false }, (el) => {
        if (state.processedCount >= BTN_CONFIG.BATCH_SIZE) {
            state.timer = setTimeout(() => processHistoryItem(el), BTN_CONFIG.SUBSEQUENT_DELAY)
        } else { processHistoryItem(el) }
    })
}

// = 激活:播放清單懸浮按鈕 / Activate: Playlist Float Buttons =
function playlistActivate() {  // = 初始化樣式並監聽播放清單頁面中的影片項目 / Initialize styles and observe video items on playlist page
    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) }
    })
}

// ============ 主控制流程 / Main Control Flow ============

// = 清理所有功能 / Cleanup All Features =
function cleanupAll() {  // = 調用各模組的 cleanup 函數,重置狀態並移除動態元素 / Call cleanup functions of all modules to reset state and remove dynamic elements
    playCleanup()
    btnCleanup()
}

// = 激活當前頁面適用功能 / Activate Features for Current Page =
function activateFeatures() {  // = 根據當前 URL 判斷並啟動對應的功能模組 / Determine and activate corresponding feature modules based on current URL
    if (isPlayTargetPage()) playActivate()
    if (isBtnTargetPage()) btnActivate()
    if (isPlaylistTargetPage()) playlistActivate()
}

// = 處理頁面導航變更 / Handle Page Navigation Change =
function handleNavigation() {  // = 清理舊狀態後延遲重新激活功能,適應 SPA 導航 / Clean old state then reactivate features with delay for SPA navigation
    cleanupAll()
    setTimeout(activateFeatures, CONFIG.NAVIGATION_DELAY)
}

// = 設置導航事件監聽 / Setup Navigation Event Listeners =
function setupNavigationListener() {  // = 監聽 YouTube 內部導航事件與瀏覽器歷史變化 / Listen to YouTube internal navigation events and browser history changes
    document.addEventListener('yt-navigate-finish', handleNavigation)
    window.addEventListener('popstate', handleNavigation)
    window.addEventListener('hashchange', handleNavigation)
}

// = 主入口函數 / Main Entry Function =
function init() {  // = 初始化事件監聽並執行首次功能激活 / Initialize event listeners and perform initial feature activation
    setupNavigationListener()
    handleNavigation()
}

// = 腳本啟動 / Script Startup =
if (document.readyState === 'loading') {  // = 若頁面仍在載入則等待 DOMContentLoaded,否則立即執行 / Wait for DOMContentLoaded if page still loading, otherwise execute immediately
    document.addEventListener('DOMContentLoaded', init)
} else {
    init()
}