// ==UserScript== // @name NotePod++ FNOS 助手 // @namespace https://greasyfork.org/users/local // @version 1.0.0 // @description 为 FNOS 文件管理器添加 NotePod++ 编辑与新建文件辅助功能。 // @author local // @match http://*/* // @match https://*/* // @run-at document-start // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @downloadURL https://raw.githubusercontent.com/ifsherlock/My-Moviepilot-Script/main/NotePod%2B%2B%20FNOS%20%E5%8A%A9%E6%89%8B.user.js // @updateURL https://raw.githubusercontent.com/ifsherlock/My-Moviepilot-Script/main/NotePod%2B%2B%20FNOS%20%E5%8A%A9%E6%89%8B.user.js // ==/UserScript== (function () { 'use strict'; const DEFAULT_MATCH_PATTERN = 'fnos.net\n:5666,:5667'; const STORAGE_KEYS = { enabled: 'notepod_fnos_enabled', matchPattern: 'notepod_fnos_match_pattern' }; function gmGet(key, fallback) { if (typeof GM_getValue === 'function') return GM_getValue(key, fallback); const raw = localStorage.getItem(key); return raw === null ? fallback : JSON.parse(raw); } function gmSet(key, value) { if (typeof GM_setValue === 'function') GM_setValue(key, value); else localStorage.setItem(key, JSON.stringify(value)); } function parseMatchPattern(pattern) { return String(pattern || '') .split('\n') .flatMap((line) => line.split('#')[0].split('//')[0].split(',')) .map((item) => item.trim()) .filter(Boolean); } function hostMatchesCurrentPage(pattern) { const host = location.host; const keywords = parseMatchPattern(pattern); return keywords.length > 0 && keywords.some((keyword) => host.includes(keyword)); } function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function escapeHtmlAttr(value) { return escapeHtml(value).replace(/`/g, '`'); } function addStyle(css) { if (typeof GM_addStyle === 'function') GM_addStyle(css); else { const style = document.createElement('style'); style.textContent = css; (document.head || document.documentElement).appendChild(style); } } function installSettingsStyles() { if (document.getElementById('np-settings-style')) return; addStyle(` #np-settings-style { display: none; } .np-settings-modal { position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; padding: 18px; background: rgba(15, 23, 42, 0.45); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; } .np-settings-panel { width: min(560px, 100%); max-height: min(720px, calc(100vh - 36px)); overflow: auto; background: #fff; color: #1f2937; border-radius: 8px; box-shadow: 0 18px 60px rgba(15, 23, 42, 0.25); border: 1px solid #e5e7eb; } .np-settings-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 18px 20px 14px; border-bottom: 1px solid #eef0f3; } .np-settings-title { margin: 0; font-size: 18px; line-height: 1.3; font-weight: 700; color: #111827; } .np-settings-close { width: 32px; height: 32px; border: 0; border-radius: 6px; background: transparent; color: #6b7280; cursor: pointer; font-size: 22px; line-height: 1; } .np-settings-close:hover { background: #f3f4f6; color: #111827; } .np-settings-body { padding: 18px 20px 20px; } .np-settings-row { margin-bottom: 16px; } .np-settings-label { display: block; margin-bottom: 7px; font-size: 14px; font-weight: 650; color: #374151; } .np-settings-help { margin: 7px 0 0; color: #6b7280; font-size: 12px; line-height: 1.55; } .np-settings-textarea { width: 100%; min-height: 150px; box-sizing: border-box; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 6px; resize: vertical; color: #111827; font-size: 13px; line-height: 1.55; outline: none; font-family: Consolas, "Microsoft YaHei Mono", monospace; } .np-settings-textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14); } .np-switch-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 14px; border: 1px solid #e5e7eb; border-radius: 8px; background: #f9fafb; } .np-switch-copy strong { display: block; color: #111827; font-size: 14px; margin-bottom: 3px; } .np-switch-copy span { color: #6b7280; font-size: 12px; } .np-switch { position: relative; display: inline-flex; width: 46px; height: 26px; flex: 0 0 auto; } .np-switch input { opacity: 0; width: 0; height: 0; } .np-slider { position: absolute; inset: 0; cursor: pointer; background: #cbd5e1; border-radius: 999px; transition: .2s; } .np-slider:before { content: ""; position: absolute; width: 20px; height: 20px; left: 3px; top: 3px; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,.25); transition: .2s; } .np-switch input:checked + .np-slider { background: #2563eb; } .np-switch input:checked + .np-slider:before { transform: translateX(20px); } .np-settings-preview { display: flex; flex-wrap: wrap; gap: 6px; min-height: 28px; padding: 9px; background: #f8fafc; border: 1px dashed #d1d5db; border-radius: 6px; } .np-settings-chip { display: inline-flex; align-items: center; max-width: 100%; padding: 3px 7px; border-radius: 999px; background: #e0ecff; color: #1d4ed8; font-size: 12px; line-height: 1.35; word-break: break-all; } .np-settings-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; } .np-settings-btn { height: 34px; padding: 0 14px; border-radius: 6px; border: 1px solid #d1d5db; background: #fff; color: #374151; cursor: pointer; font-size: 14px; font-weight: 650; } .np-settings-btn:hover { background: #f3f4f6; } .np-settings-btn.primary { border-color: #2563eb; background: #2563eb; color: #fff; } .np-settings-btn.primary:hover { background: #1d4ed8; } `); const marker = document.createElement('meta'); marker.id = 'np-settings-style'; (document.head || document.documentElement).appendChild(marker); } function showSettingsModal() { const existing = document.getElementById('np-settings-modal'); if (existing) existing.remove(); installSettingsStyles(); const enabled = gmGet(STORAGE_KEYS.enabled, true); const pattern = gmGet(STORAGE_KEYS.matchPattern, DEFAULT_MATCH_PATTERN); const modal = document.createElement('div'); modal.id = 'np-settings-modal'; modal.className = 'np-settings-modal'; modal.innerHTML = ` `; (document.body || document.documentElement).appendChild(modal); const enabledInput = modal.querySelector('#np-enabled'); const patternInput = modal.querySelector('#np-match-pattern'); const preview = modal.querySelector('#np-pattern-preview'); const close = () => modal.remove(); const renderPreview = () => { const keywords = parseMatchPattern(patternInput.value); preview.innerHTML = keywords.length ? keywords.map((keyword) => `${escapeHtml(keyword)}`).join('') : '暂无有效规则'; }; modal.querySelector('#np-settings-close').onclick = close; modal.querySelector('#np-cancel').onclick = close; modal.querySelector('#np-reset-default').onclick = () => { patternInput.value = DEFAULT_MATCH_PATTERN; renderPreview(); }; modal.querySelector('#np-save').onclick = () => { gmSet(STORAGE_KEYS.enabled, enabledInput.checked); gmSet(STORAGE_KEYS.matchPattern, patternInput.value.trim() || DEFAULT_MATCH_PATTERN); alert('设置已保存,刷新页面后生效。'); close(); }; modal.addEventListener('click', (event) => { if (event.target === modal) close(); }); patternInput.addEventListener('input', renderPreview); renderPreview(); } function registerMenus() { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('NotePod++:设置', showSettingsModal); } function installNotePod(pageWindow) { if (pageWindow.__notepod_fnos_userscript_ready__) return; pageWindow.__notepod_fnos_userscript_ready__ = true; const CONFIG = { WIN_SELECTOR: '[role="tabpanel"]', APP_TITLE: '文件管理', ROOT_LABELS: ['我的文件', '设备全部文件', '应用文件'], MENU_KEYWORDS: ['打开', '重命名', '详细信息', '下载', '压缩', '剪切'], REFRESH_ICON_PATH: 'M12 4a8 8 0 108 8', API_NEW: '/app/m-text-editor/api/new', EDITOR_URL: '/app/m-text-editor/?path=' }; let lastActiveWin = null; let lastContextMenuTarget = null; function installWebSocketInterceptor() { const OriginalWS = pageWindow.WebSocket; if (!OriginalWS || OriginalWS.__notepodWrapped) return; function WrappedWebSocket(url, protocols) { const ws = protocols === undefined ? new OriginalWS(url) : new OriginalWS(url, protocols); if (typeof url === 'string' && url.includes('type=file')) { ws.__is_file_ws = true; } return ws; } WrappedWebSocket.prototype = OriginalWS.prototype; Object.setPrototypeOf(WrappedWebSocket, OriginalWS); WrappedWebSocket.__notepodWrapped = true; pageWindow.WebSocket = WrappedWebSocket; if (!OriginalWS.prototype.__notepodOriginalSend) { OriginalWS.prototype.__notepodOriginalSend = OriginalWS.prototype.send; OriginalWS.prototype.send = function (data) { try { const strData = typeof data === 'string' ? data : new TextDecoder().decode(data); const jsonStr = strData.includes('=') ? strData.split('=').slice(1).join('=') : strData; const msg = JSON.parse(jsonStr); if (msg.req === 'file.ls') { const wsPath = msg.path || '/'; const wsLastPart = wsPath === '/' ? '我的文件' : wsPath.split('/').pop(); const wins = document.querySelectorAll(CONFIG.WIN_SELECTOR); let matchedWin = null; for (const win of wins) { const domLastPart = getWinLastBreadcrumb(win); if (domLastPart === wsLastPart || (wsPath === '/' && domLastPart === '我的文件')) { matchedWin = win; break; } } const target = matchedWin || lastActiveWin || wins[wins.length - 1]; if (target) { target.__notepod_path = wsPath; console.log(`[NotePod++] path synced: ${wsPath}`); } } } catch (_) { // Ignore non-file-manager websocket frames. } return OriginalWS.prototype.__notepodOriginalSend.apply(this, arguments); }; } } function isFileManagerWin(el) { if (!el) return false; const win = el.closest('.trim-ui__app-layout--window') || el.closest('.trim-ui__app-layout--window-shell') || el.closest(CONFIG.WIN_SELECTOR); if (!win) return false; const header = win.querySelector('.trim-ui__app-layout--header-title'); if (header && header.innerText.trim() !== CONFIG.APP_TITLE) return false; return getWinLastBreadcrumb(win) !== ''; } function getWinLastBreadcrumb(win) { const items = Array.from(win.querySelectorAll('div[title]')); const rootItem = items.find((el) => CONFIG.ROOT_LABELS.includes(el.innerText.trim())); if (!rootItem) return ''; const container = rootItem.closest('.flex-1') || rootItem.parentElement; const breadItems = container ? container.querySelectorAll('div[title]') : []; return breadItems.length > 0 ? breadItems[breadItems.length - 1].getAttribute('title').trim() : ''; } function handleContextMenuEdit() { if (!lastContextMenuTarget) return; const titleEl = lastContextMenuTarget.querySelector('[title]'); const filename = titleEl ? titleEl.getAttribute('title') : null; const winContainer = lastContextMenuTarget.closest(CONFIG.WIN_SELECTOR); const wsPath = winContainer ? winContainer.__notepod_path : null; if (wsPath && filename) { const fullPath = wsPath.endsWith('/') ? wsPath + filename : `${wsPath}/${filename}`; showEditorWindow(fullPath); } else { console.warn('[NotePod++] Unable to resolve the selected file path.'); } } function createBackdrop() { const backdrop = document.createElement('div'); backdrop.className = 'notepod-backdrop'; backdrop.style.cssText = [ 'position:absolute', 'inset:0', 'background:var(--semi-color-overlay-bg, rgba(0,0,0,0.15))', 'z-index:9999', 'display:flex', 'align-items:center', 'justify-content:center', 'backdrop-filter:blur(2px)', 'border-radius:inherit' ].join(';'); return backdrop; } function showCreateFileModal(path, container) { return new Promise((resolve) => { const backdrop = createBackdrop(); const modal = document.createElement('div'); modal.style.cssText = [ 'width:448px', 'background:var(--semi-color-bg-2, #fff)', 'border:1px solid var(--semi-color-border, #eef0f1)', 'border-radius:12px', 'box-shadow:0 8px 36px rgba(0,0,0,0.12)', 'display:flex', 'flex-direction:column', 'overflow:hidden', 'font-family:Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' ].join(';'); modal.innerHTML = `
新建文件
文件名 *

