发送到115 (悬浮窗版 - 手动配置)

115离线下载功能,重构为统一的悬浮窗交互,并支持手动配置UserID和Cookie,增强稳定性。新增手动输入离线链接功能。

// ==UserScript==
// @name         发送到115 (悬浮窗版 - 手动配置)
// @author       zxf10608 (重构 by AI Engineer)
// @version      8.1.4
// @icon         https://115.com/favicon.ico
// @namespace    https://greasyfork.org/zh-CN/scripts/408466
// @description  115离线下载功能,重构为统一的悬浮窗交互,并支持手动配置UserID和Cookie,增强稳定性。新增手动输入离线链接功能。
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://greasyfork.org/scripts/398240-gm-config-zh-cn/code/GM_config_zh-CN.js
// @require      https://greasyfork.org/scripts/412267-base64-v1-0/code/base64_v10.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/toastr.min.js
// @resource     toastrCss   https://cdn.jsdelivr.net/npm/[email protected]/build/toastr.min.css
// @match        http*://*/*
// @match        http*://*.115.com/*
// @exclude      http*://*.115.com/bridge*
// @exclude      http*://*.115.com/*/static*
// @exclude      http*://*.baidu.com/*
// @exclude      http*://*.iqiyi.com/*
// @exclude      http*://*.qq.com/*
// @exclude      http*://*.youku.com/*
// @exclude      http*://*.bilibili.com/
// @exclude      http*://*.pptv.com/*
// @exclude      http*://*.fun.tv/*
// @exclude      http*://*.sohu.com/*
// @exclude      http*://*.le.com/*
// @exclude      http*://*.tudou.com/*
// @exclude      http*://*.bilibili.com/*
// @exclude      http*://music.163.com/*
// @exclude      http*://github.com/*
// @exclude      http*://gitee.com/*
// @exclude      http*://btcache.me/*
// @exclude      http*://*.jd.com/*
// @exclude      http*://*.taobao.com/*
// @exclude      http*://*.tmall.com/*
// @exclude      http*://*.vip.com/*
// @exclude      http*://*.pinduoduo.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_download
// @grant        GM_openInTab
// @grant        GM_setClipboard
// @grant        GM_getResourceText
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @connect      115.com
// @connect      *
// @grant        unsafeWindow
// @grant        window.open
// @grant        window.close
// @run-at       document-start
// @compatible   chrome
// @license      GPL License
// ==/UserScript==

