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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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.greatest.deepsurf.us/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()
}