// ==UserScript==
// @name 115网盘 - 手机端适配
// @namespace https://github.com/cxf35/Video-Takeover
// @version 2.1.8
// @description 手机浏览器中使用115网盘:访问115.com自动直达新版网盘,App化手机端适配(底部导航、行尾⋮菜单:移动/复制/重命名/星标/删除、云下载与回收站界面优化)。视频接管由其他脚本完成。
// @author Video Takeover
// @icon https://115.com/favicon.ico
// @match https://115.com/*
// @match https://*.115.com/*
// @run-at document-start
// @grant GM_addStyle
// @grant unsafeWindow
// @license GPL-3.0
// ==/UserScript==
(function () {
'use strict';
// ============================================================
// 1. 手机端直达新版网盘
// 手机访问 115.com 根域时,115 会重定向到 w.115.com(手机门户宣传页)/ m.115.com,
// 进不了网盘。这里把任意 115 域名下非网盘/播放页的页面直接跳转到新版网盘。
// @run-at document-start 保证先于115自身跳转脚本执行。
// /storage/ 新版网盘页面自身不触发跳转,避免循环。
// ============================================================
const isMobile = /Mobi|Android|iPhone|iPad|iPod|Mobile|UCBrowser|QQBrowser/i.test(navigator.userAgent)
|| (('ontouchstart' in window) && window.innerWidth < 768);
if (isMobile && /(^|\.)115\.com$/.test(location.hostname)
&& location.pathname.indexOf('/storage/') !== 0
&& location.pathname.indexOf('/players/') !== 0) {
location.replace('https://115.com/storage/allfiles?cid=0&mode=wangpan');
return;
}
// ============================================================
// 2. 强制侧边栏"已收起"状态
// 115 用 localStorage['layout_collapsed_all_files'] 记录左侧面板收起状态
// (各 storage 页面共用此 key,用户手动收起后刷新不会展开)。
// document-start 阶段写入 true,115 初始化侧边栏时读到"已收起",
// 面板原生保持收起(布局、动画、内部状态与手动收起完全一致)。
// 配合下方隐藏 CollapseButton,用户也无法再展开。
// ============================================================
try {
localStorage.setItem('layout_collapsed_all_files', 'true');
} catch (_) {}
// ============================================================
// 3. 新版网盘侧边栏改造 + 消除横向滚动(storage 页面全局生效)
// 115 新版左侧栏(.container-leftside)含频道导航(存储/社交/我聊/生活)
// 与存储功能菜单(全部文件/云下载/星标文件/回收站等)。
// 需求:
// a) 左侧面板默认收起且"不能打开"(localStorage 收起状态 + 隐藏展开按钮);
// b) 最左侧菜单栏整体隐藏(文件管理区占满屏幕),4 个网盘功能
// (全部文件/云下载/星标文件/回收站)移至页面底部,App 风格底部导航;
// c) 消除手机端横向滚动(主容器 min-w-[800px] 适配视口 + 内容区禁横滑)。
// ============================================================
const SIDEBAR_ITEMS = [
{ name: '全部文件', href: '/storage/allfiles?cid=0&mode=wangpan', icon: '/icons/sidebar_storage_gray.svg' },
{ name: '云下载', href: '/storage/clouddownload', icon: '/icons/storage/channel_cloud_download.svg' },
{ name: '星标文件', href: '/storage/starredfiles', icon: '/icons/storage/channel_starred_files.svg' },
{ name: '回收站', href: '/storage/recyclebin', icon: '/icons/storage/channel_recycle_bin.svg' },
];
GM_addStyle(`
/* 最左侧菜单栏整体隐藏:不再占据文件管理区宽度 */
.container-leftside { display: none !important; }
/* 隐藏侧边栏展开/收起按钮(面板不能再次打开) */
[class*="CollapseButton"] { display: none !important; }
/* 消除横向滚动:115 主容器 min-w-[800px] 在窄屏强制 800px 宽导致页面可左右滑动,
强制主容器适配视口宽度 + 全页禁止横向溢出 */
[class*="min-w-[800px]"] { min-width: 100vw !important; }
html, body { overflow-x: hidden !important; }
/* 底部功能导航(App 风格) */
.vt115-tabbar {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 99999;
height: 64px; display: flex; align-items: stretch;
background: rgba(13, 17, 23, 0.92);
border-top: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
padding-bottom: env(safe-area-inset-bottom, 0);
}
.vt115-tab-item,
#vt-tabbar .vt115-tab-item {
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 3px; color: #E6EBF2 !important; font-size: 13px; line-height: 1.2;
text-decoration: none; cursor: pointer;
}
#vt-tabbar .vt115-tab-item span { color: #E6EBF2 !important; }
#vt-tabbar .vt115-tab-item img { width: 26px; height: 26px; opacity: .85; }
#vt-tabbar .vt115-tab-item:hover, #vt-tabbar .vt115-tab-item:hover span { color: #E6EBF2 !important; }
#vt-tabbar .vt115-tab-item.active { color: #4C8DFF !important; font-weight: 600; }
#vt-tabbar .vt115-tab-item.active span { color: #4C8DFF !important; }
#vt-tabbar .vt115-tab-item.active img { opacity: 1; }
/* 云下载页右下角"+"号添加按钮(App 风格 FAB,参考115手机App) */
.vt115-fab {
position: fixed; right: 16px; bottom: 80px; z-index: 999999;
width: 52px; height: 52px; border-radius: 50%;
background: #1E6FFF; color: #fff;
font-size: 30px; font-weight: 300; line-height: 1;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 12px rgba(30, 111, 255, 0.45);
cursor: pointer; user-select: none;
-webkit-tap-highlight-color: transparent;
}
.vt115-fab:active { transform: scale(0.94); }
/* 云下载页每行文件名栏最右侧"⋮"菜单按钮 */
.vt115-rowmenu {
flex-shrink: 0; width: 32px; height: 32px; margin-left: 4px;
display: flex; align-items: center; justify-content: center;
color: rgba(26, 39, 52, 0.85); font-size: 18px; line-height: 1; cursor: pointer;
border-radius: 6px; user-select: none;
-webkit-tap-highlight-color: transparent;
}
.vt115-rowmenu:active { background: rgba(255, 255, 255, 0.3); }
/* 全部文件页行尾"⋮"按钮:absolute 钉在文件行内右缘(随行滚动 = "固定在文件条目上";行滚出视口按钮自然不可见) */
.vt115-rowmenu-fixed {
position: absolute; right: 4px; z-index: 99999; display: none;
width: 36px; height: 36px; margin: 0;
background: rgba(255, 255, 255, 0.16);
-webkit-backdrop-filter: blur(10px) saturate(1.5);
backdrop-filter: blur(10px) saturate(1.5);
border: 1px solid rgba(255, 255, 255, 0.28);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
/* 行菜单浮层(磨砂玻璃) */
.vt115-rowpop {
position: fixed; z-index: 999999; display: none;
background: rgba(255, 255, 255, 0.2);
-webkit-backdrop-filter: blur(16px) saturate(1.6);
backdrop-filter: blur(16px) saturate(1.6);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 10px; padding: 6px 0; min-width: 110px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.22);
}
.vt115-rowpop-item {
padding: 10px 16px; font-size: 14px; color: rgba(26, 39, 52, 0.9);
cursor: pointer; text-align: center; user-select: none;
-webkit-tap-highlight-color: transparent;
}
.vt115-rowpop-item:active { background: rgba(255, 255, 255, 0.35); }
.vt115-rowpop-item.danger { color: #E5484D; }
`);
// 仅处理新版网盘页面
if (location.pathname.indexOf('/storage/') !== 0) return;
function setupSidebar() {
function updateActive() {
document.querySelectorAll('.vt115-tab-item').forEach(a => {
const href = (a.getAttribute('href') || '').split('?')[0];
a.classList.toggle('active', location.pathname === href);
});
}
// 横向溢出兜底:若页面仍可横向滚动,把最宽的容器 min-width 限制为视口宽。
// 内部可横向滚动的容器:全部文件等列表页禁止横向滚动(内容适配视口);
// 云下载页保留原生布局不压缩、允许横向滑动(手机端整页缩放显示完整可见,行内操作由"⋮"菜单承担)
function fixHScroll() {
const doc = document.documentElement;
if (doc.scrollWidth > window.innerWidth + 1) {
let worst = null, w = 0;
document.querySelectorAll('div,main,section,aside,table,ul,ol,nav').forEach(el => {
const r = el.getBoundingClientRect();
if (r.width > w && r.width > window.innerWidth) { w = r.width; worst = el; }
});
if (worst) worst.style.minWidth = window.innerWidth + 'px';
}
const isCloudDL = location.pathname.indexOf('/storage/clouddownload') === 0;
document.querySelectorAll('[class*="overflow-auto"],[class*="overflow-x-auto"],[class*="overflow-scroll"]').forEach(cont => {
if (cont.scrollWidth > cont.clientWidth + 2 && !isCloudDL) cont.style.overflowX = 'hidden';
});
}
// 内容滚动容器底部留出导航栏高度,避免最后内容被遮挡
function padContent() {
document.querySelectorAll('[class*="overflow-auto"]').forEach(el => {
if (el.clientWidth > 50 && el.clientHeight > 50) el.style.paddingBottom = '64px';
});
}
// 云下载行内"⋮"菜单(全局浮层单例)
let cloudPop = null;
let cloudPopRow = null;
function findActBtn(row, txt) {
let res = null;
row.querySelectorAll('div,button,a,span').forEach(el => {
if (!res && (el.textContent || '').trim() === txt) res = el;
});
return res;
}
function findRow(pBtn) {
let cur = pBtn;
for (let d = 0; d < 8 && cur; d++) {
cur = cur.parentElement;
if (!cur) break;
if (cur.querySelector('[class*="truncate"]')) return cur;
}
return null;
}
function findActionArea(pBtn) {
let cur = pBtn;
for (let d = 0; d < 6 && cur; d++) {
cur = cur.parentElement;
if (!cur) break;
const r = cur.getBoundingClientRect();
if (r.width <= 0) continue;
let hasCopy = false, hasDel = false;
cur.querySelectorAll('div,button,a,span').forEach(el => {
const t = (el.textContent || '').trim();
if (t === '复制' && el.getBoundingClientRect().width > 0) hasCopy = true;
if (t === '删除' && el.getBoundingClientRect().width > 0) hasDel = true;
});
if (hasCopy && hasDel) return cur;
}
return null;
}
function hideCloudPop() {
if (cloudPop) cloudPop.style.display = 'none';
cloudPopRow = null;
}
// 云下载页每行:注入"⋮"菜单(复制/删除),隐藏原生操作列
function setupCloudRows() {
if (location.pathname.indexOf('/storage/clouddownload') !== 0) return;
if (!cloudPop) {
cloudPop = document.createElement('div');
cloudPop.className = 'vt115-rowpop';
['复制', '删除'].forEach((txt, i) => {
const item = document.createElement('div');
item.className = 'vt115-rowpop-item' + (i === 1 ? ' danger' : '');
item.textContent = txt;
item.addEventListener('click', () => {
if (cloudPopRow) {
const btn = findActBtn(cloudPopRow, txt);
if (btn) btn.click();
}
hideCloudPop();
});
cloudPop.appendChild(item);
});
document.body.appendChild(cloudPop);
document.addEventListener('click', e => {
if (!e.target.closest('.vt115-rowpop') && !e.target.closest('.vt115-rowmenu')) hideCloudPop();
}, true);
}
// 1) 定位任务行:从文件名叶子(truncate + 长哈希文本)向上找含固定列的行容器
const seen = new Set();
document.querySelectorAll('[class*="truncate"]').forEach(leaf => {
const txt = (leaf.textContent || '').trim();
if (txt.length < 2) return;
let row = leaf;
for (let d = 0; d < 6 && row; d++) {
row = row.parentElement;
if (!row) break;
const cls = (typeof row.className === 'string') ? row.className : '';
if (cls.includes('px-4') && row.children.length >= 4) break;
}
if (!row || seen.has(row) || row.dataset.vtMenu) return;
// 排除弹窗/对话框内的行(如"添加云下载"弹窗的保存路径行,避免误伤弹窗界面)
if (row.closest('[role="dialog"], [class*="modal"], [class*="Modal"], [class*="dialog"]')) return;
// 任务行特征校验:必须含进度列(w-24)或操作列(w-44),避免误伤其他区域
let isTaskRow = false;
Array.from(row.children).forEach(k => {
const ck = (typeof k.className === 'string') ? k.className : '';
if (ck.includes('w-24') || ck.includes('w-44')) isTaskRow = true;
});
if (!isTaskRow) return;
seen.add(row);
row.dataset.vtMenu = '1';
// 提取文件夹图标(原生行首列内含 svg/img 图标)
let iconEl = null;
const nw0 = row.children[0];
if (nw0) {
const svg = nw0.querySelector('svg');
if (svg) iconEl = svg.cloneNode(true);
else {
const img = nw0.querySelector('img');
if (img) iconEl = img.cloneNode(true);
}
}
// 提取文件名与进度/大小信息
const fileName = leaf.textContent.trim();
let progTxt = '';
let sizeTxt = '';
Array.from(row.children).forEach(k => {
const cls = (typeof k.className === 'string') ? k.className : '';
if (cls.includes('w-24')) progTxt = (k.textContent || '').trim(); // 进度列
else if (cls.includes('w-16')) sizeTxt = (k.textContent || '').trim(); // 大小列
else if (cls.includes('w-44')) k.dataset.vtHide = '1'; // 操作列
});
if (sizeTxt) progTxt = progTxt ? (sizeTxt + ' · ' + progTxt) : sizeTxt;
// 隐藏 115 原生行内所有列(布局不可靠,重建为 App 风格行)
Array.from(row.children).forEach(k => { k.style.display = 'none'; });
row.style.display = 'flex';
row.style.alignItems = 'center';
row.style.overflow = 'hidden';
const inner = document.createElement('div');
// 行内内容限制在视口宽度内,保证行尾"⋮"固定在屏幕右缘可见可点(行在900px布局中靠左显示)
inner.style.cssText = 'display:flex;align-items:center;width:' + Math.max(60, window.innerWidth - 32) + 'px;min-width:0;overflow:hidden;';
if (iconEl) {
iconEl.style.cssText = 'width:30px;height:30px;flex-shrink:0;margin-right:10px;';
inner.appendChild(iconEl);
}
const left = document.createElement('div');
left.style.cssText = 'flex:1 1 auto;min-width:0;overflow:hidden;display:flex;flex-direction:column;justify-content:center;';
const nameEl = document.createElement('div');
nameEl.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#E6EBF2;font-size:14px;line-height:1.4;';
nameEl.textContent = fileName;
left.appendChild(nameEl);
if (progTxt) {
const sub = document.createElement('div');
sub.style.cssText = 'font-size:11px;color:#8A99A8;line-height:1.3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
sub.textContent = progTxt;
left.appendChild(sub);
}
inner.appendChild(left);
// 行尾"⋮"菜单按钮
const menuBtn = document.createElement('div');
menuBtn.className = 'vt115-rowmenu';
menuBtn.textContent = '⋮';
menuBtn.addEventListener('click', e => {
e.stopPropagation();
cloudPopRow = row;
const r = menuBtn.getBoundingClientRect();
const pw = cloudPop.offsetWidth || 110;
const left2 = Math.max(8, Math.min(r.right - pw, window.innerWidth - pw - 8));
const top = Math.min(r.bottom + 4, window.innerHeight - (cloudPop.offsetHeight || 140) - 8);
cloudPop.style.left = left2 + 'px';
cloudPop.style.top = Math.max(8, top) + 'px';
cloudPop.style.display = 'block';
});
inner.appendChild(menuBtn);
row.appendChild(inner);
});
// 2) 再隐藏原生操作区:"操作"列表头、行操作区(暂停/复制/删除)、"更多"按钮
document.querySelectorAll('div').forEach(el => {
const cls = (typeof el.className === 'string') ? el.className : '';
if (!cls.includes('w-44') || !cls.includes('flex-shrink-0')) return;
const t = (el.textContent || '').trim();
if ((t === '操作' || /暂停.*复制.*删除/.test(t)) && el.getBoundingClientRect().width > 0) {
el.style.display = 'none';
}
});
document.querySelectorAll('[data-vt-hide]').forEach(el => { el.style.display = 'none'; });
document.querySelectorAll('button,div,a,span').forEach(el => {
if ((el.textContent || '').trim() === '更多' && el.getBoundingClientRect().width > 0 && el.getBoundingClientRect().width < 100) {
el.style.display = 'none';
}
});
// 3) 隐藏"添加云下载"弹窗中的配额信息(本月配额/购买配额),限定在弹窗遮罩内
document.querySelectorAll('div').forEach(mask => {
const mc = (typeof mask.className === 'string') ? mask.className : '';
if (!/fixed inset-0/.test(mc) && !/bg-black\/20/.test(mc)) return;
mask.querySelectorAll('div,span,p,button,a').forEach(el => {
if (el.children.length) return;
const t = (el.textContent || '').trim();
if (t === '购买配额' || t.indexOf('本月配额') === 0) el.style.display = 'none';
});
// 隐藏"最近使用"及后面的文件夹选项(保留"保存到:云下载")
let recent = null;
mask.querySelectorAll('div,span,p,button,a').forEach(el => {
if (!el.children.length && (el.textContent || '').trim() === '最近使用') recent = el;
});
if (recent) {
let cur = recent;
for (let d = 0; d < 5 && cur; d++) {
cur = cur.parentElement;
if (!cur) break;
const t2 = (cur.textContent || '').trim();
if (getComputedStyle(cur).display === 'flex' && t2.includes('最近使用') && !t2.includes('保存到')) {
cur.style.display = 'none';
break;
}
}
}
});
}
// 全部文件页行尾"⋮"菜单(下载/移动/复制/重命名/星标/删除)
let afPop = null;
let afPopRow = null;
// 触发操作:勾选该行后,点击顶部操作栏中对应文本的按钮
function clickTopActBtn(label, triesMax, topLimit) {
return new Promise(resolve => {
let tries = 0;
const timer = setInterval(() => {
tries++;
// 选择匹配文本的最内层可见元素(外层容器无点击处理器,点击无效)
let best = null;
document.querySelectorAll('button,div,a,span').forEach(el => {
if (el.offsetParent === null) return;
if ((el.textContent || '').trim() !== label) return;
const r = el.getBoundingClientRect();
if (r.width <= 0 || r.top >= (topLimit || 250)) return;
if (!best || el.children.length < best.children.length) best = el;
});
if (best) { clearInterval(timer); best.click(); resolve(true); }
else if (tries > (triesMax || 25)) { clearInterval(timer); resolve(false); }
}, 100);
});
}
function clickTopMoreBtn(triesMax) {
// 用"已选中 N 项"定位操作栏,再点操作栏内的"更多"(避免点错顶部右侧的"更多")
return new Promise(resolve => {
let tries = 0;
const timer = setInterval(() => {
tries++;
let moreBtn = null;
document.querySelectorAll('div,span').forEach(el => {
if (el.offsetParent === null) return;
const t = (el.textContent || '').trim();
if (!/已选中\s*\d+\s*项/.test(t) || t.length > 20) return;
let bar = el;
for (let d = 0; d < 6 && bar; d++) {
bar = bar.parentElement;
if (!bar) break;
if (bar.textContent.indexOf('下载') >= 0 && bar.querySelectorAll('*').length > 10) break;
}
if (!bar) return;
let best = null;
bar.querySelectorAll('button,div,a,span').forEach(k => {
if (k.offsetParent === null) return;
if ((k.textContent || '').trim() !== '更多') return;
if (k.getBoundingClientRect().width <= 0) return;
if (!best || k.children.length < best.children.length) best = k;
});
if (best && !moreBtn) moreBtn = best;
});
if (moreBtn) { clearInterval(timer); moreBtn.click(); resolve(true); }
else if (tries > (triesMax || 25)) { clearInterval(timer); resolve(false); }
}, 100);
});
}
async function triggerAllfilesAction(row, label) {
const cb = row.querySelector('[class*="checkbox-area"]');
if (cb) {
const ccls = (typeof cb.className === 'string') ? cb.className : '';
const checked = cb.getAttribute('aria-checked') === 'true' || /checked/.test(ccls) || !!cb.querySelector('[class*="checked"]');
if (!checked) cb.click();
}
await new Promise(res => setTimeout(res, 300)); // 等勾选与操作栏渲染
if (label === '星标' || label === '取消星标') {
// 星标/取消星标在操作栏"更多"菜单里 → 展开"更多"后点击目标项
// 菜单项在操作栏下方(y≈300),放宽高度限制
const ok = await clickTopMoreBtn(25);
if (!ok) return;
await new Promise(res => setTimeout(res, 700)); // 等菜单弹出
clickTopActBtn(label, 20, 600);
} else {
clickTopActBtn(label, 25);
}
}
// ===== 单文件重命名(勾选行 + 派发 F2 → 115 原生"重命名文件"对话框)=====
// 115 新版 hover 文件行出现单文件操作栏,其中的"重命名"打开原生单文件重命名对话框。
// 手机端无法 hover(JS 派发 MouseEvent 不触发 CSS :hover),但 115 支持 F2 快捷键:
// 勾选文件后派发 F2 keydown 即弹出原生"重命名文件"对话框(输入新名 + 确认)。
function doSingleRename(row) {
const cb = row.querySelector('[class*="checkbox-area"]');
if (cb) {
const ccls = (typeof cb.className === 'string') ? cb.className : '';
const checked = cb.getAttribute('aria-checked') === 'true' || /checked/.test(ccls) || !!cb.querySelector('[class*="checked"]');
if (!checked) cb.click();
}
setTimeout(() => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', code: 'F2', keyCode: 113, which: 113, bubbles: true, cancelable: true }));
document.dispatchEvent(new KeyboardEvent('keyup', { key: 'F2', code: 'F2', keyCode: 113, which: 113, bubbles: true, cancelable: true }));
}, 350);
}
// 全部文件页/星标文件页:行尾"⋮"菜单(移动/复制/重命名/星标/删除),隐藏大小/类型/修改时间列
function setupAllfilesRows() {
if (location.pathname.indexOf('/storage/allfiles') !== 0 && location.pathname.indexOf('/storage/starredfiles') !== 0) return;
// 全部文件页/星标文件页:隐藏底部"已加载 N / M 个"文本(保留"加载更多"翻页按钮;文件过多时同样生效)
document.querySelectorAll('span').forEach(sp => {
const t = (sp.textContent || '').trim();
if (/^已加载\s*\d+\s*\/\s*\d+\s*个$/.test(t)) sp.style.display = 'none';
});
// 全部文件页/星标文件页:底部"加载更多"按钮缩小并改为"更多"
document.querySelectorAll('button').forEach(btn => {
const t = (btn.textContent || '').trim();
if (t !== '加载更多') return;
let leaf = btn;
while (leaf.children.length > 0) leaf = leaf.children[0];
if ((leaf.textContent || '').trim() === '加载更多') leaf.textContent = '更多';
btn.style.height = '24px';
btn.style.padding = '0 10px';
btn.style.fontSize = '12px';
btn.style.lineHeight = '24px';
btn.style.minWidth = '46px';
});
if (!afPop) {
afPop = document.createElement('div');
afPop.className = 'vt115-rowpop';
['移动', '复制', '重命名', '星标', '删除'].forEach((txt, i) => {
const item = document.createElement('div');
item.className = 'vt115-rowpop-item' + (i === 4 ? ' danger' : '');
item.textContent = txt;
if (txt === '星标') item.dataset.act = 'star';
item.addEventListener('click', () => {
if (afPopRow) {
if (item.textContent === '重命名') {
doSingleRename(afPopRow); // 单文件重命名(模拟 hover → 115 原生"重命名文件"对话框)
} else {
triggerAllfilesAction(afPopRow, item.textContent);
}
}
afPop.style.display = 'none';
afPopRow = null;
});
afPop.appendChild(item);
});
document.body.appendChild(afPop);
document.addEventListener('click', e => {
if (!e.target.closest('.vt115-rowpop') && !e.target.closest('.vt115-rowmenu')) {
afPop.style.display = 'none';
afPopRow = null;
}
}, true);
}
const seen = new Set();
// 表头同步隐藏"大小/类型/修改时间"列(与行内一致)
document.querySelectorAll('[class*="checkbox-area"]').forEach(cbh => {
const rh = cbh.getBoundingClientRect();
if (rh.width <= 0 || rh.top >= 150) return;
let hrow = cbh;
for (let d = 0; d < 6 && hrow; d++) {
hrow = hrow.parentElement;
if (!hrow) break;
const hc = (typeof hrow.className === 'string') ? hrow.className : '';
if (hc.includes('px-4') || (hc.includes('flex') && hrow.children.length >= 5)) break;
}
if (!hrow || hrow.dataset.vtAfHead) return;
if (/文件名/.test(hrow.textContent || '')) {
hrow.dataset.vtAfHead = '1';
Array.from(hrow.children).forEach(k => {
const ck = (typeof k.className === 'string') ? k.className : '';
if (ck.includes('w-24') || ck.includes('w-32')) k.style.display = 'none';
});
}
});
// 清理孤儿 ⋮ 按钮:对应行已不存在/行结构已变(React 重建新行节点)→ 移除,防重复累积
document.querySelectorAll('.vt115-rowmenu-fixed').forEach(mb => {
if (!mb._vtRow || !mb._vtRow.isConnected) { mb.remove(); return; }
if (!mb._vtRow.querySelector('[class*="checkbox-area"]')) mb.remove();
});
document.querySelectorAll('[class*="checkbox-area"]').forEach(cb => {
const r0 = cb.getBoundingClientRect();
if (r0.width <= 0 || r0.top < 150) return; // 跳过表头全选框
let row = cb;
for (let d = 0; d < 7 && row; d++) {
row = row.parentElement;
if (!row) break;
const cls = (typeof row.className === 'string') ? row.className : '';
if (cls.includes('px-4') || (cls.includes('flex') && cls.includes('items-center') && row.children.length >= 5)) break;
}
// 防重:dataset 标记 + body 按钮双检查——行有标记且按钮存在则跳过;标记在但按钮缺失(被外部移除)则清除标记允许重建
if (!row) return;
if (row.dataset.vtAfMenu) {
let hasBtn = false;
document.querySelectorAll('.vt115-rowmenu-fixed').forEach(mb => { if (mb._vtRow === row) hasBtn = true; });
if (hasBtn) return;
delete row.dataset.vtAfMenu;
}
if (row.closest('[role="dialog"], [class*="modal"], [class*="Modal"], [class*="dialog"]')) return;
// 跳过表头行
const rt = (row.textContent || '').trim();
if (rt.includes('文件名') && (rt.includes('大小') || rt.includes('修改时间'))) return;
seen.add(row);
row.dataset.vtAfMenu = '1';
// 提取勾选框(保留原生点击)、图标列、文件名区——只做样式调整,【不移动子元素】
const cbEl = row.querySelector('[class*="checkbox-area"]');
const iconEl = row.children[2];
const nameEl = row.children[3];
// 隐藏行内其余子级(拖拽手柄/大小/类型/修改时间)——display:none 保留在 DOM 中,不破坏 115 自身节点结构
Array.from(row.children).forEach(k => {
if (k === cbEl || k === iconEl || k === nameEl) return;
k.style.display = 'none';
});
// 原位样式调整:图标列固定宽、文件名列弹性收缩 + 省略(不移动节点)
if (iconEl) {
iconEl.style.cssText = 'flex-shrink:0;width:34px;display:flex;align-items:center;justify-content:center;';
}
if (nameEl) {
nameEl.style.cssText = 'flex:1 1 auto;min-width:0;overflow:hidden;';
// 文件名内部文本强制单行省略,防止长名撑开行
const nameTxt = nameEl.querySelector('[class*="file-name"], [class*="name"]') || nameEl;
nameTxt.style.whiteSpace = 'nowrap';
nameTxt.style.overflow = 'hidden';
nameTxt.style.textOverflow = 'ellipsis';
nameTxt.style.minWidth = '0';
}
// 行容器:收缩到视口内 + 禁横向溢出(不建 inner、不移动子元素,避免破坏 115 的 DOM diff)
row.style.minWidth = '0';
row.style.maxWidth = '100%';
// 行宽 = 视口宽 - 行左侧 → 行右缘恒定在屏幕右缘(滚动时行左移、行宽同步增大,右缘不动),行内 ⋮ 按钮贴屏幕右缘
row.style.width = Math.max(60, window.innerWidth - row.getBoundingClientRect().left - 8) + 'px';
row.style.overflow = 'hidden';
// ⋮ 菜单按钮:fixed 钉在屏幕右缘(挂 body,不参与 115 行 DOM,React 重渲染不会清除;位置由 positionMenus 每帧跟随)
const menuBtn = document.createElement('div');
menuBtn.className = 'vt115-rowmenu vt115-rowmenu-fixed';
menuBtn.textContent = '⋮';
menuBtn._vtRow = row;
menuBtn.addEventListener('click', e => {
e.stopPropagation();
// 再次点击已打开的菜单 → 退出菜单
if (afPop.style.display === 'block' && afPopRow === row) {
afPop.style.display = 'none';
afPopRow = null;
return;
}
afPopRow = row;
// 动态更新星标菜单项:该行已星标 → 显示"取消星标"
const isStarred = !!row.querySelector('[title="已星标"]');
afPop.querySelectorAll('.vt115-rowpop-item').forEach(it => {
if (it.dataset.act === 'star') it.textContent = isStarred ? '取消星标' : '星标';
});
const r = menuBtn.getBoundingClientRect();
const pw = afPop.offsetWidth || 110;
const left = Math.max(8, Math.min(r.right - pw, window.innerWidth - pw - 8));
const top = Math.min(r.bottom + 4, window.innerHeight - (afPop.offsetHeight || 280) - 8);
afPop.style.left = left + 'px';
afPop.style.top = Math.max(8, top) + 'px';
afPop.style.display = 'block';
});
row.appendChild(menuBtn);
menuBtn.style.display = 'flex';
});
}
// 行内 absolute 按钮无需 JS 定位(随行滚动);此函数仅兜底显示/隐藏未挂载的孤儿按钮
function positionMenus() {
if (location.pathname.indexOf('/storage/allfiles') !== 0 && location.pathname.indexOf('/storage/starredfiles') !== 0) return;
document.querySelectorAll('.vt115-rowmenu-fixed').forEach(mb => {
const row = mb._vtRow;
if (!row || !row.isConnected) { mb.style.display = 'none'; return; }
const rr = row.getBoundingClientRect();
if (rr.width <= 0 || rr.height <= 0) { mb.style.display = 'none'; return; }
mb.style.display = 'flex';
});
}
// ── 视频文件 → 旧版播放界面(115vod.com)接管 ──────────────────────────
// 双保险:
// ① 点击拦截:capture 阶段拦截视频行(图标为 /icons/types/{mp4,mkv,...}.svg)的
// 文件名/图标点击,阻止 115 新版播放 → 经文件列表 API(aps.115.com/natsort)
// 按文件名取 pickcode → 跳转旧版播放页。适用于手机端点击视频行弹出播放器(不跳 URL)场景。
// ② URL 检测:hook history.pushState/replaceState + popstate + 400ms 轮询,
// 一旦 URL 进入 /players/video/{pickcode}(SPA 跳转/新标签/直达)→ 跳转旧版播放页。
// pickcode 按目录缓存 60s;API 失败时静默(已阻止默认,用户可再次点击重试)。
// 跳转后脚本不在 115vod.com 运行(@match 不含该域),不会循环。
const VT_VIDEO_ICON = /\/icons\/types\/(mp4|mkv|avi|wmv|flv|mov|ts|rmvb|rm|m4v|3gp|ogv|webm)\.svg/i;
let vtVideoTaken = false;
const vtVideoCache = {}; // cid -> { map: {文件名: pickcode}, ts: 时间戳 }
function vtGetCid() {
const m = location.search.match(/[?&]cid=(\d+)/);
return m ? m[1] : '';
}
function vtFetchMap(cid) {
const c = vtVideoCache[cid];
if (c && Date.now() - c.ts < 60000) return Promise.resolve(c.map);
return fetch('https://aps.115.com/natsort/files.php?aid=1&cid=' + cid + '&o=user_00&offset=0&show_dir=1&limit=1000&format=json', { credentials: 'include' })
.then(r => r.json())
.then(j => {
const map = {};
(j.data || []).forEach(f => { if (f.n && f.pc) map[f.n] = f.pc; });
vtVideoCache[cid] = { map: map, ts: Date.now() };
return map;
})
.catch(() => { return null; });
}
function vtJumpPickcode(pc) {
if (pc) location.href = 'https://115vod.com/?pickcode=' + pc + '&share_id=0';
}
// ── 播放模式开关:旧版播放(115vod 接管,默认)/ 新版播放(115 原生)──
let vtPlayMode = 'old'; // 'old' | 'new'
try { vtPlayMode = localStorage.getItem('vt115PlayMode') || 'old'; } catch (e) { }
function vtModeIsOld() { return vtPlayMode === 'old'; }
function vtSetupModeSwitch() {
try {
if (document.getElementById('vt115-mode-switch')) return;
const btn = document.createElement('div');
btn.id = 'vt115-mode-switch';
btn.style.cssText = 'position:fixed;top:6px;right:6px;z-index:2147483647;display:flex;align-items:stretch;border-radius:14px;overflow:hidden;box-shadow:0 1px 6px rgba(0,0,0,.3);font-size:11px;line-height:1;cursor:pointer;user-select:none;';
const mk = (txt, mode) => {
const s = document.createElement('span');
s.textContent = txt;
s.dataset.mode = mode;
s.style.cssText = 'padding:5px 9px;font-weight:600;color:#fff;';
return s;
};
const segNew = mk('新版', 'new');
const segOld = mk('旧版', 'old');
btn.appendChild(segNew);
btn.appendChild(segOld);
(document.documentElement || document.body).appendChild(btn);
const paint = () => {
segNew.style.background = vtPlayMode === 'new' ? 'rgba(76,141,255,.96)' : 'rgba(20,26,36,.78)';
segOld.style.background = vtPlayMode === 'old' ? 'rgba(76,141,255,.96)' : 'rgba(20,26,36,.78)';
};
paint();
btn.addEventListener('click', e => {
const seg = e.target.closest('span[data-mode]');
if (!seg) return;
vtPlayMode = seg.dataset.mode;
try { localStorage.setItem('vt115PlayMode', vtPlayMode); } catch (err) { }
paint();
});
} catch (e) { }
}
function vtTryBlock(e) {
// 新版播放模式下不接管,完全放行
if (!vtModeIsOld()) return;
// 115 播放可能由 click / pointerdown / mousedown 触发,全部拦截(防重只处理一次)
if (location.pathname.indexOf('/storage/allfiles') !== 0 && location.pathname.indexOf('/storage/starredfiles') !== 0) return;
if (e.target.closest('.vt115-rowmenu, .vt115-rowpop, .vt115-fab, .vt115-tabbar')) return;
// 复选框点击 → 放行(保留选中功能)
if (e.target.closest('input, [class*="checkbox"]')) return;
// 拖拽排序手柄 → 放行(保持可拖动)
if (e.target.closest('[class*="drag"], [class*="handle"], [data-role="drag"]')) return;
let row = e.target.closest('[class*="checkbox-area"]');
if (!row) return;
for (let d = 0; d < 7 && row; d++) {
row = row.parentElement;
if (!row) break;
const cls = (typeof row.className === 'string') ? row.className : '';
if (cls.includes('px-4') || (cls.includes('flex') && cls.includes('items-center') && row.children.length >= 5)) break;
}
if (!row) return;
const iconEl = row.children[2];
if (!iconEl) return;
const img = iconEl.querySelector('img');
if (!img || !VT_VIDEO_ICON.test(img.src || '')) return; // 非视频 → 放行(文件夹/其他文件正常导航)
// 视频行内任意位置点击均拦截(用户可能点缩略图/文件名/时长区;复选框与⋮已排除)
// pointerdown / mousedown / click 多触发防重:同一行 1.5s 内只处理一次
const ts = row.dataset.vtBlockTs ? +row.dataset.vtBlockTs : 0;
if (Date.now() - ts < 1500) {
e.preventDefault();
e.stopPropagation();
return;
}
row.dataset.vtBlockTs = String(Date.now());
e.preventDefault();
e.stopPropagation();
const cid = vtGetCid();
if (!cid) return;
const rowText = (row.textContent || '').trim();
vtFetchMap(cid).then(map => {
if (!map) return; // API 失败:已阻止默认,用户可再次点击重试
for (const k in map) {
if (rowText.includes(k)) { vtJumpPickcode(map[k]); break; }
}
});
}
function vtCheckPlayUrl() {
if (!vtModeIsOld()) return;
const m = location.pathname.match(/^\/players\/video\/([^/]+)/);
if (m && m[1]) vtJumpPickcode(m[1]);
}
function setupVideoTakeover() {
if (vtVideoTaken) return;
vtVideoTaken = true;
vtCheckPlayUrl();
// 播放模式开关:新版 / 旧版(右上角常驻,点击切换,localStorage 持久化)
vtSetupModeSwitch();
// 拦截新窗口播放:115 播放器以 window.open 打开播放页。
// 必须用 unsafeWindow(主世界)hook——油猴隔离世界的 window.open 覆盖对页面无效
const W = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
const origOpen = W.open;
W.open = function (url, name, features) {
if (vtModeIsOld()) {
const u = url ? String(url) : '';
const m = u.match(/\/players\/video\/([^/?#]+)/);
if (m && m[1]) { vtJumpPickcode(m[1]); return null; }
}
return origOpen.apply(this, arguments);
};
// 点击任何 a 标签且 href 指向播放页 → 拦截跳旧版(覆盖 115 用链接打开播放器的场景)
document.addEventListener('click', function (e) {
if (!vtModeIsOld()) return;
const a = e.target.closest ? e.target.closest('a') : null;
if (!a) return;
const href = a.getAttribute('href') || a.href || '';
if (href.indexOf('/players/video/') >= 0 || href.indexOf('players/video') >= 0) {
const m = href.match(/\/players\/video\/([^/?#]+)/);
if (m && m[1]) {
e.preventDefault();
e.stopPropagation();
vtJumpPickcode(m[1]);
}
}
}, true);
// 多事件拦截:115 播放可能由 pointerdown / mousedown / click 触发
document.addEventListener('pointerdown', vtTryBlock, true);
document.addEventListener('mousedown', vtTryBlock, true);
document.addEventListener('click', vtTryBlock, true);
const origPush = history.pushState;
const origReplace = history.replaceState;
history.pushState = function () {
const r = origPush.apply(this, arguments);
vtCheckPlayUrl();
return r;
};
history.replaceState = function () {
const r = origReplace.apply(this, arguments);
vtCheckPlayUrl();
return r;
};
window.addEventListener('popstate', vtCheckPlayUrl);
setInterval(vtCheckPlayUrl, 400);
}
function apply() {
setupVideoTakeover(); // 视频文件点击 → 旧版播放(115vod.com),capture 阶段全局拦截,仅注册一次
// 隐藏右上角"切换旧版"按钮(fixed top-0 right-0 链接)
document.querySelectorAll('a').forEach(a => {
if ((a.textContent || '').trim() === '切换旧版') a.style.display = 'none';
});
// 回收站页:隐藏"全选""清空回收站"按钮与"列表/网格"视图切换(保留"还原""删除")
if (location.pathname.indexOf('/storage/recyclebin') === 0) {
document.querySelectorAll('button').forEach(btn => {
const t = (btn.textContent || '').trim();
if (t === '全选' || t === '清空回收站') btn.style.display = 'none';
});
document.querySelectorAll('[title="列表视图"], [title="网格视图"]').forEach(v => {
let c = v;
for (let d = 0; d < 3 && c; d++) {
const cls = (typeof c.className === 'string') ? c.className : '';
if (cls.indexOf('bg-gray-100') >= 0) { c.style.display = 'none'; break; }
c = c.parentElement;
}
});
}
// 云下载页:App 风格改造——隐藏原"添加云下载"按钮,
// 右下角注入"+"号浮动按钮(点击触发原按钮,保持原生添加流程)
const isCloudDL = location.pathname.indexOf('/storage/clouddownload') === 0;
if (isCloudDL) {
let round = 0;
while (round < 3) {
round++;
let hit = false;
document.querySelectorAll('div,button,a,span').forEach(el => {
if ((el.textContent || '').trim() === '添加云下载' && el.getBoundingClientRect().width > 0) {
el.style.display = 'none';
hit = true;
}
});
if (!hit) break;
}
if (!document.querySelector('.vt115-fab')) {
const fab = document.createElement('div');
fab.className = 'vt115-fab';
fab.textContent = '+';
fab.addEventListener('click', () => {
let btn = null;
document.querySelectorAll('div,button,a,span').forEach(el => {
if (!btn && (el.textContent || '').trim() === '添加云下载') btn = el;
});
if (btn) btn.click();
});
document.body.appendChild(fab);
}
} else if (document.querySelector('.vt115-fab')) {
document.querySelector('.vt115-fab').remove();
}
setupCloudRows();
setupAllfilesRows();
fitDialogs();
// 注入底部功能导航(幂等:已注入则跳过)
if (!document.querySelector('.vt115-tabbar')) {
const bar = document.createElement('div');
bar.className = 'vt115-tabbar';
SIDEBAR_ITEMS.forEach(it => {
const a = document.createElement('a');
a.href = it.href;
a.className = 'vt115-tab-item';
a.innerHTML = `
${it.name}`;
bar.appendChild(a);
});
bar.id = 'vt-tabbar';
document.body.appendChild(bar);
}
updateActive();
fixHScroll();
padContent();
}
// 移动/复制弹窗对话框移动端适配:CSS vw 在无 viewport meta 的手机上等于布局视口宽(900px),限宽无效,
// 改用 JS 按真实屏幕宽强制限宽(取 innerWidth/screen.width/clientWidth 的最小值,兼容整页缩放与溢出两种模式)。
// 根因:目录内出现长文件名会把对话框/页面横向撑大(与目录层级无关)——故需识别对话框容器并限宽,
// 同时把内部所有超宽后代(尤其长文件名文本)限宽+省略,防止撑开容器。
function fitDialogs() {
const realW = Math.min(window.innerWidth, window.screen.width || window.innerWidth, document.documentElement.clientWidth);
const targetW = Math.max(240, realW - 16);
const seen = new Set();
// 对话框特征容器:固定宽 420px 的旧版容器 + rounded-lg shadow-xl 真实容器 + max-h-[90vh]
const sel = '[class*="w-[420px]"][class*="rounded-xl"], [class*="rounded-lg"][class*="shadow-xl"], [class*="max-h-[90vh]"]';
document.querySelectorAll(sel).forEach(dlg => {
if (seen.has(dlg)) return;
const r = dlg.getBoundingClientRect();
if (r.width <= 0 || r.width < 200) return;
// 只处理移动/复制类对话框(含"新建文件夹"按钮或"移动到/复制到"标题),避免误伤普通卡片
const t = dlg.textContent || '';
if (t.indexOf('新建文件夹') < 0 && t.indexOf('移动到') < 0 && t.indexOf('复制到') < 0 && t.indexOf('移动文件') < 0) return;
seen.add(dlg);
if (dlg.dataset.vtFit !== String(targetW)) {
dlg.dataset.vtFit = String(targetW);
dlg.style.width = targetW + 'px';
dlg.style.maxWidth = targetW + 'px';
dlg.style.overflowX = 'hidden';
}
// 内部防撑大:每次执行(幂等,处理新目录出现的长文件名)
fitDialogContent(dlg, targetW);
});
// 已展开的嵌套目录面板(left-full):仅限宽可见/可交互的,未展开(opacity-0/pointer-events-none)不动
document.querySelectorAll('[class*="left-full"]').forEach(p => {
const cs = getComputedStyle(p);
const r = p.getBoundingClientRect();
if (r.width <= 0 || r.width <= realW) return;
if (cs.opacity === '0' || cs.pointerEvents === 'none' || cs.display === 'none') return;
if (p.dataset.vtFit !== String(targetW)) {
p.dataset.vtFit = String(targetW);
p.style.width = targetW + 'px';
p.style.maxWidth = targetW + 'px';
p.style.overflowX = 'hidden';
}
fitDialogContent(p, targetW);
});
}
// 对话框内部防撑大:所有宽度超过 targetW 的可见后代限宽;长文件名文本省略;flex 伸缩项允许收缩
function fitDialogContent(dlg, targetW) {
const all = dlg.querySelectorAll('*');
for (let i = 0; i < all.length; i++) {
const inner = all[i];
const ir = inner.getBoundingClientRect();
if (ir.width <= 0) continue;
if (ir.width > targetW) {
if (inner.children.length === 0) {
inner.style.maxWidth = targetW + 'px';
inner.style.whiteSpace = 'nowrap';
inner.style.overflow = 'hidden';
inner.style.textOverflow = 'ellipsis';
} else {
inner.style.maxWidth = '100%';
inner.style.overflowX = 'hidden';
}
}
const cs = getComputedStyle(inner);
if (ir.width > 60 && (cs.flexGrow === '1' || cs.flexShrink === '1' || cs.flexBasis === '0%' || cs.flexBasis === '0px')) {
inner.style.minWidth = '0';
inner.style.maxWidth = '100%';
}
}
// 文件名/文本类元素补省略(min-width:0 + ellipsis),防止 flex 行内长名撑开
dlg.querySelectorAll('[class*="file-name"],[class*="name"],[class*="title"]').forEach(n => {
const cs2 = getComputedStyle(n);
if (cs2.whiteSpace !== 'nowrap') {
n.style.whiteSpace = 'nowrap';
n.style.overflow = 'hidden';
n.style.textOverflow = 'ellipsis';
n.style.minWidth = '0';
}
});
}
const mo = new MutationObserver(() => { apply(); bindListScroll(); });
mo.observe(document.documentElement, { childList: true, subtree: true });
apply();
// ⋮ 按钮在文件行内(absolute 随行滚动):滚动时高频重建行内按钮(React 重渲染可能清除行内节点)+ 兜底显示
let menuTick = false;
const menuRefresh = () => {
if (menuTick) return;
menuTick = true;
requestAnimationFrame(() => {
setupAllfilesRows();
positionMenus();
menuTick = false;
});
};
window.addEventListener('scroll', menuRefresh, { passive: true });
const bindListScroll = () => {
document.querySelectorAll('[class*="overflow-y-auto"]').forEach(el => {
if (!el.__vtMenuBound) {
el.__vtMenuBound = true;
el.addEventListener('scroll', menuRefresh, { passive: true });
}
});
};
// SPA 路由跳转后刷新高亮 + 兜底修复(含 ⋮ 按钮重建)
setInterval(() => { updateActive(); fixHScroll(); padContent(); setupAllfilesRows(); positionMenus(); }, 800);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupSidebar, { once: true });
} else {
setupSidebar();
}
})();