(function() {
    'use strict';
    var newVersion = 'v8.1.4'; // 版本号更新

    if (typeof GM_config == 'undefined') {
        alert('115优化大师:\n网络异常,相关库文件加载失败,脚本无法使用,请刷新网页重新加载!');
        return;
    }

    // 配置界面
    function config() {
        var windowCss = ''; // All styles are now in addNewUIStyles
        GM_registerMenuCommand('设置', () => GM_config.open());
        // 添加菜单命令:手动输入链接离线
        GM_registerMenuCommand('手动输入链接离线', showManualInputModal);

        // [v8.1.3] 恢复完整的配置项
        GM_config.init({
            id: 'Cfg', title: `115优化大师 ${newVersion}`, isTabs: true, skin: 'tab',
            css: windowCss, frameStyle: { height: '550px', width: '445px', zIndex: '2147483648' },
            fields: {
                offline_Down: { section: ['离线升级', ''], label: '启用悬浮窗一键离线', type: 'checkbox', default: true, },
                offline_result: { label: '任务添加后显示离线结果', type: 'checkbox', default: true },
                open_List: { label: '离线后自动打开任务列表', type: 'checkbox', default: false },
                open_search: { label: '离线成功后开启视频搜索', type: 'checkbox', default: true, line: 'start' },
                search_result: { label: '显示视频搜索结果', type: 'checkbox', default: true },
                open_Popup: { label: '搜到视频自动播放', type: 'checkbox', default: false, line: 'end' },
                fuzzy_find: { label: '启用下载地址模糊匹配', type: 'checkbox', default: false },
                folder_config: {
                    section: ['自定义文件夹', '可配置多个离线下载目标文件夹'],
                    label: '文件夹配置 (格式: 别名=CID)',
                    type: 'textarea',
                    default: '默认=0',
                    title: '每行一条记录,格式为"别名=CID值"。\n例如:\n电影=1234567890123456789\n剧集=9876543210987654321',
                    css: 'width: 95%; height: 120px;'
                },
                manual_credential: { section: ['手动身份凭证', '如果自动获取凭证失效,请在此手动配置'], label: '启用手动配置凭证', labelPos: 'right', type: 'checkbox', default: false, title: '勾选后,脚本将使用下方填写的UserID和Cookie,推荐使用此方式。' },
                manualUserID: { label: '115 UserID', type: 'text', default: '', title: '请填写您的115数字ID' },
                manualCookie: { label: '115 Cookie', type: 'textarea', default: '', title: '请填入完整的115登录Cookie' },
                credential_help: { label: '如何获取凭证?', type: 'button', click: function() { alert('1. 登录115网盘。\n2. 按F12打开开发者工具,切换到【网络(Network)】标签。\n3. 刷新页面或随便点击一个文件夹。\n4. 在网络请求列表中,找到任意一个发往 115.com 的请求,点击它。\n5. 在右侧出现的【标头(Headers)】面板中,向下滚动到【请求标头(Request Headers)】区域。\n6. UserID通常可以在Cookie值中找到(如`USER_ID=...;`),或者在页面源代码中搜索`USER_ID`。\n7. 完整的Cookie值,请直接复制`Cookie:`后面的所有文本。'); } },
            },
            events: {
                save: function() { GM_config.close(); location.reload(); }
            },
        });
    }
    config();

    // ================================================================
    // 全局变量
    // ================================================================
    var G = GM_config;
    var localHref = window.location.href, show_result = G.get('offline_result'), down_reg = /^(magnet|thunder|ftp|ed2k):/i;
    var UA = navigator.userAgent, sign_url = 'http://115.com/?ct=offline&ac=space', add_urls = 'http://115.com/web/lixian/?ct=lixian&ac=add_task_urls';
    var lists_url = 'http://115.com/web/lixian/?ct=lixian&ac=task_lists', a_list = `<br><a target="_blank" class="openList" href="javascript:void(0);" style="color:blue;" title="点击打开离线链接任务列表">打开任务列表</a>`;
    var detectedLinks = [], $fab, $modal;

    // ================================================================
    // 所有函数定义 (确保在使用前定义)
    // ================================================================

    function addNewUIStyles() {
        GM_addStyle(`
            /* Modern Theme */
            :root {
                --primary-color: #2c6fbb;
                --primary-color-light: #f0f6ff;
                --background-color: #ffffff;
                --text-color: #333;
                --secondary-text-color: #555;
                --border-color: #e0e0e0;
                --shadow-color: rgba(44, 111, 187, 0.2);
                --success-color: #4caf50;
                --error-color: #f44336;
            }

            /* Floating Action Button */
            .s115-fab {
                position: fixed; bottom: 40px; right: 40px;
                width: 56px; height: 56px;
                background: linear-gradient(145deg, #3a8dff, #2c6fbb);
                border-radius: 50%;
                display: none;
                justify-content: center; align-items: center;
                cursor: pointer;
                box-shadow: 0 8px 25px var(--shadow-color);
                z-index: 2147483640;
                transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
                color: white;
                font-size: 22px;
                font-weight: bold;
                border: none;
            }
            .s115-fab:hover { transform: scale(1.1) rotate(15deg); box-shadow: 0 12px 30px var(--shadow-color); }

            /* Modal Styles */
            .s115-modal-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0, 0, 0, 0.6);
                z-index: 2147483641;
                display: none;
                justify-content: center; align-items: center;
                backdrop-filter: blur(5px);
                animation: fadeIn 0.3s ease;
            }
            @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

            .s115-modal-window {
                background-color: var(--background-color);
                width: 90%; max-width: 700px; max-height: 90vh;
                border-radius: 16px;
                box-shadow: 0 15px 40px rgba(0,0,0,0.15);
                display: flex; flex-direction: column;
                overflow: hidden;
                transform: scale(0.95);
                animation: zoomIn 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
            }
            @keyframes zoomIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }

            .s115-modal-header {
                padding: 20px 30px;
                font-size: 22px; font-weight: 700;
                color: var(--primary-color);
                border-bottom: 1px solid var(--border-color);
                display: flex; justify-content: space-between; align-items: center;
                background-color: var(--primary-color-light);
            }
            #s115-close-modal {
                background: none; border: none; font-size: 30px;
                color: #aaa; cursor: pointer;
                padding: 0; line-height: 1;
                transition: all 0.2s;
            }
            #s115-close-modal:hover { color: var(--text-color); transform: rotate(90deg); }

            .s115-modal-content { padding: 10px 30px; overflow-y: auto; flex-grow: 1; }
            .s115-modal-content ul { list-style: none; padding: 0; margin: 0; }
            .s115-modal-content li {
                display: flex; align-items: center;
                padding: 15px 5px;
                border-bottom: 1px solid #f0f0f0;
                transition: background-color 0.2s;
            }
            .s115-modal-content li:last-child { border-bottom: none; }
            .s115-modal-content li:hover { background-color: var(--primary-color-light); border-radius: 8px; }

            .s115-modal-content input[type="checkbox"] {
                margin-right: 20px;
                min-width: 20px; height: 20px;
                accent-color: var(--primary-color);
                cursor: pointer;
            }
            .s115-modal-content span { word-break: break-all; color: var(--text-color); font-size: 15px; }

            .s115-modal-footer {
                padding: 20px 30px;
                border-top: 1px solid var(--border-color);
                background-color: #f9f9f9;
                display: flex; justify-content: space-between; align-items: center;
                gap: 16px;
            }

            /* General Button & Select Styles */
            .s115-modal-footer button, #Cfg .config_var button, #s115-folder-select {
                padding: 0 20px;
                height: 44px;
                border: 1px solid var(--border-color);
                border-radius: 10px;
                cursor: pointer;
                font-size: 15px;
                font-weight: 600;
                transition: all 0.2s ease;
            }
            #s115-folder-select { background-color: white; }
            #s115-folder-select:focus, #Cfg .config_var input[type="text"]:focus, #Cfg .config_var textarea:focus {
                outline: none;
                border-color: var(--primary-color);
                box-shadow: 0 0 0 4px var(--shadow-color);
            }
            .s115-btn-primary, #Cfg #Cfg_saveBtn {
                background: linear-gradient(145deg, #3a8dff, #2c6fbb);
                color: white;
                border: none;
                box-shadow: 0 4px 10px var(--shadow-color);
            }
            .s115-btn-primary:hover, #Cfg #Cfg_saveBtn:hover {
                opacity: 0.9;
                box-shadow: 0 6px 15px var(--shadow-color);
                transform: translateY(-2px);
            }
            .s115-modal-footer button:not(.s115-btn-primary) {
                background-color: #f0f0f0;
                color: var(--text-color);
            }
            .s115-modal-footer button:not(.s115-btn-primary):hover { background-color: #e0e0e0; }

            /* GM_Config Styles Refactor */
            #Cfg { border-radius: 16px !important; overflow: hidden !important; border: none !important; box-shadow: 0 15px 40px rgba(0,0,0,0.15) !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
            #Cfg .config_header { background: linear-gradient(to right, #3a8dff, #2c6fbb) !important; color: white !important; padding: 20px !important; font-size: 24px !important; text-align: center; }
            #Cfg .tab-container { background-color: var(--primary-color-light) !important; padding: 10px 20px 0 20px !important; border-bottom: 1px solid var(--border-color); }
            #Cfg .tab { border: none !important; background-color: transparent !important; padding: 12px 18px !important; font-size: 15px !important; color: var(--secondary-text-color) !important; border-radius: 10px 10px 0 0 !important; transition: all 0.3s; position: relative; bottom: -1px; }
            #Cfg .tab[selected='true'] { background-color: var(--background-color) !important; color: var(--primary-color) !important; font-weight: 700 !important; border-top: 1px solid var(--border-color) !important; border-left: 1px solid var(--border-color) !important; border-right: 1px solid var(--border-color) !important; }
            #Cfg .config_var {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 15px;
                align-items: center;
                padding: 15px 20px;
                border-bottom: 1px solid #f0f0f0;
            }
            #Cfg .config_var:last-of-type { border-bottom: none; }
            #Cfg .section_header_div {
                grid-column: 1 / -1;
                border-bottom: none;
                padding: 20px 20px 5px 20px;
            }
            #Cfg .section_header {
                font-size: 18px; font-weight: 700; color: var(--primary-color);
                margin: 0; padding-bottom: 10px;
                border-bottom: 3px solid var(--primary-color-light);
            }
            #Cfg .config_var .field_label {
                font-weight: 600;
                font-size: 15px;
            }
            #Cfg .config_var input[type="text"], #Cfg .config_var textarea, #Cfg .config_var button {
                width: 100%;
                box-sizing: border-box;
            }
            #Cfg .config_var input[type="text"], #Cfg .config_var textarea {
                padding: 12px;
                border: 1px solid var(--border-color);
                border-radius: 10px;
                font-size: 15px;
                transition: all 0.2s ease;
            }
            #Cfg .config_var textarea { height: 140px; resize: vertical; }

            #Cfg .config_var input[type="checkbox"] {
                height: 0; width: 0; visibility: hidden;
            }
            #Cfg .config_var .field_label[for*="checkbox"] {
                position: relative;
                cursor: pointer;
                padding-left: 55px;
                line-height: 24px;
            }
            #Cfg .config_var .field_label[for*="checkbox"]::before {
                content: '';
                position: absolute; left: 0; top: 0;
                width: 44px; height: 24px;
                border-radius: 12px;
                background: #ccc;
                transition: background-color 0.3s;
            }
            #Cfg .config_var .field_label[for*="checkbox"]::after {
                content: '';
                position: absolute; left: 2px; top: 2px;
                width: 20px; height: 20px;
                border-radius: 50%;
                background: white;
                transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
                box-shadow: 0 1px 3px rgba(0,0,0,0.2);
            }
            #Cfg .config_var input[type="checkbox"]:checked + label::before { background-color: var(--success-color); }
            #Cfg .config_var input[type="checkbox"]:checked + label::after { transform: translateX(20px); }
            #Cfg #Cfg_buttons_holder { padding: 20px; border-top: 1px solid var(--border-color); background-color: #f9f9f9; text-align: right; }

            /* 手动输入链接离线模态框样式 */
            .s115-manual-modal-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background-color: rgba(0, 0, 0, 0.6);
                z-index: 2147483643;
                display: none;
                justify-content: center; align-items: center;
                backdrop-filter: blur(5px);
                animation: fadeIn 0.3s ease;
            }
            .s115-manual-modal-window {
                background-color: var(--background-color);
                width: 90%; max-width: 500px; max-height: 80vh;
                border-radius: 16px;
                box-shadow: 0 15px 40px rgba(0,0,0,0.15);
                display: flex; flex-direction: column;
                overflow: hidden;
                transform: scale(0.95);
                animation: zoomIn 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) forwards;
            }
            .s115-manual-modal-header {
                padding: 20px 30px;
                font-size: 22px; font-weight: 700;
                color: white;
                border-bottom: 1px solid rgba(255,255,255,0.1);
                display: flex; justify-content: space-between; align-items: center;
                background: linear-gradient(145deg, #37c976, #229455);
            }
            #s115-manual-close-modal {
                background: none; border: none; font-size: 30px;
                color: rgba(255,255,255,0.8); cursor: pointer;
                padding: 0; line-height: 1;
                transition: all 0.2s;
            }
            #s115-manual-close-modal:hover { color: white; transform: rotate(90deg); }
            .s115-manual-modal-content {
                padding: 20px;
                display: flex; flex-direction: column; gap: 15px;
            }
            #s115-manual-input {
                width: 100%; height: 200px;
                padding: 15px;
                border: 1px solid var(--border-color);
                border-radius: 10px;
                font-size: 15px;
                resize: vertical;
                transition: border 0.3s;
            }
            #s115-manual-input:focus {
                outline: none;
                border-color: #37c976;
                box-shadow: 0 0 0 4px rgba(55, 201, 118, 0.15);
            }
            .s115-manual-select-group {
                display: flex; align-items: center;
                gap: 10px;
            }
            #s115-manual-folder-select {
                flex: 1;
                height: 46px;
                padding: 10px 15px;
                border: 1px solid var(--border-color);
                border-radius: 8px;
                font-size: 15px;
                background-color: white;
            }
            .s115-manual-modal-footer {
                padding: 20px 30px;
                border-top: 1px solid var(--border-color);
                background-color: #f9f9f9;
                display: flex; justify-content: space-between;
            }
            .s115-btn-secondary {
                padding: 0 20px;
                height: 44px;
                background-color: #f0f0f0;
                color: var(--text-color);
                border: none;
                border-radius: 10px;
                cursor: pointer;
                font-size: 15px;
                font-weight: 600;
                transition: all 0.2s ease;
            }
            .s115-btn-secondary:hover {
                background-color: #e0e0e0;
            }
        `);
    }

    function notice() { GM_addStyle(GM_getResourceText('toastrCss')); GM_addStyle('.toast{font-size:15px!important;width:360px!important;} .toast-title{font-size:16px!important;text-align:center}'); toastr.options = { "closeButton": true, "debug": false, "progressBar": true, "timeOut": 8000, "extendedTimeOut": 8000, "positionClass": 'toast-top-right', "allowHtml": true, "newestOnTop": false, }; }
    function getRightUrl(url) {
        var n = url.trim();
        if (/^thunder/i.test(n)) {
            n = decodeURIComponent(decode64(n.replace(/thunder:\/\//i, '')).slice(2, -2));
        }
        // After thunder decoding, it could be an ed2k or magnet link
        if (/^ed2k:\/\//i.test(n)) {
            const parts = n.split('|');
            if (parts.length >= 5 && parts[1] === 'file') {
                // Reconstruct to ensure a clean, standard link
                n = `ed2k://|file|${parts[2]}|${parts[3]}|${parts[4]}|/`;
            }
        }
        if (/^magnet/i.test(n)) {
            var h = n.split('&')[0].substring(20) || n.substring(20);
            if (h.length == 32) h = base32To16(h);
            n = 'magnet:?xt=urn:btih:' + h;
        } else if (/^\/\//.test(n)) {
            n = location.protocol + n;
        } else if (/^\/(?!\/)/.test(n)) {
            n = location.protocol + '//' + location.host + url;
        }
        return n;
    }
    function base32To16(str) { if (str.length % 8 !== 0 || /[0189]/.test(str)) return str; str = str.toUpperCase(); var bin = "", newStr = "", i; for (i = 0; i < str.length; i++) { var c = str.charCodeAt(i); c = '0000' + (c < 65 ? c - 24 : c - 65).toString(2); bin += c.substr(c.length - 5); } for (i = 0; i < bin.length; i += 4) { newStr += parseInt(bin.substring(i, i + 4), 2).toString(16); } return newStr; }
    function verify() { if (confirm('立即打开验证账号弹窗?')) { try { if (window.open('https://captchaapi.115.com/?ac=security_code&type=web&cb=Close' + Date.now(), '请验证账号', `height=500,width=335,top=${(window.screen.availHeight - 500) / 2},left=${(window.screen.availWidth - 335) / 2},toolbar=no,menubar=no`) === null) alert('验证弹窗已被拦截!'); } catch (e) { alert('验证弹窗已被拦截!'); } } }
    function get115Headers() { const h = { "User-Agent": UA, Origin: "https://115.com" }; if (G.get('manual_credential') && G.get('manualCookie').trim() !== '') { h['Cookie'] = G.get('manualCookie'); } return h; }
    function getAttribute(e) { var d = []; $.each(e.attributes, (i, attr) => { if (attr.specified && attr.value.length > 30) d.push(attr.value); }); if ($(e).text().length > 25) d.push($(e).text()); return d; }

    var offline = (() => ({
        getSign: (key, cid) => new Promise((resolve, reject) => {
            if (/^\w+=/.test(key)) { resolve(key); return; }
            const UserID = G.get('manual_credential') ? G.get('manualUserID') : GM_getValue('115ID') || '';
            if (!UserID) { toastr.error('请先登录115或在设置中手动填写UserID!', '认证失败'); return reject('No UserID'); }
            GM_xmlhttpRequest({
                method: 'GET', url: sign_url, responseType: 'json', headers: get115Headers(),
                onload: (res) => {
                    if (res.responseText.includes('<html')) {
                        toastr.error('请先登录115网盘或检查Cookie是否正确!', '离线任务添加失败');
                        setTimeout(() => { if (confirm('立即打开115登录页面?')) GM_openInTab('https://115.com/?mode=login', false); }, 3000);
                        return reject('Not logged in or invalid cookie');
                    }
                    const data = { uid: UserID, sign: res.response.sign, time: res.response.time, wp_path_id: cid || '0', savepath: '' };
                    const value = $.isPlainObject(key) ? $.param($.extend(data, key)) : $.param(data) + `&url=${encodeURIComponent(key)}`;
                    resolve(value);
                },
                onerror: reject,
            });
        }),
        getData: (herf, key, cid) => offline.getSign(key, cid).then(value => new Promise((resolve, reject) => {
            const headers = { ...get115Headers(), "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Accept": "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest" };
            GM_xmlhttpRequest({ method: 'POST', data: value, url: herf, responseType: 'json', headers, onload: (res) => resolve(res.response), onerror: reject });
        })),
        addButton: () => { $('[href]').not('[Searched]').each(function() { const url = $(this).attr('href'); if (!/^(magnet|thunder|ftp|ed2k):/i.test(url) && !/\.(torrent|rar|zip|7z|mp4|rmvb|mkv|avi)$/i.test(url)) return; $(this).attr('Searched', 'true'); const link = getRightUrl(url); if (!detectedLinks.some(item => item.url === link)) { detectedLinks.push({ url: link, text: $(this).text() }); } }); updateFab(); },
        addLink: () => { $('a,button,span,li').not('[Searched],[href*="google"]').each(function() { if ($(this).find('img').length > 0) return; for (let attr of getAttribute(this)) { if (/(^|\/|&|-|\.|\?|=|:|#|_|@)([a-f0-9]{40}|[a-z2-7]{32})(?!\w)/i.test(attr)) { const link = getRightUrl('magnet:?xt=urn:btih:' + attr.match(/(?:[a-f0-9]{40}|[a-z2-7]{32})/i)[0]); if (!detectedLinks.some(item => item.url === link)) { detectedLinks.push({ url: link, text: $(this).text() }); $(this).attr('Searched', 'true'); updateFab(); } return; } } }); },
addEd2kLinksFromText: () => {
            const bodyText = document.body.innerText;
            const ed2kRegex = /(ed2k:\/\/(?:\|file\|[^|]+\|\d+\|[a-fA-F0-9]{32}\|\/))/gi;
            let match;
            while ((match = ed2kRegex.exec(bodyText)) !== null) {
                const link = getRightUrl(match[0]);
                if (!detectedLinks.some(item => item.url === link)) {
                    const parts = link.split('|');
                    const text = parts.length > 2 ? parts[2] : link; // Use filename as text
                    detectedLinks.push({ url: link, text: text });
                }
            }
        },
    }))();

    function executeBatchDownload(links, folderCid) {
        if (links.length === 0) {
            toastr.warning('没有发现任何可离线的链接');
            return;
        }

        const btn = $('#s115-batch-download');
        if(btn.attr('disabled')) return;
        btn.attr('disabled', true).css('opacity', '0.5');
        setTimeout(() => btn.attr('disabled', false).css('opacity', '1'), links.length > 10 ? 10000 : 5000);

        var linksParams = {};
        links.forEach((link, i) => linksParams[`url[${i}]`] = link.url);

        offline.getData(add_urls, linksParams, folderCid).then(json => {
            if (json.state) {
                let s = 0, e = 0, f = 0;
                json.result.forEach(r => {
                    if (r.state) s++;
                    else if (r.errcode == 10008) e++;
                    else f++;
                });
                toastr.success(`成功 ${s}, 存在 ${e}, 失败 ${f}` + a_list, `离线任务结果`, { timeOut: 10000 });
                if (G.get('open_List') && s > 0) setTimeout(() => GM_openInTab('https://115.com/?tab=offline&mode=wangpan', false), 2000);
            } else if (json.errcode == 911) {
                toastr.warning('账号异常,请验证。', '离线任务失败'); setTimeout(verify, 1000);
            } else {
                toastr.error((json.error_msg || '未知错误') + a_list, '离线任务失败');
            }
        }).catch(err => {
            toastr.error('服务器繁忙或网络错误', '离线任务异常');
            console.error(err);
        });
    }

    function handleBatchDownload() {
        var $checkedItems = $('.s115-link-checkbox:checked'), l = $checkedItems.length;
        if (l === 0) { toastr.warning('请至少选择一个链接。'); return; }

        const links = $checkedItems.map((i, el) => {
            return {
                url: $(el).val(),
                text: $(el).siblings('span').text()
            };
        }).get();

        const selectedCid = $('#s115-folder-select').val();
        hideModal();
        executeBatchDownload(links, selectedCid);
    }

    function showModal() {
        let linkHtml = '';
        detectedLinks.forEach(link => {
            const text = $('<div>').text(link.text || link.url).html();
            linkHtml += `<li><input type="checkbox" class="s115-link-checkbox" value="${link.url}" checked><span title="${link.url}">${text}</span></li>`;
        });
        $('#s115-link-list').html(linkHtml);

        let folderHtml = '';
        const folderConfig = G.get('folder_config').trim().split('\n');
        folderConfig.forEach(line => {
            const parts = line.split('=');
            if (parts.length === 2) {
                const alias = parts[0].trim();
                const cid = parts[1].trim();
                if (alias && /^\d+$/.test(cid)) {
                    folderHtml += `<option value="${cid}">${alias}</option>`;
                }
            }
        });
        $('#s115-folder-select').html(folderHtml);

        $modal.css('display', 'flex');
    }
    function hideModal() { $modal.hide(); }
    function updateFab() { var len = detectedLinks.length; if (len > 0) { $fab.css('display','flex').text(len); } else { $fab.hide(); } }
    function createUI() {
        $('body').append(`
            <div class="s115-fab">0</div>
            <div class="s115-modal-overlay"><div class="s115-modal-window">
                <div class="s115-modal-header"><span>115离线下载任务列表</span><button id="s115-close-modal" title="关闭">×</button></div>
                <div class="s115-modal-content"><ul id="s115-link-list"></ul></div>
                <div class="s115-modal-footer">
                    <div><button id="s115-select-all">全选</button><button id="s115-deselect-all" style="margin-left: 10px;">取消</button></div>
                    <div>
                        <select id="s115-folder-select" style="padding: 8px; border-radius: 5px; border: 1px solid #ccc; margin-right: 10px;"></select>
                        <button class="s115-btn-primary" id="s115-batch-download">离线下载选中项</button>
                    </div>
            </div></div></div>

            <!-- 手动输入链接离线模态框 -->
            <div class="s115-manual-modal-overlay">
                <div class="s115-manual-modal-window">
                    <div class="s115-manual-modal-header">
                        <span>手动输入链接离线下载</span>
                        <button id="s115-manual-close-modal" title="关闭">×</button>
                    </div>
                    <div class="s115-manual-modal-content">
                        <textarea
                            id="s115-manual-input"
                            placeholder="请输入磁力链接、ed2k链接、thunder链接或普通下载链接,多个链接请分行输入..."
                        ></textarea>
                        <div class="s115-manual-select-group">
                            <label>保存到文件夹:</label>
                            <select id="s115-manual-folder-select"></select>
                        </div>
                    </div>
                    <div class="s115-manual-modal-footer">
                        <button class="s115-btn-secondary" id="s115-manual-clear">清空</button>
                        <button class="s115-btn-primary" id="s115-manual-submit">开始离线下载</button>
                    </div>
                </div>
            </div>
        `);
        $fab = $('.s115-fab');
        $modal = $('.s115-modal-overlay');

        // 绑定主浮动窗口事件
        $fab.on('click', showModal);
        $modal.on('click', function(e) { if ($(e.target).is('#s115-close-modal') || $(e.target).is('.s115-modal-overlay')) hideModal(); });
        $('#s115-select-all').on('click', () => $('#s115-link-list :checkbox').prop('checked', true));
        $('#s115-deselect-all').on('click', () => $('#s115-link-list :checkbox').prop('checked', false));
        $('#s115-batch-download').on('click', handleBatchDownload);

        // 绑定手动输入窗口事件
        $('#s115-manual-close-modal').on('click', () => $('.s115-manual-modal-overlay').hide());
        $('#s115-manual-clear').on('click', () => $('#s115-manual-input').val(''));
        $('#s115-manual-submit').on('click', handleManualDownload);
        $('.s115-manual-modal-overlay').on('click', function(e) {
            if ($(e.target).is('.s115-manual-modal-overlay')) {
                $('.s115-manual-modal-overlay').hide();
            }
        });
    }

    // ================================================================
    // 手动输入链接离线功能
    // ================================================================
    function showManualInputModal() {
        populateFolderDropdown('#s115-manual-folder-select');
        $('.s115-manual-modal-overlay').css('display', 'flex');
        $('#s115-manual-input').focus();
    }

    function handleManualDownload() {
        const inputText = $('#s115-manual-input').val().trim();
        if (!inputText) {
            toastr.warning('请输入至少一个链接');
            return;
        }

        // 分割输入的链接
        const links = inputText.split('\n')
            .map(link => link.trim())
            .filter(link => link.length > 0)
            .map(link => ({
                url: getRightUrl(link),
                text: link.substring(0, 80) + (link.length > 80 ? '...' : '')
            }));

        if (links.length === 0) {
            toastr.warning('没有找到有效链接');
            return;
        }

        const selectedCid = $('#s115-manual-folder-select').val();
        $('.s115-manual-modal-overlay').hide();
        executeBatchDownload(links, selectedCid);
    }

    function populateFolderDropdown(selector) {
        let folderHtml = '<option value="0">默认文件夹</option>';
        const folderConfig = G.get('folder_config').trim().split('\n');

        folderConfig.forEach(line => {
            const parts = line.split('=');
            if (parts.length === 2) {
                const alias = parts[0].trim();
                const cid = parts[1].trim();
                if (alias && /^\d+$/.test(cid)) {
                    folderHtml += `<option value="${cid}">${alias}</option>`;
                }
            }
        });

        $(selector).html(folderHtml);
    }

    // ================================================================
    // 主执行逻辑
    // ================================================================
    $(document).ready(function() {
        notice();

        if (localHref.includes('captchaapi.115.com')) {
            $('body').on('click', '.vcode-hint', () => setTimeout(() => window.close(), 200));
            return;
        }

        if (!G.get('manual_credential') && localHref.includes('115.com/')) {
            try {
                if (unsafeWindow.USER_ID) {
                    GM_setValue('115ID', unsafeWindow.USER_ID); console.log('115账号ID自动获取成功!');
                }
            } catch (e) {
                console.log('115账号未登录或页面结构变化,无法自动获取UserID。');
            }
        }

        if (G.get('offline_Down') && !localHref.includes('115.com/')) {
            addNewUIStyles();
            createUI();

            // Use MutationObserver for more efficient link detection
            const scanForLinks = () => {
                offline.addButton();
                offline.addEd2kLinksFromText();
                if (G.get('fuzzy_find')) offline.addLink();
                updateFab(); // Consolidate FAB update
            };

            // Debounce function to avoid excessive scanning on busy pages
            let debounceTimer;
            const debouncedScan = () => {
                clearTimeout(debounceTimer);
                debounceTimer = setTimeout(scanForLinks, 300);
            };

            // Initial scan on page load
            debouncedScan();

            // Set up an observer for dynamically added content
            const observer = new MutationObserver(debouncedScan);
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    });

})();