// ==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 += `
`;
filteredLinks.forEach(link => {
const isBgOpened = link.openMode === 'background' && bgTabs[link.id];
const statusIcon = link.openMode === 'background' ? (isBgOpened ? '✔' : '□') : '';
const faviconHtml = config.showFavicon ?
` ` : '';
const shortcutHtml = link.shortcut ?
`${getShortcutDisplay(link.shortcut)} ` : '';
html += `
${statusIcon}
${faviconHtml}
${link.name}
${shortcutHtml}
`;
});
html += `
`;
});
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 += `
`;
if (group.links.length === 0) {
html += '
暂无链接,点击"添加链接"开始添加
';
} else {
group.links.forEach(link => {
const favicon = config.showFavicon ? `
` : '';
html += `
${favicon}
${link.name}
${!link.enabled ? '(已禁用) ' : ''}
${link.url}
编辑
`;
});
}
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 = `
`;
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 = `
通用设置
悬停延迟 (毫秒)
显示网站图标 (Favicon)
面板宽度
像素
面板最大高度
像素
`;
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
按钮形状
圆形
圆角矩形
应用外观设置
恢复默认外观
应用后需要刷新页面才能看到效果
`;
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 = `
数据管理
🔄 恢复默认设置
清除所有自定义配置,恢复到默认状态
`;
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();
}
})();