Greasy Fork

Greasy Fork is available in English.

夸克网盘播放音频

在文件列表特定单元格添加播放按钮,支持格式过滤与精准定位

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         夸克网盘播放音频
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  在文件列表特定单元格添加播放按钮,支持格式过滤与精准定位
// @author       Gemini 3 Pro
// @match        https://pan.quark.cn/*
// @grant        unsafeWindow
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 安全获取全局 store 对象
    const getStore = () => {
        if (typeof unsafeWindow !== 'undefined' && unsafeWindow.store) {
            return unsafeWindow.store;
        }
        if (typeof window !== 'undefined' && window.store) {
            return window.store;
        }
        return null;
    };

    // 支持的音频格式列表
    const AUDIO_EXT_REGEX = /\.(mp3|wav|ogg|m4a|aac|flac|weba|mp4)$/i;

    // 创建播放器弹窗 UI
    function createAudioPlayer(url, fileName) {
        const oldPlayer = document.getElementById('quark-custom-audio-player');
        if (oldPlayer) oldPlayer.remove();

        const container = document.createElement('div');
        container.id = 'quark-custom-audio-player';
        container.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: white;
            padding: 15px;
            border-radius: 8px;
            box-shadow: 0 4px 16px rgba(0,0,0,0.2);
            z-index: 9999;
            display: flex;
            flex-direction: column;
            gap: 10px;
            min-width: 320px;
            border: 1px solid #eee;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        `;

        const title = document.createElement('div');
        title.innerText = `正在播放: ${fileName}`;
        title.style.cssText = 'font-size: 14px; font-weight: 600; color: #333; word-break: break-all; padding-right: 20px;';

        const audio = document.createElement('audio');
        audio.controls = true;
        audio.autoplay = true;
        audio.src = url;
        audio.style.width = '100%';
        
        audio.onerror = () => {
            title.innerText = `播放失败: 格式不支持或链接过期 (${fileName})`;
            title.style.color = '#ff4d4f';
        };

        const closeBtn = document.createElement('div');
        closeBtn.innerText = '×';
        closeBtn.style.cssText = `
            position: absolute;
            top: 10px;
            right: 10px;
            cursor: pointer;
            font-size: 20px;
            color: #999;
            line-height: 1;
        `;
        closeBtn.onclick = () => container.remove();
        closeBtn.onmouseenter = () => closeBtn.style.color = '#333';
        closeBtn.onmouseleave = () => closeBtn.style.color = '#999';

        container.appendChild(closeBtn);
        container.appendChild(title);
        container.appendChild(audio);
        document.body.appendChild(container);
    }

    // 核心功能:获取链接并播放
    async function handlePlay(fid, fileName, btnElement) {
        const store = getStore();
        
        if (!store) {
            alert('无法获取夸克网盘接口 (window.store),请刷新页面后重试。');
            return;
        }

        const originalText = btnElement.innerText;

        try {
            btnElement.innerText = '⌛';
            btnElement.style.opacity = '0.7';

            const result = await store.dispatch.file.fetchDownloadFileInfo({
                fileInfos: [{ fid: fid }]
            });

            btnElement.innerText = originalText;
            btnElement.style.opacity = '1';

            if (result && result.fileInfos && result.fileInfos.length > 0) {
                const fileInfo = result.fileInfos[0];
                if (fileInfo.download_url) {
                    createAudioPlayer(fileInfo.download_url, fileName);
                } else {
                    alert('未找到下载链接,文件可能已被删除或没有权限。');
                }
            }
        } catch (error) {
            console.error('获取播放链接失败:', error);
            btnElement.innerText = '❌';
            setTimeout(() => { btnElement.innerText = originalText; }, 2000);
        }
    }

    // 向表格行添加按钮
    function addPlayButtons() {
        const rows = document.querySelectorAll('.ant-table-row.ant-table-row-level-0.ant-dropdown-trigger');

        rows.forEach(row => {
            if (row.dataset.hasPlayBtn === 'true') return;

            // 1. 校验是否为音频文件(使用你指定的 class 结构)
            const nameElement = row.querySelector('.filename-text.editable-cell');
            if (!nameElement) return;

            const fileName = nameElement.getAttribute('title');
            if (!fileName) return;

            // 如果不是音频,标记已处理并跳过
            if (!AUDIO_EXT_REGEX.test(fileName)) {
                row.dataset.hasPlayBtn = 'true';
                return; 
            }

            const fid = row.getAttribute('data-row-key');
            if (!fid) return;

            // 2. 【关键修改】查找目标 td 单元格
            const targetTd = row.querySelector('td.td-file.td-file-sort');
            
            // 如果找不到目标单元格(防御性编程),则不操作
            if (!targetTd) return;

            // 3. 创建按钮
            const btn = document.createElement('button');
            btn.innerText = '▶';
            btn.title = `播放 ${fileName}`;
            // 样式优化:定位到 td 内部右侧
            btn.style.cssText = `
                position: absolute;
                right: 180px; /* 预留位置给官方的悬浮操作按钮 */
                top: 50%;
                transform: translateY(-50%);
                padding: 4px 10px;
                background: #1677ff;
                color: white;
                border: none;
                border-radius: 4px;
                cursor: pointer;
                font-size: 12px;
                z-index: 100; /* 确保在图层上方 */
                line-height: 1.5;
                box-shadow: 0 2px 4px rgba(0,0,0,0.1);
                transition: all 0.2s;
            `;
            
            btn.onclick = (e) => {
                e.stopPropagation(); 
                handlePlay(fid, fileName, btn);
            };
            
            btn.onmouseenter = () => { btn.style.background = '#4096ff'; btn.style.transform = 'translateY(-50%) scale(1.1)'; };
            btn.onmouseleave = () => { btn.style.background = '#1677ff'; btn.style.transform = 'translateY(-50%) scale(1)'; };

            // 4. 确保 td 具有定位属性,以便按钮绝对定位
            // 获取计算样式,避免覆盖已有的 relative/absolute
            const tdStyle = window.getComputedStyle(targetTd);
            if (tdStyle.position === 'static') {
                targetTd.style.position = 'relative';
            }

            // 5. 插入到 td 中
            targetTd.appendChild(btn);
            
            // 标记完成
            row.dataset.hasPlayBtn = 'true';
        });
    }

    // 监听 DOM 变化
    const observer = new MutationObserver((mutations) => {
        addPlayButtons();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    setTimeout(addPlayButtons, 1000);

})();