// ==UserScript==
// @name 115网盘 - 手机端适配
// @namespace https://github.com/cxf35/Video-Takeover
// @version 1.38.17
// @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
// @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: 56px; 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 {
flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 2px; color: #8A99A8; font-size: 11px; line-height: 1.2;
text-decoration: none; cursor: pointer;
}
.vt115-tab-item img { width: 22px; height: 22px; opacity: .85; }
.vt115-tab-item:hover { color: #B6C2D0; }
.vt115-tab-item.active { color: #4C8DFF; font-weight: 600; }
.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); }
/* 全部文件页行尾"⋮"按钮:fixed 钉在屏幕右缘(不参与 115 行布局,避免 DOM 结构被破坏) */
.vt115-rowmenu-fixed {
position: fixed; 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 个"文本(保留"加载更多"翻页按钮)
if (location.pathname.indexOf('/storage/starredfiles') === 0) {
document.querySelectorAll('span').forEach(sp => {
const t = (sp.textContent || '').trim();
if (/^已加载\s*\d+\s*\/\s*\d+\s*个$/.test(t)) sp.style.display = 'none';
});
}
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';
});
}
});
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;
}
if (!row || seen.has(row) || row.dataset.vtAfMenu) return;
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.overflow = 'hidden';
// ⋮ 菜单按钮:fixed 定位钉在屏幕右缘(不 append 到行内,不参与 115 行布局,DOM diff 兼容)
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';
});
document.body.appendChild(menuBtn);
});
positionMenus();
}
// 定位所有 fixed"⋮"按钮到屏幕右缘、对应行的垂直位置;行不可见/已移除则隐藏
function positionMenus() {
if (location.pathname.indexOf('/storage/allfiles') !== 0 && location.pathname.indexOf('/storage/starredfiles') !== 0) return;
const rightX = window.innerWidth - 44;
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 || rr.top > window.innerHeight || rr.bottom < 0) {
mb.style.display = 'none';
return;
}
mb.style.display = 'flex';
mb.style.left = rightX + 'px';
mb.style.top = Math.round(rr.top + rr.height / 2 - 18) + 'px';
});
}
function apply() {
// 隐藏右上角"切换旧版"按钮(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);
});
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(); });
mo.observe(document.documentElement, { childList: true, subtree: true });
apply();
// 滚动时即时重定位行尾"⋮"按钮
let scrollTick = false;
window.addEventListener('scroll', () => {
if (scrollTick) return;
scrollTick = true;
requestAnimationFrame(() => { positionMenus(); scrollTick = false; });
}, { passive: true });
// SPA 路由跳转后刷新高亮 + 兜底修复
setInterval(() => { updateActive(); fixHScroll(); padContent(); positionMenus(); }, 800);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', setupSidebar, { once: true });
} else {
setupSidebar();
}
})();