Script Finder+

记录位置、自动翻译、点击外部关闭、新用户引导、全数据展示。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name            Script Finder+
// @name:zh-CN      Script Finder 油猴脚本查找
// @description:zh-CN 加载状态移到标题栏,避免列表跳动;增强英文脚本自动翻译的接口兼容性。桌面端面板固定宽度,窗口较小时自适应。修复桌面端靠右显示逻辑。手机竖版“查找”不遮挡。渲染优先、异步翻译、支持拖动、位置记录。
// @namespace       https://github.com/HHXXYY123/script-finder-plus
// @version         2026.9.1.0518
// @author          HHXXYY123
// @match           *://*/*
// @connect         greasyfork.org
// @connect         translate.googleapis.com
// @connect         clients5.google.com
// @grant           GM_xmlhttpRequest
// @grant           GM_addStyle
// @license         MIT
// @description 记录位置、自动翻译、点击外部关闭、新用户引导、全数据展示。
// ==/UserScript==

(function () {
    const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
    const userLang = (navigator.languages && navigator.languages[0]) || navigator.language || 'en'
    const isChinese = userLang.startsWith('zh')

    const getT = (key) => {
        const dict = isChinese ? {
            Author: '作者', Installs: '总安装', Daily: '日安装', Created: '创建', Updated: '更新', Loading: '加载中...', LoadMore: '加载更多', AllLoaded: '到底啦',
            Search: '搜索脚本...', Scripts: '脚本查找', MiniBtn: '查找', Timeout: '超时', Install: '安装', OpenGF: '打开 GreasyFork'
        } : {
            Author: 'Author', Installs: 'Total', Daily: 'Daily', Created: 'Created', Updated: 'Updated', Loading: 'Loading...', LoadMore: 'More', AllLoaded: 'End',
            Search: 'Search...', Scripts: 'Scripts', MiniBtn: 'Find', Timeout: 'Timeout', Install: 'Install', OpenGF: 'Open GreasyFork'
        }
        return dict[key] || key
    }

    function setLoadingStatus(text = '', visible = true) {
        const status = document.querySelector('.sf-loading-status')
        if (!status) return
        status.textContent = text || ''
        status.classList.toggle('is-visible', visible && Boolean(text))
        status.setAttribute('aria-busy', visible ? 'true' : 'false')
    }

    const domain = window.location.hostname.split('.').slice(-2).join('.')
    let neverLoaded = true, collapsed = true, loadedPages = 0, hideTimer = null, isDragging = false
    let isLoadingPage = false, pendingSearchTerm = '', searchLoadTimer = null, autoPreloadTimer = null, totalLoadedItems = 0

    const translationCache = new Map()
    const translationQueue = []
    const translationPending = new Map()
    let translationRunning = false
    let lastTranslationStart = 0
    const translationInterval = 650

    function applyTranslation(element, translated) {
        if (!element || !translated) return
        element.style.display = 'block'
        element.innerText = `🏮 ${translated}`
    }

    function parseGoogleSingle(body) {
        const data = JSON.parse(body)
        return Array.isArray(data?.[0])
            ? data[0].map(part => part?.[0] || '').join('').trim()
            : ''
    }

    function parseGoogleCompact(body) {
        const data = JSON.parse(body)
        return Array.isArray(data?.[0]) ? String(data[0][0] || '').trim() : ''
    }

    function requestTextByGM(url) {
        return new Promise((resolve, reject) => {
            let settled = false
            const finish = (handler, value) => {
                if (settled) return
                settled = true
                handler(value)
            }
            try {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url,
                    timeout: 8000,
                    onload: (res) => {
                        if (res.status >= 200 && res.status < 300) finish(resolve, res.responseText)
                        else finish(reject, new Error(`HTTP ${res.status}`))
                    },
                    onerror: () => finish(reject, new Error('request failed')),
                    ontimeout: () => finish(reject, new Error('request timeout'))
                })
            } catch (e) {
                finish(reject, e)
            }
        })
    }

    function requestTextByFetch(url) {
        return fetch(url, { credentials: 'omit' }).then(res => {
            if (!res.ok) throw new Error(`HTTP ${res.status}`)
            return res.text()
        })
    }

    function requestTranslation(text, done) {
        const encoded = encodeURIComponent(text)
        const endpoints = [
            {
                url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh-CN&dt=t&q=${encoded}`,
                parse: parseGoogleSingle
            },
            {
                url: `https://clients5.google.com/translate_a/t?client=dict-chrome-ex&sl=auto&tl=zh-CN&q=${encoded}`,
                parse: parseGoogleCompact
            }
        ]

        const tryRequest = (transportIndex, endpointIndex) => {
            if (endpointIndex >= endpoints.length) {
                if (transportIndex === 0) return tryRequest(1, 0)
                done('')
                return
            }
            const endpoint = endpoints[endpointIndex]
            const request = transportIndex === 0 ? requestTextByGM(endpoint.url) : requestTextByFetch(endpoint.url)
            request.then(body => {
                try {
                    const translated = endpoint.parse(body)
                    if (translated) {
                        done(translated)
                        return
                    }
                } catch (e) {}
                tryRequest(transportIndex, endpointIndex + 1)
            }).catch(() => tryRequest(transportIndex, endpointIndex + 1))
        }
        tryRequest(0, 0)
    }

    function processTranslationQueue() {
        if (translationRunning || translationQueue.length === 0) return
        const text = translationQueue.shift()
        const elements = translationPending.get(text) || []
        const cached = translationCache.get(text)
        if (cached) {
            elements.forEach(element => applyTranslation(element, cached))
            translationPending.delete(text)
            processTranslationQueue()
            return
        }

        const wait = Math.max(0, translationInterval - (Date.now() - lastTranslationStart))
        setTimeout(() => {
            translationRunning = true
            lastTranslationStart = Date.now()
            requestTranslation(text, translated => {
                if (translated) {
                    translationCache.set(text, translated)
                    elements.forEach(element => applyTranslation(element, translated))
                }
                translationPending.delete(text)
                translationRunning = false
                processTranslationQueue()
            })
        }, wait)
    }

    function queueTranslation(text, element, delay) {
        const normalized = (text || '').trim()
        if (!normalized || /[\u4e00-\u9fa5]/.test(normalized) || !element) return
        setTimeout(() => {
            const cached = translationCache.get(normalized)
            if (cached) {
                applyTranslation(element, cached)
                return
            }
            const pending = translationPending.get(normalized)
            if (pending) {
                pending.push(element)
                return
            }
            translationPending.set(normalized, [element])
            translationQueue.push(normalized)
            processTranslationQueue()
        }, delay)
    }

    function getScriptsInfo(domain, page = 1, options = {}) {
        const btn = document.querySelector('.sf-load-more')
        const { silent = false, onComplete = null } = options
        if (isLoadingPage) return
        isLoadingPage = true
        if (!silent) setLoadingStatus(`${getT('Loading')} (${page})`, true)
        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://greasyfork.org/scripts/by-site/${domain}?filter_locale=0&sort=updated&page=${page}`,
            timeout: 10000,
            onload: (res) => {
                isLoadingPage = false
                if (!silent) setLoadingStatus('', false)
                const doc = new DOMParser().parseFromString(res.responseText, 'text/html')
                const scripts = doc.querySelector('#browse-script-list')?.querySelectorAll('[data-script-id]')
                if (page === 1) {
                    const headerText = doc.querySelector('body')?.innerText || '';
                    const match = headerText.match(/(?:共|of)\s*([\d,]+)\s*(?:个脚本|scripts|条)/i) || headerText.match(/(\d+)\s*(?:个脚本|scripts found)/i);
                    if (match) {
                        const count = match[1].replace(/,/g, '');
                        document.querySelector('.sf-total-count').innerText = ` (共 ${count} 个,已加载 ${totalLoadedItems || scripts.length} 个)`;
                    } else {
                        // 备用方案:如果没匹配到具体总数文本,获取当前页的脚本数量
                        document.querySelector('.sf-total-count').innerText = ` (已加载 ${totalLoadedItems || (scripts ? scripts.length : 0)} 个)`;
                    }
                }
                if (!scripts || scripts.length === 0) {
                    if (page === 1 && !silent) setLoadingStatus('该域暂无可用脚本', true)
                    if (page > 1) { loadedPages = 'max'; btn.textContent = getT('AllLoaded'); btn.disabled = true }
                    if (typeof onComplete === 'function') onComplete([])
                    return
                }
                totalLoadedItems += scripts.length
                const totalCountNode = document.querySelector('.sf-total-count')
                if (totalCountNode && !totalCountNode.innerText.includes('共 ')) totalCountNode.innerText = ` (已加载 ${totalLoadedItems} 个)`
                let staggerDelay = 100
                scripts.forEach(s => {
                    const typeBadge = s.querySelector('.script-type')?.textContent || '';
                    const isLibrary = typeBadge.includes('Library') || typeBadge.includes('库');
                    const extension = isLibrary ? '.js' : '.user.js';
                    const nameEncoded = encodeURIComponent(s.getAttribute('data-script-name'));
                    const installUrl = `https://update.greasyfork.org/scripts/${s.getAttribute('data-script-id')}/${nameEncoded}${extension}`;

                    const info = {
                        id: s.getAttribute('data-script-id'),
                        name: s.getAttribute('data-script-name'),
                        author: s.querySelector('dd.script-list-author')?.textContent || 'Unknown',
                        desc: s.querySelector('.script-description')?.textContent || '',
                        version: s.getAttribute('data-script-version'),
                        url: 'https://greasyfork.org/scripts/' + s.getAttribute('data-script-id'),
                        installUrl: installUrl,
                        installs: s.getAttribute('data-script-total-installs') || '0',
                        daily: s.getAttribute('data-script-daily-installs') || '0',
                        created: s.getAttribute('data-script-created-date') || '',
                        updated: s.getAttribute('data-script-updated-date') || '',
                        rating: s.getAttribute('data-script-rating-score') || '0',
                        typeBadge: typeBadge
                    }
                    const li = appendItem(info)
                    queueTranslation(info.name, li.querySelector('.sf-trans-name'), staggerDelay)
                    queueTranslation(info.desc, li.querySelector('.sf-trans-desc'), staggerDelay + 50)
                    staggerDelay += 150
                })
                const pageCount = scripts.length
                if (pageCount < 100) {
                    loadedPages = 'max'; btn.textContent = getT('AllLoaded'); btn.disabled = true
                } else {
                    loadedPages = page; btn.style.display = 'block'; btn.textContent = getT('LoadMore'); btn.disabled = false
                }
                if (typeof onComplete === 'function') onComplete(Array.from(scripts))
            },
            ontimeout: () => {
                isLoadingPage = false
                setLoadingStatus('', false)
                if (!silent) setLoadingStatus(getT('Timeout'), true)
                if (typeof onComplete === 'function') onComplete([])
            }
        })
    }

    function updateSearchResults(term) {
        const normalized = (term || '').trim().toLowerCase()
        const items = Array.from(document.querySelectorAll('.sf-info-item'))
        let visibleCount = 0
        items.forEach(li => {
            const matched = !normalized || li.innerText.toLowerCase().includes(normalized)
            li.style.display = matched ? 'block' : 'none'
            if (matched) visibleCount++
        })
        return visibleCount
    }

    function maybeLoadMoreForSearch() {
        const term = pendingSearchTerm
        if (!term || isLoadingPage || loadedPages === 'max') return
        const visibleCount = updateSearchResults(term)
        if (visibleCount > 0) return
        const nextPage = typeof loadedPages === 'number' ? loadedPages + 1 : 1
        setLoadingStatus(`${getT('Loading')} (${nextPage})`, true)
        getScriptsInfo(domain, nextPage, {
            silent: true,
            onComplete: () => {
                const matchedCount = updateSearchResults(term)
                if (matchedCount === 0 && loadedPages !== 'max') {
                    searchLoadTimer = setTimeout(maybeLoadMoreForSearch, 150)
                } else {
                    setLoadingStatus('', false)
                }
            }
        })
    }

    function scheduleAutoPreload(delay = 180) {
        if (autoPreloadTimer) clearTimeout(autoPreloadTimer)
        if (collapsed || isLoadingPage || loadedPages === 'max' || pendingSearchTerm) return
        autoPreloadTimer = setTimeout(() => loadNextPage('auto'), delay)
    }

    function loadNextPage(reason = 'manual') {
        if (isLoadingPage || loadedPages === 'max') return
        const nextPage = typeof loadedPages === 'number' ? loadedPages + 1 : 1
        setLoadingStatus(`${getT('Loading')} (${nextPage})`, true)
        getScriptsInfo(domain, nextPage, {
            silent: true,
            onComplete: () => {
                if (pendingSearchTerm) {
                    const matchedCount = updateSearchResults(pendingSearchTerm)
                    if (reason === 'search' && matchedCount === 0 && loadedPages !== 'max') {
                        searchLoadTimer = setTimeout(() => loadNextPage('search'), 120)
                        return
                    }
                }
                setLoadingStatus('', false)
                if ((reason === 'auto' || reason === 'open') && loadedPages !== 'max') {
                    scheduleAutoPreload(180)
                }
            }
        })
    }

    function maybeLoadMoreOnScroll() {
        if (isLoadingPage || loadedPages === 'max') return
        const panel = document.querySelector('.sf-panel')
        if (!panel || panel.style.display !== 'block') return
        const remaining = panel.scrollHeight - panel.scrollTop - panel.clientHeight
        if (remaining <= 160) {
            loadNextPage('scroll')
        }
    }

    function appendItem(s) {
        const list = document.querySelector('.sf-info-list')
        const li = document.createElement('li')
        li.className = 'sf-info-item'; li.style = "border-bottom: 1px solid #eee; padding: 12px 0; list-style: none;"
        li.innerHTML = `
            <div class="sf-item-card" style="position:relative;">
                <div style="display:flex; justify-content:space-between; align-items:flex-start; padding-right:90px;">
                    <div>
                        <a class="sf-name" href="${s.url}" target="_blank" style="font-size:17px; font-weight:bold; color:#1e90ff; text-decoration:none;" title="查看详情">${s.name}</a>
                        <span style="font-size:13px; color:#999; margin-left:10px;">v${s.version}</span>
                        ${s.typeBadge ? `<span style="font-size:12px; background:#e0e0e0; color:#555; padding:2px 6px; border-radius:4px; margin-left:8px;">${s.typeBadge}</span>` : ''}
                    </div>
                    <a href="${s.installUrl}" target="_blank" style="position:absolute; right:0; top:0; padding:6px 12px; background:#4CAF50; color:white; text-decoration:none; border-radius:4px; font-size:13px; font-weight:bold; box-shadow:0 2px 4px rgba(0,0,0,0.1); transition:background 0.2s;" title="点击直接安装">${getT('Install')}</a>
                </div>
                <div class="sf-trans-name" style="display:none; font-size:14px; color:#d35400; background:#fff5eb; padding:3px 6px; border-radius:4px; margin:4px 0;"></div>
                <p class="sf-desc" style="font-size:14px; color:#444; margin:6px 0; line-height:1.5;">${s.desc}</p>
                <div class="sf-trans-desc" style="display:none; font-size:14px; color:#d35400; background:#fff5eb; padding:3px 6px; border-radius:4px; margin:4px 0;"></div>
                <div style="display:flex; flex-wrap:wrap; gap:12px; font-size:13px; color:#888; margin-top:8px;">
                    <span>👤 ${getT('Author')}: <span style="color:#555">${s.author}</span></span>
                    <span>📥 ${getT('Installs')}: <span style="color:#555">${s.installs}</span></span>
                    <span>📈 ${getT('Daily')}: <span style="color:#555">${s.daily}</span></span>
                    ${s.created ? `<span>📅 ${getT('Created')}: <span style="color:#555">${s.created}</span></span>` : ''}
                    ${s.updated ? `<span>🔄 ${getT('Updated')}: <span style="color:#555">${s.updated}</span></span>` : ''}
                    ${s.rating && s.rating !== '0' ? `<span>⭐ <span style="color:#f39c12">${s.rating}</span></span>` : ''}
                </div>
            </div>`
        list.appendChild(li); return li
    }

    function setupUI() {
        GM_addStyle(`
            scrbutton.sf-main-btn {
                position: fixed; right: 10px; padding: 10px 18px; font-size: 16px; font-weight: bold;
                border: none; border-radius: 30px; background: #1e90ff; color: #fff; cursor: grab; z-index: 999999;
                box-shadow: 0 4px 12px rgba(0,0,0,0.2); opacity: 0; display: none; transition: opacity 0.4s;
                user-select: none; touch-action: none;
            }
            @media screen and (max-width: 768px) {
                scrbutton.sf-main-btn {
                    padding: 10px 6px; font-size: 13px; width: 28px; line-height: 1.2;
                    border-radius: 8px; right: 5px; text-align: center;
                    word-break: break-all; display: flex; align-items: center; justify-content: center;
                }
            }
            div.sf-panel {
                display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
                width: 650px !important; max-width: calc(100vw - 30px) !important; box-sizing: border-box;
                background: #fff; border-radius: 12px; z-index: 1000000;
                box-shadow: 0 10px 40px rgba(0,0,0,0.4); padding: 15px; max-height: 80vh; overflow-y: auto;
            }
            .sf-sticky-head {
                position: sticky; top: -15px; z-index: 3; background: inherit; padding-top: 15px;
            }
            @media (prefers-color-scheme: dark) {
                div.sf-panel { background: #222; color: #ddd; }
                .sf-desc { color: #bbb !important; }
                .sf-trans-name, .sf-trans-desc { background: #3a2a1a !important; color: #ff9900 !important; }
            }
            .sf-loading-status {
                display: inline-block; width: 104px; overflow: hidden; text-overflow: ellipsis;
                font-size: 12px; font-weight: normal; color: #999; white-space: nowrap;
                vertical-align: middle; visibility: hidden;
            }
            .sf-loading-status.is-visible { visibility: visible; }
        `)

        const button = document.createElement('scrbutton'), panel = document.createElement('div')
        button.className = 'sf-main-btn'
        button.innerText = isMobile ? getT('MiniBtn') : getT('Scripts')
        document.body.appendChild(button)

        const savedPos = localStorage.getItem('sf-btn-top')
        button.style.top = savedPos ? savedPos + 'px' : '70%'

        panel.className = 'sf-panel'; panel.innerHTML = `
            <div class="sf-sticky-head">
                <div style="display:flex; justify-content:space-between; align-items:center; border-bottom:2px solid #1e90ff; padding-bottom:8px; gap:10px;">
                    <div style="display:flex; align-items:center; gap:8px; flex-wrap:wrap;">
                        <div style="font-size:18px; font-weight:bold; color:#1e90ff;">Script Finder<span class="sf-total-count" style="font-size:13px; color:#999; margin-left:6px;"></span><span class="sf-loading-status" role="status" aria-live="polite" aria-busy="false"></span></div>
                        <a class="sf-open-gf" href="https://greasyfork.org/scripts/by-site/${domain}?filter_locale=0&sort=updated" target="_blank" style="font-size:12px; color:#1e90ff; text-decoration:none; border:1px solid #1e90ff; border-radius:999px; padding:4px 8px;">${getT('OpenGF')}</a>
                    </div>
                    <button class="sf-close" style="border:none; background:none; cursor:pointer; font-size:24px; color:#999;">&times;</button>
                </div>
                <input type="text" class="sf-search" placeholder="${getT('Search')}" style="width:100%; padding:10px; margin:10px 0; border:1px solid #ddd; border-radius:6px; font-size:15px; box-sizing:border-box;">
            </div>
            <ul class="sf-info-list" style="padding:0; margin:0;"></ul>
            <div class="sf-auto-more-sentinel" style="height:2px; width:100%;"></div>
            <button class="sf-load-more" style="display:block; width:100%; padding:10px; background:#1e90ff; color:#fff; border:none; border-radius:4px; margin-top:10px; font-size:15px; cursor:pointer;">${getT('LoadMore')}</button>
        `
        document.body.appendChild(panel)

        let startY, startTop, moveDist = 0
        const onStart = (e) => {
            isDragging = true; moveDist = 0; button.style.transition = 'none'
            const cy = e.touches ? e.touches[0].clientY : e.clientY
            startY = cy; startTop = button.offsetTop; clearTimeout(hideTimer)
        }
        const onMove = (e) => {
            if (!isDragging) return
            if (e.cancelable) e.preventDefault()
            const cy = e.touches ? e.touches[0].clientY : e.clientY
            const dy = cy - startY; moveDist = Math.abs(dy)
            const nt = Math.max(10, Math.min(startTop + dy, window.innerHeight - 60))
            button.style.top = nt + 'px'; localStorage.setItem('sf-btn-top', nt)
        }
        const onEnd = () => { if (!isDragging) return; isDragging = false; button.style.transition = 'opacity 0.4s'; if (collapsed) startTimer() }

        button.addEventListener('mousedown', onStart); window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onEnd)
        button.addEventListener('touchstart', onStart, {passive:false}); window.addEventListener('touchmove', onMove, {passive:false}); window.addEventListener('touchend', onEnd)

        const startTimer = (d = 2000) => {
            clearTimeout(hideTimer); hideTimer = setTimeout(() => { if (collapsed && !isDragging) { button.style.opacity = '0'; setTimeout(() => { if (button.style.opacity === '0') button.style.display = 'none' }, 400) } }, d)
        }
        const showBtn = (d = 2000) => {
            if (button.style.display !== 'flex') {
                button.style.display = 'flex';
                setTimeout(() => button.style.opacity = '0.9', 10);
            } else {
                button.style.opacity = '0.9';
            }
            if (collapsed && !isDragging) startTimer(d);
        }

        // --- 核心修复:鼠标靠右自动显示 ---
        window.addEventListener('mousemove', (e) => {
            // 使用 document.documentElement.clientWidth 兼容存在滚动条的情况,将判定范围放宽到 60px
            const clientW = document.documentElement.clientWidth || window.innerWidth;
            if (collapsed && e.clientX > clientW - 60) showBtn(2000)
        })

        window.addEventListener('scroll', () => { if (collapsed) showBtn(2000) })
        document.addEventListener('mousedown', (e) => { if (!collapsed && !panel.contains(e.target) && !button.contains(e.target)) closePanel() })

        const closePanel = () => { if (autoPreloadTimer) clearTimeout(autoPreloadTimer); panel.style.display = 'none'; collapsed = true; startTimer() }
        button.onclick = () => {
            if (moveDist > 5) return
            if (collapsed) {
                panel.style.display = 'block'
                collapsed = false
                clearTimeout(hideTimer)
                if (neverLoaded) {
                    getScriptsInfo(domain, 1, { onComplete: () => loadNextPage('open') })
                    neverLoaded = false
                } else {
                    scheduleAutoPreload(120)
                }
            }
            else closePanel()
        }
        panel.querySelector('.sf-close').onclick = closePanel
        panel.querySelector('.sf-search').oninput = (e) => {
            pendingSearchTerm = e.target.value.trim().toLowerCase()
            if (searchLoadTimer) clearTimeout(searchLoadTimer)
            const visibleCount = updateSearchResults(pendingSearchTerm)
            if (!pendingSearchTerm) {
                setLoadingStatus('', false)
                return
            }
            if (visibleCount === 0 && loadedPages !== 'max') {
                searchLoadTimer = setTimeout(() => loadNextPage('search'), 120)
            }
        }
        panel.querySelector('.sf-load-more').onclick = () => { if (loadedPages !== 'max' && !isLoadingPage) loadNextPage('manual') }
        panel.addEventListener('scroll', maybeLoadMoreOnScroll, { passive: true })
        panel.addEventListener('wheel', () => setTimeout(maybeLoadMoreOnScroll, 0), { passive: true })
        panel.addEventListener('touchend', () => setTimeout(maybeLoadMoreOnScroll, 0), { passive: true })

        // 初始显示 2 秒
        showBtn(2000)
    }

    // 只要 DOM 准备好就立即执行,不用等图片等资源完全加载,提升出现速度
    if (document.body) {
        setupUI();
    } else {
        document.addEventListener('DOMContentLoaded', setupUI);
    }
})()