// ==UserScript== // @name NAS外链自动替换成内链(最终版) // @namespace https://scriptcat.org/zh-CN/users/171872 // @version 3.4 // @author C // @description 适用于frp内网穿透用户,快速在原链/内网链接/自定义链接间切换,使得分享文件更多渠道选择; // @match http://192.168.*:5666/* // @grant none // @run-at document-start // ==/UserScript== /** * ⚠️⚠️如果脚本运行失败或者无效果,请将上方"@match http://192.168.*:5666/* "修改为自己的内网地址⚠️⚠️ * 建议使用脚本猫拓展运行,兼容性更好; * * 🔄 三种工作模式: * * 内网模式(默认) * - 复制链接时自动替换为内网地址,如192.168 * - 无需确认,直接生效 * * 原链模式 * - 保持原始nas设置链接不变 * - 适合需要外网分享的场景 * * 自定义模式 * - 自动将链接替换为自定义链接 * - ✏️可在配置区修改自定义链接,默认为https://s.fnnas.net,可修改为自己的域名; * * 🎛️ 操作方式: * - 右下角悬浮圆形图标显示当前模式 * - 点击图标可循环切换三种模式 * - 设置自动保存,刷新后依然有效 * * 💡 适用场景: * - 避免外网链接在内网访问的延迟 * - 根据不同需求灵活切换链接类型 * * 总结:一键解决内外网链接切换问题,提升NAS使用体验! */ (function () { 'use strict'; // --- 配置区 --- const STORAGE_KEY = 'fn_nas_copy_mode'; const POSITION_KEY = 'fn_nas_switcher_position'; let COPY_MODE = 1; // 0=原链, 1=内网, 3=自定义 const ENABLE_SCRIPT = true; let CUSTOM_LINK = 'https://s.fnnas.net'; let BUTTON_OPACITY = 0.45; const MENU_ITEM_SIZE = 30; const MENU_ITEM_SPACING = 7; // --- 全局变量 --- let modeSwitcher = null; let modeMenu = null; let isDragging = false; let offsetX, offsetY; let lastPosition = { left: '20px', bottom: '20px' }; let hideTimeout = null; // --- 工具函数 --- function loadModeFromStorage() { try { const savedMode = localStorage.getItem(STORAGE_KEY); if (savedMode !== null && ['0', '1', '3'].includes(savedMode)) { COPY_MODE = parseInt(savedMode); } } catch (e) { console.warn('加载模式设置失败:', e); } } function saveModeToStorage() { try { localStorage.setItem(STORAGE_KEY, COPY_MODE.toString()); } catch (e) { console.warn('保存模式设置失败:', e); } } function savePosition() { try { localStorage.setItem(POSITION_KEY, JSON.stringify(lastPosition)); } catch (e) { console.warn('保存按钮位置失败:', e); } } function loadPosition() { try { const savedPos = localStorage.getItem(POSITION_KEY); if (savedPos) { const pos = JSON.parse(savedPos); if (pos.left && pos.bottom) { lastPosition = pos; return pos; } } } catch (e) { console.warn('加载按钮位置失败:', e); } return { left: '20px', bottom: '20px' }; } // 修复:使用双引号包裹CSS,并转义内部双引号 function injectToastStyle() { const style = document.createElement('style'); style.textContent = ` .tm-toast { position: fixed; top: 20px; left: 20px; background: rgba(0, 0, 0, 0.85); color: #fff; padding: 12px 16px; border-radius: 6px; font-size: 14px; opacity: 0; transition: opacity 0.3s ease-in-out; z-index: 9999; white-space: nowrap; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); min-width: 200px; font-family: Arial, sans-serif; } .tm-toast.show { opacity: 1; } .tm-mode-text { display: inline-block; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-right: 5px; } .tm-mode-internal { background: #4CAF50; color: white; } .tm-mode-original { background: #2196F3; color: white; } .tm-mode-custom { background: #9C27B0; color: white; } .tm-success-toast { background: rgba(0, 100, 0, 0.85); } .tm-info-toast { background: rgba(0, 100, 150, 0.85); } .tm-mode-switcher { position: fixed; width: 50px; height: 50px; background: rgba(0, 0, 0, ${BUTTON_OPACITY}); border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: move; z-index: 9998; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; user-select: none; color: white; font-weight: bold; font-size: 14px; opacity: ${BUTTON_OPACITY}; } .tm-mode-switcher:hover { background: rgba(0, 0, 0, 0.9); transform: scale(1.1); opacity: 1; } .tm-mode-switcher.dragging { cursor: grabbing; opacity: 1; } .tm-mode-menu { position: fixed; display: flex; flex-direction: column; gap: ${MENU_ITEM_SPACING}px; padding: 6px 10px; background: rgba(0, 0, 0, 0.85); border-radius: 30px; z-index: 9997; opacity: 0; pointer-events: none; transition: opacity 0.2s; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); } .tm-mode-menu.active { opacity: 1; pointer-events: auto; } .tm-mode-item { width: ${MENU_ITEM_SIZE}px; height: ${MENU_ITEM_SIZE}px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.2); } .tm-mode-item:hover { transform: scale(1.15); z-index: 10; } .tm-mode-item.active { transform: scale(1.2); box-shadow: 0 0 0 3px white; } `; document.head.appendChild(style); } function getModeText() { switch (COPY_MODE) { case 0: return '原链'; case 1: return '内网'; case 3: return '自定'; default: return '内网'; } } function getModeClass() { switch (COPY_MODE) { case 0: return 'original'; case 1: return 'internal'; case 3: return 'custom'; default: return 'internal'; } } function createModeSwitcher() { if (modeSwitcher) modeSwitcher.remove(); const position = loadPosition(); modeSwitcher = document.createElement('div'); modeSwitcher.className = 'tm-mode-switcher'; modeSwitcher.style.left = position.left; modeSwitcher.style.bottom = position.bottom; const textDiv = document.createElement('div'); textDiv.className = 'tm-mode-switcher-text'; textDiv.textContent = getModeText(); modeSwitcher.appendChild(textDiv); modeSwitcher.addEventListener('mousedown', startDrag); modeSwitcher.addEventListener('touchstart', handleTouchStart, { passive: false }); modeSwitcher.addEventListener('mouseenter', showModeMenu); modeSwitcher.addEventListener('mouseleave', () => { clearTimeout(hideTimeout); hideTimeout = setTimeout(hideModeMenu, 300); }); document.body.appendChild(modeSwitcher); } function createModeMenu() { if (modeMenu) modeMenu.remove(); modeMenu = document.createElement('div'); modeMenu.className = 'tm-mode-menu'; const modes = [ { id: 0, name: '原链', class: 'original' }, { id: 1, name: '内网', class: 'internal' }, { id: 3, name: '自定', class: 'custom' } ]; modes.forEach(mode => { const item = document.createElement('div'); item.className = `tm-mode-item tm-mode-${mode.class}`; item.textContent = mode.name; item.dataset.mode = mode.id; if (COPY_MODE === mode.id) item.classList.add('active'); item.addEventListener('click', () => { COPY_MODE = mode.id; saveModeToStorage(); updateModeSwitcher(); hideModeMenu(); showSimpleToast(`已切换到${mode.name}模式`, 'info', 1500); }); modeMenu.appendChild(item); }); modeMenu.addEventListener('mouseenter', () => clearTimeout(hideTimeout)); modeMenu.addEventListener('mouseleave', hideModeMenu); document.body.appendChild(modeMenu); } function updateModeSwitcher() { if (!modeSwitcher) return; modeSwitcher.querySelector('.tm-mode-switcher-text').textContent = getModeText(); if (modeMenu) createModeMenu(); } function showModeMenu() { if (!modeMenu) createModeMenu(); const switcherRect = modeSwitcher.getBoundingClientRect(); const menuWidth = modeMenu.offsetWidth; modeMenu.style.left = `${switcherRect.left + switcherRect.width / 2 - menuWidth / 2}px`; modeMenu.style.top = `${switcherRect.top - modeMenu.offsetHeight - 10}px`; modeMenu.classList.add('active'); clearTimeout(hideTimeout); } function hideModeMenu() { if (modeMenu) modeMenu.classList.remove('active'); } function startDrag(e) { e.preventDefault(); isDragging = true; modeSwitcher.classList.add('dragging'); const rect = modeSwitcher.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); } function handleTouchStart(e) { e.preventDefault(); isDragging = true; modeSwitcher.classList.add('dragging'); const rect = modeSwitcher.getBoundingClientRect(); offsetX = e.touches[0].clientX - rect.left; offsetY = e.touches[0].clientY - rect.top; document.addEventListener('touchmove', dragTouch, { passive: false }); document.addEventListener('touchend', stopDragTouch); } // 简化拖动逻辑,直接更新 function drag(e) { if (!isDragging) return; e.preventDefault(); const left = e.clientX - offsetX; const top = e.clientY - offsetY; const boundedLeft = Math.max(0, Math.min(window.innerWidth - 50, left)); const boundedTop = Math.max(0, Math.min(window.innerHeight - 50, top)); modeSwitcher.style.left = `${boundedLeft}px`; modeSwitcher.style.bottom = `${window.innerHeight - boundedTop - 50}px`; lastPosition = { left: `${boundedLeft}px`, bottom: `${window.innerHeight - boundedTop - 50}px` }; // 为了流畅,这里不再实时保存,只在停止拖动时保存 } function dragTouch(e) { if (!isDragging) return; e.preventDefault(); const left = e.touches[0].clientX - offsetX; const top = e.touches[0].clientY - offsetY; const boundedLeft = Math.max(0, Math.min(window.innerWidth - 50, left)); const boundedTop = Math.max(0, Math.min(window.innerHeight - 50, top)); modeSwitcher.style.left = `${boundedLeft}px`; modeSwitcher.style.bottom = `${window.innerHeight - boundedTop - 50}px`; lastPosition = { left: `${boundedLeft}px`, bottom: `${window.innerHeight - boundedTop - 50}px` }; } function stopDrag() { isDragging = false; modeSwitcher.classList.remove('dragging'); document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); // 拖动停止时再保存,保证流畅 savePosition(); } function stopDragTouch() { isDragging = false; modeSwitcher.classList.remove('dragging'); document.removeEventListener('touchmove', dragTouch); document.removeEventListener('touchend', stopDragTouch); savePosition(); } function showSimpleToast(msg, type = 'info', duration = 2000) { const toast = document.createElement('div'); toast.className = 'tm-toast'; if (type === 'success') toast.classList.add('tm-success-toast'); else if (type === 'info') toast.classList.add('tm-info-toast'); const mode = getModeText(); const modeText = document.createElement('span'); modeText.className = `tm-mode-text tm-mode-${getModeClass()}`; modeText.textContent = mode; toast.innerHTML = `${modeText.outerHTML} ${msg}`; document.body.appendChild(toast); toast.offsetWidth; // 触发重排,确保动画生效 toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); toast.addEventListener('transitionend', () => toast.remove(), { once: true }); }, duration); } function matchShareLink(text) { return text.match(/https?:\/\/[^\/]+(\/s\/[^\s]*)/i); } function getSelectedText() { const sel = window.getSelection(); if (sel && !sel.isCollapsed) return sel.toString(); const activeEl = document.activeElement; if (activeEl && ['INPUT', 'TEXTAREA'].includes(activeEl.tagName)) return activeEl.value; return null; } function replaceWithInternalLink(originalText, match) { const path = match[1]; const internalLink = window.location.origin + path; const prefix = originalText.slice(0, match.index).trim(); return [prefix, `内网链接: ${internalLink}`].join('\n'); } function replaceWithCustomLink(originalText, match) { const path = match[1]; const customLink = CUSTOM_LINK + path; const prefix = originalText.slice(0, match.index).trim(); return [prefix, `自定义链接: ${customLink}`].join('\n'); } function handleCopyEvent(e) { if (!ENABLE_SCRIPT) return; const rawText = getSelectedText(); if (!rawText) return; const match = matchShareLink(rawText); if (!match) return; if (COPY_MODE === 0) { showSimpleToast('已复制原链接', 'info'); return; } else if (COPY_MODE === 1) { e.preventDefault(); const internalText = replaceWithInternalLink(rawText, match); e.clipboardData.setData('text/plain', internalText); showSimpleToast('已替换为内网链接', 'success'); } else if (COPY_MODE === 3) { e.preventDefault(); const customText = replaceWithCustomLink(rawText, match); e.clipboardData.setData('text/plain', customText); showSimpleToast('已替换为自定义链接', 'success'); } } function init() { loadModeFromStorage(); injectToastStyle(); createModeSwitcher(); // createModeMenu(); // 初始时不需要创建菜单,鼠标悬停时再创建 document.addEventListener('copy', handleCopyEvent); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();