// ==UserScript==
// @name 115分享页一键转存按钮
// @version 0.5
// @description 增加一键转存按钮,调用115web接口转存,可自定义Cookie和目标文件夹ID,并保存设置,右下角可修改设置,可用ui来查看文件夹id并保存设置,支持并适配移动端与pc端
// @author 楠
// @match *://115cdn.com/s/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @license MIT
// @icon https://115.com/favicon.ico
// @namespace https://greasyfork.org/users/1514724
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
function showToast(message, duration = 2500) {
const toast = document.createElement('div');
const isError = message.includes('❌') || message.includes('⚠️') || message.includes('请先设置') || message.includes('不能为空') || message.includes('已经转存过') || message.includes('失败') || message.includes('无法解析');
const bgGradient = isError ? 'linear-gradient(135deg, #ff5252, #b71c1c)' : 'linear-gradient(135deg, #4CAF50, #2E7D32)';
Object.assign(toast.style, {
position: 'fixed', top: '110px', right: '20px', padding: '16px 24px',
background: bgGradient, color: '#fff', borderRadius: '12px',
boxShadow: isError ? '0 6px 20px rgba(0,0,0,0.2), 0 0 20px rgba(255,82,82,0.3)' : '0 6px 20px rgba(0,0,0,0.2), 0 0 20px rgba(76,175,80,0.3)',
fontSize: '14px', fontWeight: '500', opacity: '0',
transform: 'translateX(100%) scale(0.9)',
transition: 'all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55)',
zIndex: 10000, maxWidth: '300px', backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)', overflow: 'hidden'
});
const progressBar = document.createElement('div');
Object.assign(progressBar.style, {
position: 'absolute', bottom: '0', left: '0', height: '3px',
background: 'linear-gradient(90deg, rgba(255,255,255,0.5), rgba(255,255,255,0.8))',
width: '100%', transform: 'scaleX(1)', transformOrigin: 'left center',
transition: 'transform linear', borderRadius: '0 0 12px 12px'
});
toast.appendChild(progressBar);
const icon = document.createElement('span');
icon.innerHTML = isError ? '⚠️' : '✓';
Object.assign(icon.style, {display: 'inline-block', marginRight: '10px', fontSize: '16px', fontWeight: 'bold', verticalAlign: 'middle'});
const textSpan = document.createElement('span');
textSpan.textContent = message; textSpan.style.verticalAlign = 'middle';
toast.appendChild(icon); toast.appendChild(textSpan); document.body.appendChild(toast);
requestAnimationFrame(() => {
toast.style.opacity = '1'; toast.style.transform = 'translateX(0) scale(1)';
progressBar.style.transition = `transform ${duration}ms linear`; progressBar.style.transform = 'scaleX(0)';
});
setTimeout(() => {
toast.style.opacity = '0'; toast.style.transform = 'translateX(100%) scale(0.9)';
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 500);
}, duration);
}
async function getFolders(cid = 0) {
const cookie = GM_getValue('cookie');
if (!cookie) { showToast('请先设置Cookie'); return []; }
try {
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET", url: `https://webapi.115.com/files?aid=1&cid=${cid}&show_dir=1&nsprefix=1`,
headers: {"Cookie": cookie, "User-Agent": "Mozilla/5.0"},
onload: resolve, onerror: reject
});
});
const data = JSON.parse(response.responseText);
if (data.state && data.data) {
return data.data.filter(item => item.fl && item.fl.length === 0).map(item => ({name: item.n, cid: item.cid}));
}
return [];
} catch { showToast('获取文件夹列表失败'); return []; }
}
function showSettingsModal() {
if (document.querySelector('#tm-settings-modal')) return;
const cookie = GM_getValue('cookie') || '';
const cid = GM_getValue('target_cid') || '';
const enableCopyBtn = GM_getValue('enable_copy_btn') || false;
const overlay = document.createElement('div');
overlay.id = 'tm-settings-modal';
Object.assign(overlay.style, {
position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
background: 'rgba(0,0,0,0.5)', zIndex: 10001,
display: 'flex', justifyContent: 'center', alignItems: 'center'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
background: '#fff', padding: '20px 25px', borderRadius: '10px', width: '420px',
boxShadow: '0 6px 20px rgba(0,0,0,0.3)', fontFamily: 'Arial, sans-serif',
maxHeight: '80vh', overflowY: 'auto'
});
modal.innerHTML = `
115 设置
`;
overlay.appendChild(modal); document.body.appendChild(overlay);
overlay.querySelector('#tm-toggle-cookie').onclick = () => {
const input = document.querySelector('#tm-cookie-input');
if (input.type === 'password') { input.type = 'text'; overlay.querySelector('#tm-toggle-cookie').textContent = '隐藏'; }
else { input.type = 'password'; overlay.querySelector('#tm-toggle-cookie').textContent = '显示'; }
};
overlay.querySelector('#tm-browse-folders').onclick = () => {
GM_setValue('cookie', document.querySelector('#tm-cookie-input').value.trim());
showFolderBrowser();
};
overlay.querySelector('#tm-settings-cancel').onclick = () => overlay.remove();
overlay.querySelector('#tm-settings-save').onclick = () => {
GM_setValue('cookie', document.querySelector('#tm-cookie-input').value.trim());
GM_setValue('target_cid', document.querySelector('#tm-cid-input').value.trim());
GM_setValue('enable_copy_btn', document.querySelector('#tm-enable-copy-btn').checked);
showToast('✅ 设置已保存'); overlay.remove(); updateCopyButton();
};
}
async function showFolderBrowser() {
if (document.querySelector('#tm-folder-browser')) return;
const overlay = document.createElement('div');
overlay.id = 'tm-folder-browser';
Object.assign(overlay.style, {
position: 'fixed', top: '0', left: '0', width: '100%', height: '100%',
background: 'rgba(0,0,0,0.5)', zIndex: 10002,
display: 'flex', justifyContent: 'center', alignItems: 'center'
});
const modal = document.createElement('div');
Object.assign(modal.style, {
background: '#fff', padding: '20px', borderRadius: '10px', width: '500px',
maxHeight: '80vh', boxShadow: '0 6px 20px rgba(0,0,0,0.3)',
fontFamily: 'Arial, sans-serif', display: 'flex', flexDirection: 'column'
});
modal.innerHTML = `
浏览文件夹
根目录
`;
overlay.appendChild(modal); document.body.appendChild(overlay);
let currentCid = 0, currentPath = ["根目录"], cidStack = [], pathStack = [];
async function loadFolders(cid = 0) {
const foldersList = document.getElementById('tm-folders-list');
foldersList.innerHTML = '加载中...
';
const folders = await getFolders(cid);
if (folders.length === 0) { foldersList.innerHTML = '该目录下没有文件夹
'; return; }
foldersList.innerHTML = '';
folders.forEach(folder => {
const folderItem = document.createElement('div');
folderItem.className = 'tm-folder-item';
folderItem.style.padding = '10px'; folderItem.style.borderBottom = '1px solid #eee'; folderItem.style.cursor = 'pointer';
folderItem.innerHTML = `${folder.name}CID: ${folder.cid}`;
folderItem.onclick = () => { cidStack.push(currentCid); pathStack.push([...currentPath]); currentCid = folder.cid; currentPath.push(folder.name); updatePathDisplay(); loadFolders(currentCid); };
foldersList.appendChild(folderItem);
});
}
function updatePathDisplay() { document.getElementById('tm-current-path').textContent = currentPath.join(' / '); }
document.getElementById('tm-folder-back').onclick = () => {
if (cidStack.length > 0) { currentCid = cidStack.pop(); currentPath = pathStack.pop(); updatePathDisplay(); loadFolders(currentCid); }
};
document.getElementById('tm-folder-cancel').onclick = () => overlay.remove();
document.getElementById('tm-folder-select').onclick = () => {
if (currentCid !== 0) { const cidInput = document.querySelector('#tm-cid-input'); if (cidInput) cidInput.value = currentCid; showToast(`已选择: ${currentPath.join(' / ')}`); }
overlay.remove();
};
loadFolders(currentCid); updatePathDisplay();
}
function addSettingsButton() {
if (document.querySelector('#tm-settings-btn')) return;
const btn = document.createElement('div');
btn.id = 'tm-settings-btn'; btn.textContent = '⚙️ 115设置';
Object.assign(btn.style, {
position: 'fixed', bottom: '160px', right: '26px',
backgroundColor: '#2196F3', color: '#fff', padding: '8px 12px',
borderRadius: '8px', cursor: 'pointer', zIndex: 10000, fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)', fontSize: '14px',
width: '90px', height: '18px', textAlign: 'center', lineHeight: '18px'
});
btn.onclick = showSettingsModal; document.body.appendChild(btn);
}
function addCopyButton() {
if (document.querySelector('#tm-copy-btn')) return;
const btn = document.createElement('div');
btn.id = 'tm-copy-btn'; btn.textContent = '📋 复制链接';
Object.assign(btn.style, {
position: 'fixed', bottom: '200px', right: '26px',
backgroundColor: '#9C27B0', color: '#fff', padding: '8px 12px',
borderRadius: '8px', cursor: 'pointer', zIndex: 10000, fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(0,0,0,0.2)', fontSize: '14px',
width: '90px', height: '18px', textAlign: 'center', lineHeight: '18px'
});
btn.onclick = () => {
navigator.clipboard.writeText(location.href).then(() => showToast('✅ 已复制当前链接')).catch(() => showToast('❌ 复制失败'));
};
document.body.appendChild(btn);
}
function updateCopyButton() {
const enable = GM_getValue('enable_copy_btn') || false;
const existing = document.querySelector('#tm-copy-btn');
if (enable && !existing) addCopyButton();
if (!enable && existing) existing.remove();
}
function copyTo115() {
const cookie = GM_getValue('cookie');
const target_cid = GM_getValue('target_cid');
if (!cookie) { showToast('⚠️ 请先设置Cookie', 3000); showSettingsModal(); return; }
if (!target_cid) { showToast('⚠️ 请先设置目标文件夹CID', 3000); showSettingsModal(); return; }
const share_link = location.href;
const share_code_match = share_link.match(/\/s\/([^?]+)/);
const password_match = share_link.match(/password=([^&]{4})/);
if (!share_code_match || !password_match) { showToast('❌ 无法解析分享链接或密码', 3000); return; }
const share_code = share_code_match[1], receive_code = password_match[1];
GM_xmlhttpRequest({
method: "POST", url: "https://proapi.115.com/android/2.0/share/receive",
headers: {"Cookie": cookie, "Content-Type": "application/x-www-form-urlencoded"},
data: `share_code=${encodeURIComponent(share_code)}&receive_code=${encodeURIComponent(receive_code)}&cid=${encodeURIComponent(target_cid)}&is_check=0`,
onload: function(response) {
try {
const responseData = JSON.parse(response.responseText);
if (responseData.errno === 4100024) showToast('⚠️ 你已经转存过该文件');
else if (responseData.state === true) showToast('✅ 转存成功!');
else showToast('❌ 转存失败: ' + (responseData.error || response.responseText));
} catch { showToast('❌ 响应解析失败: ' + response.responseText); }
},
onerror: function() { showToast('❌ 转存接口调用失败'); }
});
}
function addCustomButton() {
const original1 = document.querySelector('#js-share_save3');
if (original1 && !document.querySelector('#tm-copy-save-btn1')) {
const button = original1.cloneNode(true);
button.id = 'tm-copy-save-btn1'; button.removeAttribute('href'); button.removeAttribute('onclick');
button.textContent = '一键转存'; button.style.backgroundColor = '#4CAF50'; button.style.color = '#fff'; button.style.borderColor = '#4CAF50'; button.onclick = copyTo115;
original1.parentNode.insertBefore(button, original1.nextSibling);
}
const original2 = document.querySelector('a[btn="save"]');
if (original2 && !document.querySelector('#tm-copy-save-btn2')) {
const button = document.createElement('a');
button.id = 'tm-copy-save-btn2'; button.className = original2.className;
button.innerHTML = `一键转存`;
button.style.backgroundColor = '#4CAF50'; button.style.color = '#fff'; button.style.borderColor = '#4CAF50'; button.style.cursor = 'pointer'; button.onclick = copyTo115;
original2.parentNode.insertBefore(button, original2.nextSibling);
}
const original3 = document.querySelector('a[btn="confirm"].button.btn-large');
if (original3 && !document.querySelector('#tm-copy-save-btn3')) {
const button = document.createElement('a');
button.id = 'tm-copy-save-btn3'; button.className = 'button btn-large';
button.innerHTML = '一键转存';
button.style.backgroundColor = '#4CAF50'; button.style.color = '#fff'; button.style.borderColor = '#4CAF50';
button.style.marginTop = '-15px'; button.style.display = 'block'; button.style.cursor = 'pointer'; button.onclick = copyTo115;
original3.parentNode.appendChild(document.createElement('br')); original3.parentNode.appendChild(button);
}
}
const observer = new MutationObserver(addCustomButton);
observer.observe(document.body, {childList: true, subtree: true});
addCustomButton(); addSettingsButton(); updateCopyButton();
})();