// ==UserScript== // @name 悬浮快捷面板 // @namespace http://scriptcat.org/ // @version 1.0.5 // @description 可自定义的悬浮快捷面板,支持链接分组、快捷键、后台打开、搜索过滤等功能 // @author ScriptCat // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_addStyle // @run-at document-end // @grant unsafeWindow // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'floating_panel_config_v1'; const BG_TABS_KEY = 'floating_panel_bg_tabs_v1'; const defaultConfig = { position: { x: 20, y: 100 }, isCollapsed: true, hoverExpand: false, hoverDelay: 300, panelWidth: 320, panelMaxHeight: 500, showFavicon: true, showSearch: true, theme: 'light', blacklist: [], autoSnap: true, snapThreshold: 50, hideButton: false, themeMode: 'custom', appearance: { buttonColor: '#3b82f6', buttonHoverColor: '#2563eb', buttonText: '⚡', buttonSize: 32, buttonShape: 'circle', panelBgColor: '#ffffff', panelHeaderColor: '#3b82f6', panelHeaderHoverColor: '#2563eb', panelTextColor: '#1f2937', panelLinkHoverBg: '#eff6ff', panelBorderRadius: 12, panelShadow: 'rgba(0,0,0,0.15)' }, groups: [ { id: 'default', name: '默认分组', expanded: true, links: [ { id: 'link_1', name: 'Guanor', url: 'http://Guanor.cn', openMode: 'newtab', enabled: true, shortcut: '', groupId: 'default' } ] } ] }; let config = null; let configLoadedSuccessfully = false; let bgTabs = {}; let panelEl = null; let settingsEl = null; let isDragging = false; let dragOffset = { x: 0, y: 0 }; let hoverTimer = null; let searchKeyword = ''; let groupExpandStates = {}; function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } function generateId(prefix = 'id') { return prefix + '_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } function loadConfig() { configLoadedSuccessfully = false; try { const saved = GM_getValue(STORAGE_KEY, null); if (saved) { let parsed; if (typeof saved === 'string') { try { parsed = JSON.parse(saved); } catch (parseErr) { console.warn('主配置解析失败,尝试从备份恢复:', parseErr); const backup = GM_getValue(STORAGE_KEY + '_backup', null); if (backup) { if (typeof backup === 'string') { parsed = JSON.parse(backup); } else { parsed = backup; } console.log('已从备份恢复配置'); } else { throw parseErr; } } } else { parsed = saved; } const defaults = deepClone(defaultConfig); config = deepMerge(defaults, parsed); if (!config.groups || !Array.isArray(config.groups) || config.groups.length === 0) { config.groups = deepClone(defaultConfig.groups); } if (!config.appearance || typeof config.appearance !== 'object') { config.appearance = deepClone(defaultConfig.appearance); } if (config.hideButton === undefined) { config.hideButton = defaultConfig.hideButton; } if (!config.themeMode) { config.themeMode = defaultConfig.themeMode; } configLoadedSuccessfully = true; } else { config = deepClone(defaultConfig); configLoadedSuccessfully = true; } } catch (e) { console.error('加载配置失败:', e); config = deepClone(defaultConfig); configLoadedSuccessfully = false; } } function deepMerge(target, source) { const result = deepClone(target); for (const key of Object.keys(source)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (result[key] && typeof result[key] === 'object') { result[key] = deepMerge(result[key], source[key]); } else { result[key] = deepClone(source[key]); } } else { result[key] = source[key]; } } return result; } function saveConfig(force = false) { try { if (!configLoadedSuccessfully && !force) { console.warn('配置未成功加载,跳过保存以防止数据丢失'); return false; } const configStr = JSON.stringify(config); const oldValue = GM_getValue(STORAGE_KEY, null); if (oldValue) { GM_setValue(STORAGE_KEY + '_backup', oldValue); } GM_setValue(STORAGE_KEY, configStr); configLoadedSuccessfully = true; return true; } catch (e) { console.error('保存配置失败:', e); alert('保存配置失败: ' + e.message); return false; } } function loadBgTabs() { try { const saved = GM_getValue(BG_TABS_KEY, null); if (saved) { if (typeof saved === 'string') { bgTabs = JSON.parse(saved); } else { bgTabs = saved; } } else { bgTabs = {}; } } catch (e) { console.error('加载后台标签状态失败:', e); bgTabs = {}; } } function saveBgTabs() { try { GM_setValue(BG_TABS_KEY, JSON.stringify(bgTabs)); } catch (e) { console.error('保存后台标签状态失败:', e); } } function isBlacklisted() { const url = window.location.href; return config.blacklist.some(pattern => { try { const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*'); const regex = new RegExp('^' + escaped + '$'); return regex.test(url); } catch (e) { return url.includes(pattern); } }); } function extractThemeColor() { let color = null; const metaTheme = document.querySelector('meta[name="theme-color"]'); if (metaTheme && metaTheme.content) { color = metaTheme.content; } if (!color) { const metaThemeApple = document.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]'); if (metaThemeApple && metaThemeApple.content) { const appleColors = { 'default': '#000000', 'black': '#000000', 'black-translucent': '#000000' }; color = appleColors[metaThemeApple.content] || null; } } if (!color) { try { const rootStyle = getComputedStyle(document.documentElement); const cssVars = [ '--primary', '--primary-color', '--accent', '--accent-color', '--brand', '--brand-color', '--theme-color', '--main-color' ]; for (const varName of cssVars) { const val = rootStyle.getPropertyValue(varName).trim(); if (val && (/^#[0-9A-Fa-f]{3,6}$/.test(val) || /^rgb/.test(val))) { color = val; break; } } } catch (e) {} } if (!color) { try { const header = document.querySelector('header, [role="banner"], .header, .navbar'); if (header) { const style = getComputedStyle(header); const bgColor = style.backgroundColor; if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') { color = bgColor; } } } catch (e) {} } if (!color) { try { const h1 = document.querySelector('h1'); if (h1) { const style = getComputedStyle(h1); const colorVal = style.color; if (colorVal && colorVal !== 'transparent') { color = colorVal; } } } catch (e) {} } if (!color) { color = '#3b82f6'; } return color; } function getThemeColors() { let primary = extractThemeColor(); const hexPrimary = colorToHex(primary); if (!hexPrimary) { primary = '#3b82f6'; } const darker = darkenColor(primary, 0.2); const lighter = lightenColor(primary, 0.15); return { buttonColor: primary, buttonHoverColor: darker, panelHeaderColor: primary, panelHeaderHoverColor: darker, panelLinkHoverBg: lightenColor(primary, 0.9) }; } function hexToRgb(hex) { hex = hex.replace(/^#/, ''); if (hex.length === 3) { hex = hex.split('').map(c => c + c).join(''); } const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; } function rgbToHex(r, g, b) { return '#' + [r, g, b].map(x => { const hex = Math.round(x).toString(16); return hex.length === 1 ? '0' + hex : hex; }).join(''); } function colorToHex(color) { if (!color) return null; if (/^#[0-9A-Fa-f]{3,6}$/.test(color)) { return color; } const rgbMatch = color.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); if (rgbMatch) { return rgbToHex(parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])); } const rgbaMatch = color.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*[\d.]+\)$/); if (rgbaMatch) { return rgbToHex(parseInt(rgbaMatch[1]), parseInt(rgbaMatch[2]), parseInt(rgbaMatch[3])); } return null; } function darkenColor(color, percent) { const hex = colorToHex(color); if (!hex) return color; const rgb = hexToRgb(hex); if (!rgb) return color; return rgbToHex( rgb.r * (1 - percent), rgb.g * (1 - percent), rgb.b * (1 - percent) ); } function lightenColor(color, percent) { const hex = colorToHex(color); if (!hex) return color; const rgb = hexToRgb(hex); if (!rgb) return color; return rgbToHex( rgb.r + (255 - rgb.r) * percent, rgb.g + (255 - rgb.g) * percent, rgb.b + (255 - rgb.b) * percent ); } function getFaviconUrl(url) { try { const u = new URL(url); const domain = u.hostname; return `https://api.iowen.cn/favicon/${domain}.png`; } catch (e) { return ''; } } function getShortcutDisplay(shortcut) { if (!shortcut) return ''; return shortcut.replace(/\+/g, ' + ').replace(/ctrl/gi, 'Ctrl').replace(/alt/gi, 'Alt').replace(/shift/gi, 'Shift').replace(/meta/gi, 'Meta'); } function injectStyles() { let colors = config.appearance; if (config.themeMode === 'auto') { const themeColors = getThemeColors(); colors = { ...config.appearance, ...themeColors }; } const app = colors; const btnSize = app.buttonSize; const borderRadius = app.buttonShape === 'circle' ? '50%' : app.panelBorderRadius + 'px'; const panelOffset = btnSize + 12; const css = ` #sc-floating-panel { position: fixed; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; user-select: none; } #sc-floating-panel.sc-collapsed .sc-panel-body { display: none; } #sc-floating-panel .sc-toggle-btn { width: ${btnSize}px; height: ${btnSize}px; border-radius: ${borderRadius}; background: linear-gradient(135deg, ${app.buttonColor} 0%, ${app.buttonHoverColor} 100%); color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.3); font-size: ${btnSize * 0.42}px; transition: transform 0.2s, box-shadow 0.2s; } #sc-floating-panel .sc-toggle-btn:hover { transform: scale(1.05); box-shadow: 0 6px 16px rgba(0,0,0,0.4); } #sc-floating-panel .sc-toggle-btn:active { transform: scale(0.95); } #sc-floating-panel .sc-panel-body { position: absolute; top: 0; left: ${panelOffset}px; width: ${config.panelWidth}px; max-height: ${config.panelMaxHeight}px; background: ${app.panelBgColor}; border-radius: ${app.panelBorderRadius}px; box-shadow: 0 8px 32px ${app.panelShadow}; overflow: hidden; display: flex; flex-direction: column; border: 1px solid rgba(0,0,0,0.1); } #sc-floating-panel.panel-on-right .sc-panel-body { left: auto; right: ${panelOffset}px; } #sc-floating-panel .sc-panel-header { background: linear-gradient(135deg, ${app.panelHeaderColor} 0%, ${app.panelHeaderHoverColor} 100%); color: white; padding: 12px 16px; cursor: move; display: flex; align-items: center; justify-content: space-between; } #sc-floating-panel .sc-panel-title { font-weight: 600; font-size: 14px; } #sc-floating-panel .sc-header-actions { display: flex; gap: 8px; } #sc-floating-panel .sc-header-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 28px; height: 28px; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: background 0.2s; } #sc-floating-panel .sc-header-btn:hover { background: rgba(255,255,255,0.35); } #sc-floating-panel .sc-search-box { padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.08); } #sc-floating-panel .sc-search-input { width: 100%; padding: 8px 12px; border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; font-size: 13px; outline: none; box-sizing: border-box; transition: border-color 0.2s; background: ${app.panelBgColor}; color: ${app.panelTextColor}; } #sc-floating-panel .sc-search-input:focus { border-color: ${app.buttonColor}; } #sc-floating-panel .sc-groups-container { flex: 1; overflow-y: auto; padding: 8px 0; } #sc-floating-panel .sc-groups-container::-webkit-scrollbar { width: 6px; } #sc-floating-panel .sc-groups-container::-webkit-scrollbar-track { background: ${app.panelBgColor}; } #sc-floating-panel .sc-groups-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); border-radius: 3px; } #sc-floating-panel .sc-group { margin-bottom: 4px; } #sc-floating-panel .sc-group-header { display: flex; align-items: center; padding: 8px 16px; cursor: pointer; font-size: 12px; font-weight: 600; color: rgba(0,0,0,0.5); text-transform: uppercase; letter-spacing: 0.5px; transition: background 0.15s; } #sc-floating-panel .sc-group-header:hover { background: ${app.panelLinkHoverBg}; } #sc-floating-panel .sc-group-arrow { display: inline-block; width: 12px; margin-right: 6px; transition: transform 0.2s; } #sc-floating-panel .sc-group.collapsed .sc-group-arrow { transform: rotate(-90deg); } #sc-floating-panel .sc-group.collapsed .sc-links-list { display: none; } #sc-floating-panel .sc-links-list { list-style: none; margin: 0; padding: 0; } #sc-floating-panel .sc-link-item { display: flex; align-items: center; padding: 8px 16px 8px 32px; cursor: pointer; transition: background 0.15s; font-size: 13px; color: ${app.panelTextColor}; } #sc-floating-panel .sc-link-item:hover { background: ${app.panelLinkHoverBg}; } #sc-floating-panel .sc-link-item.disabled { opacity: 0.5; cursor: not-allowed; } #sc-floating-panel .sc-link-status { width: 16px; margin-right: 8px; text-align: center; color: #10b981; font-weight: bold; } #sc-floating-panel .sc-link-item:not(.bg-mode) .sc-link-status { visibility: hidden; } #sc-floating-panel .sc-link-favicon { width: 16px; height: 16px; margin-right: 8px; border-radius: 3px; flex-shrink: 0; } #sc-floating-panel .sc-link-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #sc-floating-panel .sc-link-shortcut { font-size: 11px; color: rgba(0,0,0,0.4); background: rgba(0,0,0,0.05); padding: 2px 6px; border-radius: 4px; margin-left: 8px; } #sc-floating-panel .sc-empty-tip { padding: 32px 16px; text-align: center; color: rgba(0,0,0,0.4); font-size: 13px; } #sc-settings-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 2147483647; display: flex; align-items: center; justify-content: center; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } #sc-settings-modal .sc-settings-container { width: 700px; max-width: 90vw; height: 600px; max-height: 90vh; background: #fff; border-radius: 16px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.3); } #sc-settings-modal .sc-settings-header { padding: 20px 24px; background: linear-gradient(135deg, ${app.buttonColor} 0%, ${app.buttonHoverColor} 100%); color: white; display: flex; align-items: center; justify-content: space-between; } #sc-settings-modal .sc-settings-title { font-size: 18px; font-weight: 600; } #sc-settings-modal .sc-close-btn { background: rgba(255,255,255,0.2); border: none; color: white; width: 32px; height: 32px; border-radius: 8px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; } #sc-settings-modal .sc-close-btn:hover { background: rgba(255,255,255,0.35); } #sc-settings-modal .sc-settings-body { flex: 1; display: flex; overflow: hidden; } #sc-settings-modal .sc-settings-sidebar { width: 160px; background: #f9fafb; border-right: 1px solid #e5e7eb; padding: 12px 0; } #sc-settings-modal .sc-sidebar-item { padding: 10px 20px; cursor: pointer; font-size: 13px; color: #4b5563; border-left: 3px solid transparent; transition: all 0.15s; } #sc-settings-modal .sc-sidebar-item:hover { background: #f3f4f6; } #sc-settings-modal .sc-sidebar-item.active { background: #eef2ff; color: #4f46e5; border-left-color: #4f46e5; font-weight: 500; } #sc-settings-modal .sc-settings-content { flex: 1; overflow-y: auto; padding: 20px 24px; } #sc-settings-modal .sc-settings-content::-webkit-scrollbar { width: 6px; } #sc-settings-modal .sc-settings-content::-webkit-scrollbar-thumb { background: #d1d5db; border-radius: 3px; } #sc-settings-modal .sc-section-title { font-size: 15px; font-weight: 600; color: #111827; margin-bottom: 16px; } #sc-settings-modal .sc-form-item { margin-bottom: 16px; } #sc-settings-modal .sc-form-label { display: block; font-size: 13px; font-weight: 500; color: #374151; margin-bottom: 6px; } #sc-settings-modal .sc-form-input { width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 13px; outline: none; box-sizing: border-box; transition: border-color 0.2s; } #sc-settings-modal .sc-form-input:focus { border-color: ${app.buttonColor}; } #sc-settings-modal .sc-form-select { width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 13px; outline: none; box-sizing: border-box; background: white; cursor: pointer; } #sc-settings-modal .sc-form-checkbox { display: flex; align-items: center; gap: 8px; cursor: pointer; } #sc-settings-modal .sc-form-checkbox input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } #sc-settings-modal .sc-btn { padding: 8px 16px; border-radius: 8px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.15s; } #sc-settings-modal .sc-btn-primary { background: linear-gradient(135deg, ${app.buttonColor} 0%, ${app.buttonHoverColor} 100%); color: white; } #sc-settings-modal .sc-btn-primary:hover { opacity: 0.9; } #sc-settings-modal .sc-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; } #sc-settings-modal .sc-btn-secondary:hover { background: #e5e7eb; } #sc-settings-modal .sc-btn-danger { background: #fee2e2; color: #dc2626; } #sc-settings-modal .sc-btn-danger:hover { background: #fecaca; } #sc-settings-modal .sc-btn-sm { padding: 4px 10px; font-size: 12px; } #sc-settings-modal .sc-link-row { display: flex; align-items: center; gap: 8px; padding: 10px; background: #f9fafb; border-radius: 8px; margin-bottom: 8px; } #sc-settings-modal .sc-link-row .sc-link-favicon { width: 20px; height: 20px; flex-shrink: 0; } #sc-settings-modal .sc-link-row .sc-link-info { flex: 1; overflow: hidden; } #sc-settings-modal .sc-link-row .sc-link-name-sm { font-size: 13px; font-weight: 500; color: #111827; } #sc-settings-modal .sc-link-row .sc-link-url-sm { font-size: 11px; color: #6b7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #sc-settings-modal .sc-link-row .sc-link-actions { display: flex; gap: 4px; } #sc-settings-modal .sc-group-section { margin-bottom: 20px; } #sc-settings-modal .sc-group-section-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; } #sc-settings-modal .sc-group-name { font-weight: 600; font-size: 14px; color: #1f2937; } #sc-settings-modal .sc-shortcut-recorder { display: flex; align-items: center; gap: 8px; } #sc-settings-modal .sc-shortcut-display { flex: 1; padding: 8px 12px; background: #f9fafb; border: 1px solid #d1d5db; border-radius: 8px; font-size: 13px; color: #374151; text-align: center; } #sc-settings-modal .sc-shortcut-display.recording { border-color: #f59e0b; background: #fffbeb; color: #92400e; animation: sc-pulse 1.5s infinite; } @keyframes sc-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } #sc-settings-modal .sc-import-export-area { margin-top: 20px; display: flex; gap: 12px; } #sc-settings-modal .sc-blacklist-item { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: #f9fafb; border-radius: 6px; margin-bottom: 6px; } #sc-settings-modal .sc-blacklist-item .sc-blacklist-pattern { flex: 1; font-size: 13px; color: #374151; } #sc-settings-modal .sc-modal-footer { padding: 16px 24px; border-top: 1px solid #e5e7eb; display: flex; justify-content: flex-end; gap: 12px; } #sc-settings-modal .sc-add-row { display: flex; gap: 8px; margin-top: 10px; } #sc-settings-modal .sc-hint { font-size: 12px; color: #6b7280; margin-top: 4px; } #sc-settings-modal .sc-form-row { display: flex; gap: 16px; } #sc-settings-modal .sc-form-row .sc-form-item { flex: 1; } #sc-settings-modal .sc-color-preview { width: 32px; height: 32px; border-radius: 6px; border: 2px solid #e5e7eb; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #sc-settings-modal .sc-color-picker-row { display: flex; align-items: center; gap: 12px; } #sc-settings-modal .sc-appearance-preview { width: 60px; height: 60px; border-radius: ${app.buttonShape === 'circle' ? '50%' : '12px'}; background: linear-gradient(135deg, ${app.buttonColor} 0%, ${app.buttonHoverColor} 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 24px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); margin: 16px 0; } `; GM_addStyle(css); } function createFloatingPanel() { panelEl = document.createElement('div'); panelEl.id = 'sc-floating-panel'; panelEl.className = config.isCollapsed ? 'sc-collapsed' : ''; panelEl.style.left = config.position.x + 'px'; panelEl.style.top = config.position.y + 'px'; if (config.hideButton) { panelEl.style.display = 'none'; } const btnText = config.appearance.buttonText || '⚡'; panelEl.innerHTML = `
${btnText}
快捷面板
${config.showSearch ? ` ` : ''}
`; document.body.appendChild(panelEl); bindPanelEvents(); renderLinks(); } function bindPanelEvents() { const toggleBtn = panelEl.querySelector('.sc-toggle-btn'); const collapseBtn = panelEl.querySelector('.sc-collapse-btn'); const settingsBtn = panelEl.querySelector('.sc-settings-btn'); const header = panelEl.querySelector('.sc-panel-header'); const searchInput = panelEl.querySelector('.sc-search-input'); toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(); }); collapseBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePanel(true); }); settingsBtn.addEventListener('click', (e) => { e.stopPropagation(); openSettings(); }); header.addEventListener('mousedown', (e) => { if (e.target.closest('.sc-header-btn')) return; startDrag(e); }); toggleBtn.addEventListener('mousedown', (e) => { if (e.button !== 0) return; startDrag(e); }); if (searchInput) { searchInput.addEventListener('input', (e) => { const newKeyword = e.target.value.trim().toLowerCase(); if (newKeyword && !searchKeyword) { groupExpandStates = {}; config.groups.forEach(g => { groupExpandStates[g.id] = g.expanded; g.expanded = true; }); } else if (!newKeyword && searchKeyword) { config.groups.forEach(g => { if (groupExpandStates.hasOwnProperty(g.id)) { g.expanded = groupExpandStates[g.id]; } }); groupExpandStates = {}; } searchKeyword = newKeyword; renderLinks(); }); searchInput.addEventListener('click', (e) => e.stopPropagation()); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { e.stopPropagation(); if (searchKeyword) { searchInput.value = ''; searchKeyword = ''; config.groups.forEach(g => { if (groupExpandStates.hasOwnProperty(g.id)) { g.expanded = groupExpandStates[g.id]; } }); groupExpandStates = {}; renderLinks(); } else { togglePanel(true); } } }); } if (config.hoverExpand) { toggleBtn.addEventListener('mouseenter', () => { if (config.isCollapsed) { hoverTimer = setTimeout(() => { togglePanel(false); }, config.hoverDelay); } }); toggleBtn.addEventListener('mouseleave', () => { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } }); panelEl.addEventListener('mouseleave', () => { if (!config.isCollapsed && config.hoverExpand) { setTimeout(() => { if (!panelEl.matches(':hover')) { togglePanel(true); } }, 500); } }); } document.addEventListener('click', (e) => { if (!config.isCollapsed && !panelEl.contains(e.target) && !settingsEl) { togglePanel(true); } }); window.addEventListener('resize', onWindowResize); } function onWindowResize() { if (!panelEl) return; const rect = panelEl.getBoundingClientRect(); let x = rect.left; let y = rect.top; const btnSize = config.appearance.buttonSize || 48; const maxX = window.innerWidth - btnSize; const maxY = window.innerHeight - btnSize; x = Math.max(0, Math.min(x, maxX)); y = Math.max(0, Math.min(y, maxY)); if (x !== rect.left || y !== rect.top) { panelEl.style.left = x + 'px'; panelEl.style.top = y + 'px'; config.position.x = x; config.position.y = y; saveConfig(); } if (!config.isCollapsed) { const panelWidth = config.panelWidth + 60; const onRight = x + panelWidth > window.innerWidth; panelEl.classList.toggle('panel-on-right', onRight); } } function togglePanel(collapse = null) { const shouldCollapse = collapse !== null ? collapse : !config.isCollapsed; config.isCollapsed = shouldCollapse; panelEl.classList.toggle('sc-collapsed', shouldCollapse); if (!shouldCollapse) { const rect = panelEl.getBoundingClientRect(); const panelWidth = config.panelWidth + 60; const onRight = rect.left + panelWidth > window.innerWidth; panelEl.classList.toggle('panel-on-right', onRight); } saveConfig(); } function startDrag(e) { isDragging = true; const rect = panelEl.getBoundingClientRect(); dragOffset.x = e.clientX - rect.left; dragOffset.y = e.clientY - rect.top; panelEl.style.transition = 'none'; document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDrag); document.addEventListener('mouseleave', stopDrag); e.preventDefault(); } function onDrag(e) { if (!isDragging) return; let x = e.clientX - dragOffset.x; let y = e.clientY - dragOffset.y; const btnSize = config.appearance.buttonSize || 48; const maxX = window.innerWidth - btnSize; const maxY = window.innerHeight - btnSize; x = Math.max(0, Math.min(x, maxX)); y = Math.max(0, Math.min(y, maxY)); panelEl.style.left = x + 'px'; panelEl.style.top = y + 'px'; } function stopDrag() { if (isDragging) { isDragging = false; let x = parseInt(panelEl.style.left); let y = parseInt(panelEl.style.top); if (config.autoSnap) { const btnSize = config.appearance.buttonSize || 48; const threshold = config.snapThreshold || 50; const maxX = window.innerWidth - btnSize; const maxY = window.innerHeight - btnSize; if (x < threshold) x = 0; else if (x > maxX - threshold) x = maxX; if (y < threshold) y = 0; else if (y > maxY - threshold) y = maxY; panelEl.style.left = x + 'px'; panelEl.style.top = y + 'px'; panelEl.style.transition = ''; } config.position.x = x; config.position.y = y; saveConfig(); } document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); document.removeEventListener('mouseleave', stopDrag); } function getAllLinks() { const links = []; config.groups.forEach(group => { group.links.forEach(link => { links.push({ ...link, groupName: group.name }); }); }); return links; } function renderLinks() { const container = panelEl.querySelector('.sc-groups-container'); if (!container) return; let html = ''; let hasVisibleLinks = false; config.groups.forEach(group => { const filteredLinks = group.links.filter(link => { if (searchKeyword) { return link.name.toLowerCase().includes(searchKeyword) || link.url.toLowerCase().includes(searchKeyword); } return link.enabled; }); if (filteredLinks.length === 0 && searchKeyword) return; hasVisibleLinks = true; html += `
${group.name} ${filteredLinks.length}
`; }); if (!hasVisibleLinks) { html = '
暂无匹配的链接
'; } container.innerHTML = html; container.querySelectorAll('.sc-group-header').forEach(header => { header.addEventListener('click', (e) => { e.stopPropagation(); const groupEl = header.closest('.sc-group'); const groupId = groupEl.dataset.groupId; const group = config.groups.find(g => g.id === groupId); if (group) { group.expanded = !group.expanded; groupEl.classList.toggle('collapsed', !group.expanded); saveConfig(); } }); }); container.querySelectorAll('.sc-link-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); const linkId = item.dataset.linkId; openLinkById(linkId); }); }); } function openLinkById(linkId) { const link = getAllLinks().find(l => l.id === linkId); if (!link || !link.enabled) return; openLink(link); } function openLink(link) { switch (link.openMode) { case 'newtab': GM_openInTab(link.url, { active: true, insert: true }); if (!config.isCollapsed) togglePanel(true); break; case 'current': window.location.href = link.url; break; case 'background': if (bgTabs[link.id]) { delete bgTabs[link.id]; } else { GM_openInTab(link.url, { active: false, insert: true }); bgTabs[link.id] = Date.now(); } saveBgTabs(); renderLinks(); break; } } function setupShortcuts() { document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) { return; } const keys = []; if (e.ctrlKey) keys.push('ctrl'); if (e.altKey) keys.push('alt'); if (e.shiftKey) keys.push('shift'); if (e.metaKey) keys.push('meta'); if (keys.length === 0) return; const key = e.key.toLowerCase(); if (['control', 'alt', 'shift', 'meta'].includes(key)) return; keys.push(key); const shortcutStr = keys.join('+'); const allLinks = getAllLinks(); const matchedLink = allLinks.find(l => l.shortcut && l.shortcut.toLowerCase() === shortcutStr && l.enabled); if (matchedLink) { e.preventDefault(); openLink(matchedLink); } }); } function openSettings() { if (settingsEl) return; settingsEl = document.createElement('div'); settingsEl.id = 'sc-settings-modal'; document.body.appendChild(settingsEl); renderSettings(); bindSettingsEvents(); } function closeSettings() { if (settingsEl) { document.removeEventListener('keydown', onSettingsKeydown); settingsEl.remove(); settingsEl = null; } renderLinks(); } let currentSettingsTab = 'links'; let isLinkEditorOpen = false; function renderSettings() { settingsEl.innerHTML = `
⚡ 快捷面板设置
链接管理
通用设置
外观设置
黑名单
数据管理
`; renderSettingsContent(); } function renderSettingsContent() { const content = settingsEl.querySelector('#sc-settings-content'); switch (currentSettingsTab) { case 'links': renderLinksSettings(content); break; case 'general': renderGeneralSettings(content); break; case 'appearance': renderAppearanceSettings(content); break; case 'blacklist': renderBlacklistSettings(content); break; case 'data': renderDataSettings(content); break; } } function renderLinksSettings(container) { let html = '
链接管理
'; config.groups.forEach((group, groupIndex) => { html += `
📁 ${group.name}
`; if (group.links.length === 0) { html += '
暂无链接,点击"添加链接"开始添加
'; } else { group.links.forEach(link => { const favicon = config.showFavicon ? `` : ''; html += ` `; }); } html += '
'; }); html += ` `; container.innerHTML = html; container.querySelectorAll('[data-action="add-group"]').forEach(btn => { btn.addEventListener('click', () => addGroup()); }); container.querySelectorAll('[data-action="rename-group"]').forEach(btn => { btn.addEventListener('click', () => { const groupId = btn.dataset.groupId; const group = config.groups.find(g => g.id === groupId); if (group) { const newName = prompt('请输入新的分组名称:', group.name); if (newName && newName.trim()) { group.name = newName.trim(); saveConfig(); renderSettingsContent(); } } }); }); container.querySelectorAll('[data-action="delete-group"]').forEach(btn => { btn.addEventListener('click', () => { const groupId = btn.dataset.groupId; if (config.groups.length <= 1) { alert('至少需要保留一个分组!'); return; } if (confirm('确定要删除此分组及其所有链接吗?')) { config.groups = config.groups.filter(g => g.id !== groupId); saveConfig(); renderSettingsContent(); } }); }); container.querySelectorAll('[data-action="add-link"]').forEach(btn => { btn.addEventListener('click', () => { const groupId = btn.dataset.groupId; openLinkEditor(null, groupId); }); }); container.querySelectorAll('[data-action="edit-link"]').forEach(btn => { btn.addEventListener('click', () => { const linkId = btn.dataset.linkId; const link = getAllLinks().find(l => l.id === linkId); if (link) { openLinkEditor(link); } }); }); } function openLinkEditor(link, groupId = null) { const isEdit = !!link; const targetGroupId = link ? link.groupId : groupId || config.groups[0].id; isLinkEditorOpen = true; let html = `
${isEdit ? '编辑链接' : '添加链接'}
${isEdit && link.shortcut ? getShortcutDisplay(link.shortcut) : '点击录制按钮开始录制'}
支持 Ctrl / Alt / Shift + 任意键组合
${isEdit ? `` : ''}
`; const wrapper = document.createElement('div'); wrapper.innerHTML = html; settingsEl.appendChild(wrapper.firstElementChild); const modal = settingsEl.lastElementChild; let recordingShortcut = false; let recordedShortcut = isEdit ? link.shortcut : ''; const nameInput = modal.querySelector('#sc-edit-name'); const urlInput = modal.querySelector('#sc-edit-url'); const display = modal.querySelector('#sc-shortcut-display'); const recordBtn = modal.querySelector('#sc-record-btn'); function updateShortcutDisplay() { if (recordingShortcut) { display.textContent = '按下快捷键组合...'; display.classList.add('recording'); } else if (recordedShortcut) { display.textContent = getShortcutDisplay(recordedShortcut); display.classList.remove('recording'); } else { display.textContent = '点击录制按钮开始录制'; display.classList.remove('recording'); } } recordBtn.addEventListener('click', () => { recordingShortcut = !recordingShortcut; updateShortcutDisplay(); if (recordingShortcut) { nameInput.blur(); urlInput.blur(); } }); modal.querySelector('#sc-clear-shortcut').addEventListener('click', () => { recordedShortcut = ''; recordingShortcut = false; updateShortcutDisplay(); }); function handleKeyDown(e) { if (!recordingShortcut) { if (e.key === 'Enter' && !e.ctrlKey && !e.altKey && !e.metaKey) { if (e.target.tagName === 'INPUT' && e.target.type !== 'button') { e.preventDefault(); modal.querySelector('#sc-save-btn').click(); } } return; } if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); recordingShortcut = false; updateShortcutDisplay(); return; } e.preventDefault(); e.stopPropagation(); const keys = []; if (e.ctrlKey) keys.push('ctrl'); if (e.altKey) keys.push('alt'); if (e.shiftKey) keys.push('shift'); if (e.metaKey) keys.push('meta'); if (keys.length === 0) return; const key = e.key.toLowerCase(); if (['control', 'alt', 'shift', 'meta'].includes(key)) return; keys.push(key); recordedShortcut = keys.join('+'); recordingShortcut = false; updateShortcutDisplay(); } document.addEventListener('keydown', handleKeyDown, true); function cleanup() { document.removeEventListener('keydown', handleKeyDown, true); isLinkEditorOpen = false; } modal.querySelector('#sc-cancel-btn').addEventListener('click', () => { cleanup(); modal.remove(); }); modal.addEventListener('click', (e) => { if (e.target === modal) { cleanup(); modal.remove(); } }); if (isEdit) { modal.querySelector('#sc-delete-link-btn').addEventListener('click', () => { if (confirm('确定要删除此链接吗?')) { const group = config.groups.find(g => g.id === link.groupId); if (group) { group.links = group.links.filter(l => l.id !== link.id); if (bgTabs[link.id]) delete bgTabs[link.id]; saveBgTabs(); saveConfig(); } cleanup(); modal.remove(); renderSettingsContent(); } }); } modal.querySelector('#sc-save-btn').addEventListener('click', () => { const name = nameInput.value.trim(); const url = urlInput.value.trim(); const groupIdVal = modal.querySelector('#sc-edit-group').value; const mode = modal.querySelector('#sc-edit-mode').value; const enabled = modal.querySelector('#sc-edit-enabled').checked; if (!name) { alert('请输入链接名称'); return; } if (!url) { alert('请输入网址'); return; } try { new URL(url); } catch (e) { alert('请输入有效的URL地址(需要以 http:// 或 https:// 开头)'); return; } if (recordedShortcut) { const allLinks = getAllLinks(); const duplicate = allLinks.find(l => l.shortcut && l.shortcut.toLowerCase() === recordedShortcut.toLowerCase() && l.id !== (isEdit ? link.id : null)); if (duplicate) { alert(`快捷键 "${getShortcutDisplay(recordedShortcut)}" 已被链接 "${duplicate.name}" 使用,请更换一个。`); return; } } if (isEdit) { const oldGroup = config.groups.find(g => g.id === link.groupId); const newGroup = config.groups.find(g => g.id === groupIdVal); if (oldGroup && newGroup) { if (oldGroup.id === newGroup.id) { const idx = oldGroup.links.findIndex(l => l.id === link.id); if (idx !== -1) { oldGroup.links[idx] = { ...oldGroup.links[idx], name, url, openMode: mode, enabled, shortcut: recordedShortcut }; } } else { oldGroup.links = oldGroup.links.filter(l => l.id !== link.id); newGroup.links.push({ ...link, name, url, openMode: mode, enabled, shortcut: recordedShortcut, groupId: groupIdVal }); } } } else { const newGroup = config.groups.find(g => g.id === groupIdVal); if (newGroup) { newGroup.links.push({ id: generateId('link'), name, url, openMode: mode, enabled, shortcut: recordedShortcut, groupId: groupIdVal }); } } saveConfig(); cleanup(); modal.remove(); renderSettingsContent(); }); nameInput.focus(); } function addGroup() { const name = prompt('请输入分组名称:', '新分组'); if (name && name.trim()) { config.groups.push({ id: generateId('group'), name: name.trim(), expanded: true, links: [] }); saveConfig(); renderSettingsContent(); } } function renderGeneralSettings(container) { let html = `
通用设置
鼠标移到圆形按钮上时自动展开面板
链接较多时可快速过滤
隐藏后可通过快捷键或脚本猫菜单打开设置
像素
像素
将悬浮按钮恢复到默认位置
`; container.innerHTML = html; container.querySelector('#sc-hover-expand').addEventListener('change', (e) => { config.hoverExpand = e.target.checked; container.querySelector('#sc-hover-delay-item').style.display = e.target.checked ? '' : 'none'; saveConfig(); }); container.querySelector('#sc-hover-delay').addEventListener('change', (e) => { config.hoverDelay = parseInt(e.target.value) || 300; saveConfig(); }); container.querySelector('#sc-show-search').addEventListener('change', (e) => { config.showSearch = e.target.checked; saveConfig(); }); container.querySelector('#sc-show-favicon').addEventListener('change', (e) => { config.showFavicon = e.target.checked; saveConfig(); }); container.querySelector('#sc-hide-button').addEventListener('change', (e) => { config.hideButton = e.target.checked; if (panelEl) { panelEl.style.display = e.target.checked ? 'none' : ''; } saveConfig(); }); container.querySelector('#sc-panel-width').addEventListener('change', (e) => { config.panelWidth = parseInt(e.target.value) || 320; const panelBody = panelEl?.querySelector('.sc-panel-body'); if (panelBody) panelBody.style.width = config.panelWidth + 'px'; saveConfig(); }); container.querySelector('#sc-panel-max-height').addEventListener('change', (e) => { config.panelMaxHeight = parseInt(e.target.value) || 500; const panelBody = panelEl?.querySelector('.sc-panel-body'); if (panelBody) panelBody.style.maxHeight = config.panelMaxHeight + 'px'; saveConfig(); }); container.querySelector('#sc-reset-position').addEventListener('click', () => { config.position = { x: 20, y: 100 }; if (panelEl) { panelEl.style.left = '20px'; panelEl.style.top = '100px'; } saveConfig(); alert('位置已重置'); }); } function renderAppearanceSettings(container) { const previewApp = deepClone(config.appearance); const previewAutoSnap = config.autoSnap; const previewSnapThreshold = config.snapThreshold; let html = `
外观设置
跟随页面模式会自动从页面提取主题颜色
${previewApp.buttonText}
悬浮按钮设置
支持表情符号和普通文字
${previewApp.buttonSize}px
按钮采用渐变色,这是渐变的结束颜色
面板设置
${previewApp.panelBorderRadius}px
边缘吸附设置
拖拽时自动吸附到屏幕边缘
${previewSnapThreshold}px
应用后需要刷新页面才能看到效果
`; container.innerHTML = html; const preview = container.querySelector('#sc-appearance-preview'); let tempAutoSnap = previewAutoSnap; let tempSnapThreshold = previewSnapThreshold; function updatePreview() { preview.style.width = previewApp.buttonSize + 'px'; preview.style.height = previewApp.buttonSize + 'px'; preview.style.borderRadius = previewApp.buttonShape === 'circle' ? '50%' : previewApp.panelBorderRadius + 'px'; preview.style.background = `linear-gradient(135deg, ${previewApp.buttonColor} 0%, ${previewApp.buttonHoverColor} 100%)`; preview.style.fontSize = (previewApp.buttonSize * 0.42) + 'px'; preview.textContent = previewApp.buttonText; } container.querySelector('#sc-btn-text').addEventListener('input', (e) => { previewApp.buttonText = e.target.value; updatePreview(); }); const sizeRange = container.querySelector('#sc-btn-size'); const sizeValue = container.querySelector('#sc-btn-size-value'); sizeRange.addEventListener('input', (e) => { previewApp.buttonSize = parseInt(e.target.value); sizeValue.textContent = previewApp.buttonSize + 'px'; updatePreview(); }); container.querySelector('#sc-btn-shape').addEventListener('change', (e) => { previewApp.buttonShape = e.target.value; updatePreview(); }); function bindColorPicker(colorInput, hexInput, previewDiv, configKey) { colorInput.addEventListener('input', (e) => { previewApp[configKey] = e.target.value; hexInput.value = e.target.value; previewDiv.style.background = e.target.value; updatePreview(); }); hexInput.addEventListener('input', (e) => { const value = e.target.value; if (/^#[0-9A-Fa-f]{6}$/.test(value)) { previewApp[configKey] = value; colorInput.value = value; previewDiv.style.background = value; updatePreview(); } }); } bindColorPicker( container.querySelector('#sc-btn-color'), container.querySelector('#sc-btn-color-hex'), container.querySelector('#sc-btn-color + .sc-color-preview'), 'buttonColor' ); bindColorPicker( container.querySelector('#sc-btn-hover-color'), container.querySelector('#sc-btn-hover-color-hex'), container.querySelector('#sc-btn-hover-color + .sc-color-preview'), 'buttonHoverColor' ); bindColorPicker( container.querySelector('#sc-panel-bg'), container.querySelector('#sc-panel-bg-hex'), container.querySelector('#sc-panel-bg + .sc-color-preview'), 'panelBgColor' ); bindColorPicker( container.querySelector('#sc-panel-text'), container.querySelector('#sc-panel-text-hex'), container.querySelector('#sc-panel-text + .sc-color-preview'), 'panelTextColor' ); bindColorPicker( container.querySelector('#sc-panel-header'), container.querySelector('#sc-panel-header-hex'), container.querySelector('#sc-panel-header + .sc-color-preview'), 'panelHeaderColor' ); bindColorPicker( container.querySelector('#sc-panel-header-hover'), container.querySelector('#sc-panel-header-hover-hex'), container.querySelector('#sc-panel-header-hover + .sc-color-preview'), 'panelHeaderHoverColor' ); bindColorPicker( container.querySelector('#sc-link-hover'), container.querySelector('#sc-link-hover-hex'), container.querySelector('#sc-link-hover + .sc-color-preview'), 'panelLinkHoverBg' ); const radiusRange = container.querySelector('#sc-panel-radius'); const radiusValue = container.querySelector('#sc-panel-radius-value'); radiusRange.addEventListener('input', (e) => { previewApp.panelBorderRadius = parseInt(e.target.value); radiusValue.textContent = previewApp.panelBorderRadius + 'px'; updatePreview(); }); const autoSnap = container.querySelector('#sc-auto-snap'); const snapThresholdItem = container.querySelector('#sc-snap-threshold-item'); autoSnap.addEventListener('change', (e) => { tempAutoSnap = e.target.checked; snapThresholdItem.style.display = e.target.checked ? '' : 'none'; }); const snapThresholdRange = container.querySelector('#sc-snap-threshold'); const snapThresholdValue = container.querySelector('#sc-snap-threshold-value'); snapThresholdRange.addEventListener('input', (e) => { tempSnapThreshold = parseInt(e.target.value); snapThresholdValue.textContent = tempSnapThreshold + 'px'; }); const themeModeSelect = container.querySelector('#sc-theme-mode'); container.querySelector('#sc-apply-appearance').addEventListener('click', () => { config.appearance = deepClone(previewApp); config.autoSnap = tempAutoSnap; config.snapThreshold = tempSnapThreshold; config.themeMode = themeModeSelect.value; saveConfig(true); alert('外观设置已保存,请刷新页面查看效果!'); }); container.querySelector('#sc-reset-appearance').addEventListener('click', () => { if (confirm('确定要恢复默认外观设置吗?')) { config.appearance = deepClone(defaultConfig.appearance); config.autoSnap = defaultConfig.autoSnap; config.snapThreshold = defaultConfig.snapThreshold; config.themeMode = defaultConfig.themeMode; saveConfig(true); renderAppearanceSettings(container); } }); } function renderBlacklistSettings(container) { let html = `
页面黑名单
在匹配的页面上不显示悬浮面板,支持通配符 *
`; if (config.blacklist.length === 0) { html += '
暂无黑名单规则
'; } else { config.blacklist.forEach((pattern, idx) => { html += `
${pattern}
`; }); } html += `
`; container.innerHTML = html; container.querySelectorAll('[data-remove-idx]').forEach(btn => { btn.addEventListener('click', () => { const idx = parseInt(btn.dataset.removeIdx); config.blacklist.splice(idx, 1); saveConfig(); renderSettingsContent(); }); }); container.querySelector('#sc-add-blacklist').addEventListener('click', () => { const input = container.querySelector('#sc-blacklist-input'); const pattern = input.value.trim(); if (pattern) { config.blacklist.push(pattern); saveConfig(); input.value = ''; renderSettingsContent(); } }); container.querySelector('#sc-add-current').addEventListener('click', () => { const url = window.location.href; const pattern = url.replace(/^(https?:\/\/[^\/]+)\/.*$/, '$1/*'); if (!config.blacklist.includes(pattern)) { config.blacklist.push(pattern); saveConfig(); renderSettingsContent(); } }); } function renderDataSettings(container) { let html = `
数据管理
将所有配置导出为JSON文件
从JSON文件导入配置(将覆盖当前配置)
清除所有自定义配置,恢复到默认状态
`; container.innerHTML = html; container.querySelector('#sc-export-btn').addEventListener('click', () => { const exportData = { version: 1, exportTime: new Date().toISOString(), config: config }; const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `floating-panel-config-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); }); const fileInput = container.querySelector('#sc-import-file'); container.querySelector('#sc-import-btn').addEventListener('click', () => { fileInput.click(); }); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const data = JSON.parse(ev.target.result); const importedConfig = data.config || data; if (!importedConfig.groups || !Array.isArray(importedConfig.groups)) { throw new Error('无效的配置格式'); } if (confirm('导入将覆盖当前配置,确定继续吗?')) { const defaults = deepClone(defaultConfig); config = deepMerge(defaults, importedConfig); if (!config.groups || !Array.isArray(config.groups) || config.groups.length === 0) { config.groups = deepClone(defaultConfig.groups); } if (!config.appearance || typeof config.appearance !== 'object') { config.appearance = deepClone(defaultConfig.appearance); } saveConfig(true); alert('导入成功!'); closeSettings(); if (isBlacklisted()) { if (panelEl) { panelEl.remove(); panelEl = null; } } else { if (panelEl) panelEl.remove(); panelEl = null; createFloatingPanel(); } } } catch (err) { alert('导入失败: ' + err.message); } }; reader.readAsText(file); fileInput.value = ''; }); container.querySelector('#sc-reset-btn').addEventListener('click', () => { if (confirm('确定要恢复默认设置吗?所有自定义配置将丢失!')) { config = deepClone(defaultConfig); saveConfig(true); bgTabs = {}; saveBgTabs(); alert('已恢复默认设置'); closeSettings(); if (panelEl) panelEl.remove(); panelEl = null; createFloatingPanel(); } }); } function bindSettingsEvents() { settingsEl.querySelector('.sc-close-btn').addEventListener('click', closeSettings); settingsEl.addEventListener('click', (e) => { if (e.target.id === 'sc-settings-modal') { closeSettings(); } }); settingsEl.querySelectorAll('.sc-sidebar-item').forEach(item => { item.addEventListener('click', () => { currentSettingsTab = item.dataset.tab; settingsEl.querySelectorAll('.sc-sidebar-item').forEach(i => i.classList.remove('active')); item.classList.add('active'); renderSettingsContent(); }); }); document.addEventListener('keydown', onSettingsKeydown); } function onSettingsKeydown(e) { if (e.key === 'Escape' && !isLinkEditorOpen) { closeSettings(); } } function registerMenuCommand() { GM_registerMenuCommand('⚙ 设置', () => { if (isBlacklisted()) { if (confirm('当前页面在黑名单中,是否打开设置?')) { openSettings(); } } else { openSettings(); } }); } function init() { loadConfig(); loadBgTabs(); if (isBlacklisted()) { registerMenuCommand(); return; } injectStyles(); createFloatingPanel(); setupShortcuts(); registerMenuCommand(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();