保存到 ${escapeHtml(path)}

`; container.appendChild(backdrop); backdrop.appendChild(modal); const input = modal.querySelector('#np-filename'); const wrap = modal.querySelector('#np-input-wrap'); input.focus(); const finish = (value) => { if (value !== null && !value.trim()) { wrap.style.borderColor = 'var(--semi-color-danger, #f93920)'; return; } backdrop.remove(); resolve(value); }; modal.querySelector('#np-btn-ok').onclick = () => finish(input.value); modal.querySelector('#np-btn-cancel').onclick = () => finish(null); modal.querySelector('#np-close').onclick = () => finish(null); input.onkeydown = (event) => { if (event.key === 'Enter') finish(input.value); if (event.key === 'Escape') finish(null); }; }); } function showNotePodAlert(title, content, container) { const backdrop = createBackdrop(); const modal = document.createElement('div'); modal.style.cssText = [ 'width:448px', 'background:var(--semi-color-bg-2, #fff)', 'border:1px solid var(--semi-color-border, #f0f0f0)', 'border-radius:12px', 'box-shadow:0 8px 36px rgba(0,0,0,0.12)', 'display:flex', 'flex-direction:column', 'overflow:hidden', 'font-family:Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif' ].join(';'); modal.innerHTML = `
${escapeHtml(title)}
${escapeHtml(content)}
`; const target = container || document.body; target.appendChild(backdrop); backdrop.appendChild(modal); const close = () => backdrop.remove(); modal.querySelector('#np-alert-close').onclick = close; modal.querySelector('#alert-ok').onclick = close; } function showEditorWindow(path) { const existing = document.getElementById('notepod-editor-win'); if (existing) existing.remove(); const savedState = JSON.parse(pageWindow.localStorage.getItem('notepod-editor-win-state') || '{}'); const container = document.createElement('div'); container.id = 'notepod-editor-win'; container.style.cssText = [ 'position:fixed', `top:${savedState.top || '15%'}`, `left:${savedState.left || '15%'}`, `width:${savedState.width || '70%'}`, `height:${savedState.height || '70%'}`, 'background:var(--semi-color-bg-2, #fff)', 'border-radius:12px', 'box-shadow:0 12px 48px rgba(0,0,0,0.25)', 'z-index:10001', 'display:flex', 'flex-direction:column', 'overflow:hidden', 'border:1px solid var(--semi-color-border, #f0f0f0)', 'min-width:400px', 'min-height:300px', 'resize:both' ].join(';'); const editorUrl = `${CONFIG.EDITOR_URL}${encodeURIComponent(path)}`; container.innerHTML = `
NotePod++
`; document.body.appendChild(container); const iframe = container.querySelector('iframe'); const header = container.querySelector('.np-win-header'); const saveState = () => { pageWindow.localStorage.setItem('notepod-editor-win-state', JSON.stringify({ top: container.style.top, left: container.style.left, width: container.style.width, height: container.style.height })); }; container.querySelector('.win-btn-external').onclick = () => pageWindow.open(editorUrl, '_blank'); container.querySelector('.win-btn-close').onclick = () => { saveState(); container.remove(); }; let isDragging = false; let startX = 0; let startY = 0; let initialX = 0; let initialY = 0; header.addEventListener('mousedown', (event) => { isDragging = true; startX = event.clientX; startY = event.clientY; initialX = container.offsetLeft; initialY = container.offsetTop; iframe.style.pointerEvents = 'none'; header.style.cursor = 'grabbing'; }); pageWindow.addEventListener('mousemove', (event) => { if (!isDragging) return; container.style.left = `${initialX + event.clientX - startX}px`; container.style.top = `${initialY + event.clientY - startY}px`; container.style.right = 'auto'; container.style.bottom = 'auto'; }); pageWindow.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; iframe.style.pointerEvents = 'auto'; header.style.cursor = 'move'; saveState(); }); container.addEventListener('mouseup', saveState); } function injectNotePodMenuItem(menu) { const firstItem = Array.from(menu.querySelectorAll('div')).find((el) => el.innerText.trim() === '打开')?.closest('div'); if (!firstItem || menu.querySelector('.notepod-menu-item')) return; const newItem = document.createElement('div'); newItem.className = 'notepod-menu-item'; newItem.innerHTML = `
使用 NotePod++ 编辑
`; newItem.onclick = (event) => { event.stopPropagation(); handleContextMenuEdit(); }; firstItem.after(newItem); } function inject() { for (const menu of document.querySelectorAll('div')) { const text = menu.innerText || ''; const isPotentialMenu = menu.offsetWidth > 0 && CONFIG.MENU_KEYWORDS.every((keyword) => text.includes(keyword)); if (isPotentialMenu) { if (lastContextMenuTarget && isFileManagerWin(lastContextMenuTarget)) injectNotePodMenuItem(menu); break; } } document.querySelectorAll('button').forEach((targetBtn) => { const btnText = targetBtn.innerText || ''; if (!btnText.includes('新建文件夹') && !btnText.includes('上传文件夹')) return; const winContainer = targetBtn.closest(CONFIG.WIN_SELECTOR); if (!winContainer || !isFileManagerWin(winContainer)) return; const hasNewFileBtn = Array.from(winContainer.querySelectorAll('button')).some((button) => button.innerText.includes('新建文件') && !button.innerText.includes('文件夹')); if (hasNewFileBtn) return; const btn = document.createElement('button'); btn.className = 'notepod-new-file-btn'; btn.style.cssText = [ 'padding:0 12px', 'height:28px', 'border-radius:6px', 'border:1px solid var(--semi-color-border, #dcdfe6)', 'background:transparent', 'cursor:pointer', 'font-size:14px', 'font-weight:600', 'font-family:Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', 'color:var(--semi-color-text-1, #41464f)', 'display:flex', 'align-items:center', 'gap:4px' ].join(';'); btn.innerHTML = ` 新建文件 `; btn.onclick = async (event) => { event.stopPropagation(); const wsPath = winContainer.__notepod_path; const domLastPart = getWinLastBreadcrumb(winContainer); const wsLastPart = wsPath === '/' ? '我的文件' : (wsPath ? wsPath.split('/').pop() : ''); const isMatch = (wsPath === '/' && domLastPart === '我的文件') || (wsPath && wsLastPart === domLastPart); if (!isMatch || !wsPath) { showNotePodAlert('识别延迟', '路径同步中,请稍后重试或刷新页面。', winContainer); return; } if (wsPath === '/') { showNotePodAlert('操作受限', '根目录不允许创建文件。', winContainer); return; } const filename = await showCreateFileModal(wsPath, winContainer); if (!filename) return; const fullPath = wsPath.endsWith('/') ? wsPath + filename : `${wsPath}/${filename}`; try { const resp = await pageWindow.fetch(CONFIG.API_NEW, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: fullPath }) }); const res = await resp.json(); if (res.error) { showNotePodAlert('创建失败', res.error, winContainer); return; } const refreshed = Array.from(winContainer.querySelectorAll('button')).some((button) => { const pathNode = button.querySelector('path'); if (pathNode && (pathNode.getAttribute('d') || '').includes(CONFIG.REFRESH_ICON_PATH)) { button.click(); return true; } return false; }); if (!refreshed) console.warn('[NotePod++] Refresh button not found.'); } catch (err) { showNotePodAlert('网络错误', '无法连接到后端服务。', winContainer); } }; targetBtn.after(btn); }); } function escapeHtml(value) { return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function startDomObserver() { document.addEventListener('mousedown', (event) => { const win = event.target.closest(CONFIG.WIN_SELECTOR); if (win) lastActiveWin = win; }, true); document.addEventListener('contextmenu', (event) => { lastContextMenuTarget = event.target.closest('[data-path]') || event.target.closest('tr') || event.target.closest('li'); }, true); if (pageWindow.__notepod_observer__) pageWindow.__notepod_observer__.disconnect(); pageWindow.__notepod_observer__ = new MutationObserver(inject); pageWindow.__notepod_observer__.observe(document.body, { childList: true, subtree: true }); inject(); } installWebSocketInterceptor(); if (document.body) { startDomObserver(); } else { document.addEventListener('DOMContentLoaded', startDomObserver, { once: true }); } console.log('[NotePod++] FNOS userscript loaded.'); } registerMenus(); const enabled = gmGet(STORAGE_KEYS.enabled, true); const matchPattern = gmGet(STORAGE_KEYS.matchPattern, DEFAULT_MATCH_PATTERN); if (!enabled || !hostMatchesCurrentPage(matchPattern)) return; installNotePod(typeof unsafeWindow === 'undefined' ? window : unsafeWindow); })();