// ==UserScript==
// @name GreasyFork 优化增强
// @name:zh-CN GreasyFork 优化增强
// @name:en-US GreasyFork Optimization and Enhancement
// @namespace https://github.com/shiyi312
// @version 2026.9.5.3
// @description 为 GreasyFork 提供实用增强:多主题、收藏笔记、代码复制、论坛过滤、大纲导航、搜索语法、自动登录等,界面清爽,配置简洁。
// @author 辻弌20
// @license MIT
// @homepage https://github.com/shiyi312/Greasy-Fork-Optimization-and-Enhancement
// @homepageURL https://github.com/shiyi312/Greasy-Fork-Optimization-and-Enhancement
// @supportURL https://github.com/shiyi312/Greasy-Fork-Optimization-and-Enhancement/issues
// @icon https://raw.githubusercontent.com/shiyi312/Greasy-Fork-Optimization-and-Enhancement/main/icon.png
// @icon64 https://raw.githubusercontent.com/shiyi312/Greasy-Fork-Optimization-and-Enhancement/main/icon.png
// @match https://greasyfork.org/*
// @match https://sleazyfork.org/*
// @match https://cn-greasyfork.org/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// @grant GM_getResourceText
// @grant GM_setClipboard
// @grant unsafeWindow
// @require https://cdn.jsdelivr.net/npm/viewerjs@1.11.8/dist/viewer.js
// @require https://cdn.jsdelivr.net/npm/otpauth@9.5.1/dist/otpauth.umd.min.js
// @resource ViewerCSS https://cdn.jsdelivr.net/npm/viewerjs@1.11.8/dist/viewer.min.css
// @run-at document-end
// ==/UserScript==
/**
* GreasyFork 优化增强
* 作者: 辻弌20
* 主页: https://scriptcat.org/zh-CN/users/202800
* 仓库: https://github.com/shiyi312/Greasy-Fork-Optimization-and-Enhancement
*/
(function() {
'use strict';
const log = {
info: (...args) => console.log('[GFE]', ...args),
warn: (...args) => console.warn('[GFE]', ...args),
error: (...args) => console.error('[GFE]', ...args),
success: (...args) => console.log('[GFE] ✅', ...args),
};
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const Utils = {
get: (key, def) => { try { return GM_getValue(key, def); } catch { return localStorage.getItem(key) || def; } },
set: (key, val) => { try { GM_setValue(key, val); } catch { localStorage.setItem(key, val); } },
del: (key) => { try { GM_deleteValue(key); } catch { localStorage.removeItem(key); } },
list: () => { try { return GM_listValues(); } catch { return Object.keys(localStorage); } },
addStyle: (css) => { try { GM_addStyle(css); } catch { const s = document.createElement('style'); s.textContent = css; document.head.appendChild(s); } },
getResource: (name) => { try { return GM_getResourceText(name); } catch { return null; } },
registerMenu: (name, fn) => { try { GM_registerMenuCommand(name, fn); } catch { /* ignore */ } },
xhr: (details) => { try { return GM_xmlhttpRequest(details); } catch { /* ignore */ } },
waitForEl: (selector, timeout = 5000) => new Promise((resolve) => {
if ($(selector)) return resolve($(selector));
const start = Date.now();
const interval = setInterval(() => {
const el = $(selector);
if (el || Date.now() - start > timeout) {
clearInterval(interval);
resolve(el);
}
}, 100);
}),
observe: (target, config, callback) => {
const observer = new MutationObserver(callback);
observer.observe(target, config);
return observer;
},
escapeHtml: (str) => {
if (!str) return '';
return str.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
},
toJSON: (str) => { try { return JSON.parse(str); } catch { return null; } },
formatTime: (ts, fmt = 'yyyy-MM-dd HH:mm:ss') => {
const d = new Date(ts);
const o = {
'M+': d.getMonth() + 1,
'd+': d.getDate(),
'H+': d.getHours(),
'm+': d.getMinutes(),
's+': d.getSeconds()
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (d.getFullYear() + '').substr(4 - RegExp.$1.length));
for (let k in o) if (new RegExp('(' + k + ')').test(fmt))
fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length));
return fmt;
}
};
// ========== 配置管理 ==========
const CONFIG_KEY = 'GFE_lite_config';
const Config = {
get(key, def) {
const all = this.getAll();
return all.hasOwnProperty(key) ? all[key] : def;
},
set(key, val) {
const all = this.getAll();
all[key] = val;
Utils.set(CONFIG_KEY, JSON.stringify(all));
},
getAll() {
try { return JSON.parse(Utils.get(CONFIG_KEY, '{}')); } catch { return {}; }
},
delete(key) {
const all = this.getAll();
delete all[key];
Utils.set(CONFIG_KEY, JSON.stringify(all));
},
addListener(key, cb) {
try {
return GM_addValueChangeListener(CONFIG_KEY, (name, old, val) => {
try {
const newObj = JSON.parse(val);
if (newObj.hasOwnProperty(key)) cb(key, newObj[key], old ? JSON.parse(old)[key] : undefined);
} catch {}
});
} catch { return null; }
},
removeListener(id) { try { GM_removeValueChangeListener(id); } catch {} }
};
// ========== 主题系统 ==========
const THEME_KEY = 'theme';
const THEME_DEFAULT = 'system';
let currentTheme = Config.get(THEME_KEY, THEME_DEFAULT);
const THEME_COLORS = {
light: { bg: '#f5f7fb', text: '#111827', card: '#ffffff', border: '#e4e7ed', shadow: 'rgba(0,0,0,0.06)' },
dark: { bg: '#0b111a', text: '#e5e7eb', card: '#1e2633', border: '#2d3748', shadow: 'rgba(0,0,0,0.4)' }
};
const systemDark = window.matchMedia('(prefers-color-scheme: dark)');
function resolveTheme(theme) {
return theme === 'system' ? (systemDark.matches ? 'dark' : 'light') : theme;
}
function applyTheme(theme) {
currentTheme = theme;
Config.set(THEME_KEY, theme);
const resolved = resolveTheme(theme);
const isDark = resolved === 'dark';
const root = document.documentElement;
root.classList.toggle('gfe-dark', isDark);
root.style.colorScheme = isDark ? 'dark' : 'light';
const colors = isDark ? THEME_COLORS.dark : THEME_COLORS.light;
Object.entries(colors).forEach(([k, v]) => root.style.setProperty('--gfe-' + k, v));
root.style.backgroundColor = colors.bg;
root.style.color = colors.text;
// 卡片样式通过 CSS 变量自动响应,无需遍历
}
function initTheme() {
applyTheme(currentTheme);
systemDark.addEventListener('change', () => {
if (currentTheme === 'system') applyTheme('system');
});
}
// ========== 收藏管理 ==========
const FAV_KEY = 'gfe_favorites';
function getFavorites() { return Config.get(FAV_KEY, {}); }
function setFavorites(fav) { Config.set(FAV_KEY, fav); }
function isFavorite(scriptId) { return !!getFavorites()[scriptId]; }
async function toggleFavorite(scriptId, scriptName) {
const fav = getFavorites();
if (fav[scriptId]) {
delete fav[scriptId];
setFavorites(fav);
showToast('已移除收藏', 'success');
} else {
fav[scriptId] = { name: scriptName || scriptId, time: Date.now() };
setFavorites(fav);
showToast('已添加到收藏', 'success');
}
updateAllFavButtons();
}
function updateAllFavButtons() {
document.querySelectorAll('.gfe-btn-fav[data-script-id]').forEach(btn => {
const id = btn.dataset.scriptId;
const active = isFavorite(id);
btn.classList.toggle('active', active);
btn.textContent = active ? '❤️' : '🤍';
});
}
// ========== 笔记管理 ==========
const NOTES_KEY = 'gfe_notes';
function getNote(scriptId) { return Config.get(NOTES_KEY, {})[scriptId] || ''; }
function setNote(scriptId, content) {
const notes = Config.get(NOTES_KEY, {});
if (content && content.trim()) notes[scriptId] = content.trim();
else delete notes[scriptId];
Config.set(NOTES_KEY, notes);
}
// ========== 过滤系统 ==========
const FILTER_KEY = 'gfe_filters';
const REGEX_FILTER_KEY = 'gfe_regex_filter';
function getFilters() { return Config.get(FILTER_KEY, []); }
function addFilter(type, value) {
const filters = getFilters();
filters.push({ type, value });
Config.set(FILTER_KEY, filters);
}
function removeFilter(index) {
const filters = getFilters();
if (index >= 0 && index < filters.length) {
filters.splice(index, 1);
Config.set(FILTER_KEY, filters);
}
}
function getRegexFilter() { return Config.get(REGEX_FILTER_KEY, ''); }
function setRegexFilter(pattern) { Config.set(REGEX_FILTER_KEY, pattern); }
// 修改:matchFilter 增加 try-catch 容错
function matchFilter(data) {
const filters = getFilters();
for (const f of filters) {
const val = data[f.type];
if (!val) continue;
try {
if (String(val).match(new RegExp(f.value, 'i'))) return true;
} catch (_) {
// 忽略无效正则,当作不匹配
}
}
return false;
}
function matchRegexFilter(data) {
const pattern = getRegexFilter();
if (!pattern) return false;
try {
const re = new RegExp(pattern, 'i');
return re.test(data.scriptName) || re.test(data.scriptAuthor);
} catch(e) { return false; }
}
function applyFilters() {
document.querySelectorAll('.gfe-script-card[data-script-id]').forEach(card => {
const info = {
scriptId: card.dataset.scriptId,
scriptName: card.dataset.scriptName,
scriptAuthor: card.dataset.scriptAuthor,
};
const hidden = matchFilter(info) || matchRegexFilter(info);
card.style.display = hidden ? 'none' : '';
});
document.querySelectorAll('.discussion-list-container').forEach(container => {
const title = container.querySelector('.discussion-title')?.textContent?.trim() || '';
const author = container.querySelector('.user-link')?.textContent?.trim() || '';
const info = { title, author };
if (matchFilter(info)) {
container.style.display = 'none';
container.dataset.filtered = 'true';
}
});
}
// ========== Toast ==========
let toastTimer = null;
function showToast(msg, type = 'info') {
const old = document.querySelector('.gfe-toast');
if (old) old.remove();
if (toastTimer) clearTimeout(toastTimer);
const div = document.createElement('div');
div.className = 'gfe-toast';
const colors = { info: '#409eff', success: '#67c23a', warning: '#e6a23c', error: '#f56c6c' };
div.style.cssText = `
position: fixed; top: 20px; right: 20px; z-index: 999999;
padding: 12px 24px; border-radius: 8px;
background: ${colors[type] || colors.info};
color: #fff; font-size: 14px; font-family: sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
max-width: 400px; word-break: break-word;
animation: gfeFadeIn 0.3s ease;
transition: opacity 0.3s;
`;
div.textContent = msg;
document.body.appendChild(div);
if (!document.getElementById('gfe-toast-style')) {
const style = document.createElement('style');
style.id = 'gfe-toast-style';
style.textContent = `@keyframes gfeFadeIn { from { opacity:0; transform:translateY(-10px); } to { opacity:1; transform:translateY(0); } }`;
document.head.appendChild(style);
}
toastTimer = setTimeout(() => {
div.style.opacity = '0';
setTimeout(() => div.remove(), 300);
toastTimer = null;
}, 3000);
}
// ========== 样式注入 ==========
function injectStyles() {
const css = `
.gfe-script-list {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)) !important;
gap: 16px !important;
padding: 0 !important;
list-style: none !important;
}
.gfe-script-list > li {
list-style: none !important;
border: 1px solid var(--gfe-border, #e4e7ed) !important;
border-radius: 8px !important;
padding: 12px 16px !important;
background: var(--gfe-card, #ffffff) !important;
box-shadow: 0 2px 8px var(--gfe-shadow, rgba(0,0,0,0.06)) !important;
transition: transform 0.15s, box-shadow 0.15s !important;
margin: 0 !important;
}
.gfe-script-list > li:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 16px var(--gfe-shadow, rgba(0,0,0,0.12)) !important;
}
.gfe-script-card-title { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
.gfe-script-card-title a { color: inherit; text-decoration: none; }
.gfe-script-card-title a:hover { color: #409eff; }
.gfe-script-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 13px; color: var(--gfe-text, #606266); opacity: 0.7; margin: 4px 0; }
.gfe-script-meta span { display: inline-flex; align-items: center; gap: 4px; }
.gfe-script-desc { color: var(--gfe-text, #606266); font-size: 14px; margin: 6px 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; opacity: 0.8; }
.gfe-script-actions { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
.gfe-script-actions .install-link { background: #409eff; color: #fff; border: none; padding: 4px 14px; border-radius: 4px; text-decoration: none; font-size: 13px; }
.gfe-script-actions .install-link:hover { background: #66b1ff; }
.gfe-script-actions button { background: transparent; border: 1px solid var(--gfe-border, #dcdfe6); padding: 4px 10px; border-radius: 4px; cursor: pointer; font-size: 13px; color: var(--gfe-text, #333); }
.gfe-script-actions button:hover { border-color: #409eff; color: #409eff; }
.gfe-script-actions .gfe-btn-fav.active { color: #f56c6c; border-color: #f56c6c; }
.gfe-note-panel { border: 1px solid var(--gfe-border, #e4e7ed); border-radius: 4px; padding: 8px 12px; margin: 6px 0; background: var(--gfe-card, #f5f7fa); }
.gfe-note-panel textarea { width: 100%; border: 1px solid var(--gfe-border, #dcdfe6); border-radius: 4px; padding: 6px 10px; resize: vertical; font-size: 13px; background: var(--gfe-card, #fff); color: var(--gfe-text, #333); }
.gfe-note-panel .note-actions { display: flex; gap: 8px; margin-top: 6px; }
.gfe-note-panel .note-actions button { padding: 2px 12px; font-size: 12px; }
/* 暗色主题下的样式覆盖 */
.gfe-dark {
--gfe-bg: #0b111a;
--gfe-text: #e5e7eb;
--gfe-card: #1e2633;
--gfe-border: #2d3748;
--gfe-shadow: rgba(0,0,0,0.4);
}
.gfe-dark .gfe-script-actions button {
color: #ffffff !important;
opacity: 0.95;
border-color: #4a5a7a;
}
.gfe-dark .gfe-script-actions button:hover {
color: #409eff !important;
border-color: #409eff;
opacity: 1;
}
.gfe-dark .gfe-script-actions .gfe-btn-fav.active {
color: #ff6b6b !important;
border-color: #ff6b6b;
}
.gfe-dark .gfe-script-actions .gfe-btn-bookmark.active {
color: #ffd93d !important;
border-color: #ffd93d;
}
.gfe-dark .gfe-script-meta span {
color: #c0c8d8;
}
.gfe-dark .gfe-script-desc {
color: #b0b8c8;
}
.gfe-dark .gfe-script-card-title a {
color: #e5e7eb !important;
}
.gfe-dark .gfe-script-card-title a:hover {
color: #409eff !important;
}
.gfe-dark .gfe-note-panel {
background: #252f3f;
border-color: #3a4a5a;
}
.gfe-dark .gfe-note-panel textarea {
background: #1a2230;
color: #e0e8f0;
border-color: #3a4a5a;
}
/* ===== 版本页暗色样式(新增) ===== */
.gfe-dark .gfe-version-item {
color: #e5e7eb !important;
}
.gfe-dark .gfe-version-item .gfe-version-number a {
color: #e5e7eb !important;
}
.gfe-dark .gfe-version-item .gfe-version-date {
color: #c0c8d8 !important;
}
.gfe-dark .gfe-version-item .gfe-version-changelog {
color: #b0b8c8 !important;
}
.gfe-dark .gfe-version-actions a,
.gfe-dark .gfe-version-actions button {
color: #ffffff !important;
}
.gfe-dark .gfe-version-actions .install-link {
background: #409eff !important;
color: #fff !important;
}
.gfe-dark .gfe-version-actions .install-link:hover {
background: #66b1ff !important;
}
.gfe-dark .gfe-version-actions button {
border-color: #4a5a7a !important;
background: transparent;
}
.gfe-dark .gfe-version-actions button:hover {
border-color: #409eff !important;
color: #409eff !important;
}
/* 版本页原生的 diff-controls 等文字颜色 */
.gfe-dark .diff-controls label {
color: #e5e7eb !important;
}
.gfe-dark .diff-controls input[type="radio"] {
accent-color: #409eff;
}
.gfe-version-item { padding: 12px 16px; border: 1px solid var(--gfe-border, #e4e7ed); border-radius: 6px; margin: 8px 0; background: var(--gfe-card, #fafafa); }
.gfe-version-header { display: flex; justify-content: space-between; align-items: center; }
.gfe-version-number { font-weight: bold; font-size: 16px; }
.gfe-version-date { color: #909399; font-size: 13px; }
.gfe-version-changelog { margin: 8px 0 0; white-space: pre-wrap; }
.gfe-version-actions { display: flex; gap: 8px; margin-top: 8px; }
.gfe-code-toolbar {
display: flex;
justify-content: flex-end;
margin-bottom: 8px;
}
.gfe-code-toolbar button {
padding: 4px 16px;
font-size: 13px;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.gfe-code-toolbar button:hover {
background: #66b1ff;
}
.gfe-code-toolbar button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.gfe-code-toolbar .gfe-code-stats {
font-size: 13px;
color: #909399;
margin-left: 12px;
line-height: 32px;
}
.discussion-list-container[data-filtered="true"] { display: none !important; }
.discussion-read-hidden { display: none !important; }
.discussion-read-italic { font-style: italic; opacity: 0.6; color: gray; }
.gfe-outline {
position: sticky;
float: right;
padding: 0 0 0 0.5em;
margin: 0 0.5em -99vh;
max-height: 80vh;
border: 1px solid var(--gfe-border, #ccc);
background: var(--gfe-card, #fff);
list-style: none;
width: 12%;
border-radius: 5px;
overflow-y: auto;
z-index: 10;
}
.gfe-outline a { color: var(--gfe-text, #333); text-decoration: none; display: block; padding: 2px 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gfe-outline a:hover { background: #409eff; color: #fff; }
.gfe-outline li { list-style: none; }
.gfe-sticky-pagination { position: sticky; bottom: 0; backdrop-filter: blur(4px); padding: 8px; background: rgba(255,255,255,0.7); z-index: 5; }
.gfe-flat-layout .script-list > li { padding-right: 0; }
.gfe-flat-layout .script-list > li article { display: flex; flex-direction: row; align-items: center; }
.gfe-flat-layout .script-list > li article > h2 { width: 60%; overflow: hidden; text-overflow: ellipsis; margin-right: 0.5em; border-right: 1px solid #ccc; }
.gfe-flat-layout .script-list > li article > .script-meta-block { width: 40%; }
@media (max-width: 600px) { .gfe-flat-layout .script-list > li article { flex-direction: column; align-items: stretch; } }
.gfe-bookmark-btn { cursor: pointer; margin-left: 4px; }
.gfe-bookmark-btn.active { color: #f56c6c; }
.gfe-tooltip {
position: fixed;
padding: 4px 10px;
background: rgba(0,0,0,0.75);
color: #fff;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
z-index: 99999;
white-space: nowrap;
opacity: 0;
transition: opacity 0.15s ease;
}
.gfe-tooltip.visible {
opacity: 1;
}
`;
Utils.addStyle(css);
const viewerCSS = Utils.getResource('ViewerCSS');
if (viewerCSS) Utils.addStyle(viewerCSS);
}
// ========== Tooltip 延迟显示逻辑 ==========
let tooltipTimer = null;
let tooltipEl = null;
function initTooltip() {
tooltipEl = document.createElement('div');
tooltipEl.className = 'gfe-tooltip';
document.body.appendChild(tooltipEl);
document.addEventListener('mouseenter', (e) => {
const btn = e.target.closest('[data-tooltip]');
if (!btn) return;
if (tooltipTimer) {
clearTimeout(tooltipTimer);
tooltipTimer = null;
}
tooltipTimer = setTimeout(() => {
const rect = btn.getBoundingClientRect();
tooltipEl.textContent = btn.dataset.tooltip;
tooltipEl.style.left = (rect.left + rect.width / 2 - tooltipEl.offsetWidth / 2) + 'px';
tooltipEl.style.top = (rect.bottom + 6) + 'px';
tooltipEl.classList.add('visible');
tooltipTimer = null;
}, 400);
}, true);
document.addEventListener('mouseleave', (e) => {
const btn = e.target.closest('[data-tooltip]');
if (!btn) return;
if (tooltipTimer) {
clearTimeout(tooltipTimer);
tooltipTimer = null;
}
tooltipEl.classList.remove('visible');
}, true);
document.addEventListener('mouseleave', () => {
if (tooltipTimer) {
clearTimeout(tooltipTimer);
tooltipTimer = null;
}
tooltipEl.classList.remove('visible');
}, true);
}
// ========== 通用复制函数 ==========
function copyTextWithFeedback(text, btn, successMsg, failMsg) {
const originalText = btn.textContent;
btn.textContent = '复制中...';
btn.disabled = true;
const resetBtn = () => {
btn.textContent = originalText;
btn.disabled = false;
};
const doCopy = () => {
try {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text, 'text');
showToast(successMsg || '复制成功', 'success');
resetBtn();
} else {
navigator.clipboard.writeText(text)
.then(() => { showToast(successMsg || '复制成功', 'success'); resetBtn(); })
.catch(() => { showToast(failMsg || '复制失败', 'error'); resetBtn(); });
}
} catch (e) {
showToast(failMsg || '复制失败', 'error');
resetBtn();
}
};
doCopy();
}
// ========== 脚本卡片 ==========
function enhanceScriptList() {
const list = document.querySelector('ol.script-list, ul.script-list, #browse-script-list');
if (!list) { log.warn('Script list not found'); return; }
list.classList.add('gfe-script-list');
const items = list.querySelectorAll('li[data-script-id]');
if (!items.length) return;
items.forEach(li => {
if (li.dataset.gfeEnhanced) return;
li.dataset.gfeEnhanced = '1';
const scriptId = li.dataset.scriptId;
const scriptName = li.dataset.scriptName || 'Unknown';
const codeUrl = li.dataset.codeUrl || '';
const rating = li.dataset.scriptRatingScore || 'N/A';
const version = li.dataset.scriptVersion || 'N/A';
const author = li.dataset.scriptAuthorName || 'unknown';
const desc = li.dataset.scriptDescription || '';
const link = li.querySelector('a.script-link');
const linkHref = link ? link.href : '#';
const html = `
⭐ ${rating}
📦 ${version}
👤 ${Utils.escapeHtml(author)}
${Utils.escapeHtml(desc)}
${codeUrl ? `
安装` : ''}
加载详情...
`;
li.innerHTML = html;
li.style.listStyle = 'none';
li.querySelector('.gfe-btn-fav').addEventListener('click', () => toggleFavorite(scriptId, scriptName));
const noteBtn = li.querySelector('.gfe-btn-note');
const panel = li.querySelector('.gfe-note-panel');
noteBtn.addEventListener('click', () => {
const isOpen = panel.style.display !== 'none';
panel.style.display = isOpen ? 'none' : 'block';
if (!isOpen) panel.querySelector('textarea').focus();
});
panel.querySelector('.note-save').addEventListener('click', () => {
const val = panel.querySelector('textarea').value;
setNote(scriptId, val);
showToast('笔记已保存', 'success');
});
panel.querySelector('.note-delete').addEventListener('click', () => {
panel.querySelector('textarea').value = '';
setNote(scriptId, '');
showToast('笔记已删除', 'success');
panel.style.display = 'none';
});
const detailBtn = li.querySelector('.gfe-btn-detail');
const detailPanel = li.querySelector('.gfe-detail-panel');
detailBtn.addEventListener('click', async () => {
if (detailPanel.style.display !== 'none') {
detailPanel.style.display = 'none';
return;
}
detailPanel.style.display = 'block';
detailPanel.textContent = '加载详情...';
try {
const info = await fetchScriptInfo(scriptId);
detailPanel.innerHTML = `
描述:${info.description || '无描述'}
安装量:${info.total_installs || 0}
更新:${info.code_updated_at ? new Date(info.code_updated_at).toLocaleString() : '未知'}
`;
} catch(e) {
detailPanel.textContent = '加载失败';
}
});
li.querySelector('.gfe-btn-filter').addEventListener('click', () => {
const val = prompt('输入过滤规则 (如 scriptId=123)', 'scriptId=' + scriptId);
if (val) {
const parts = val.split('=');
if (parts.length === 2) {
addFilter(parts[0].trim(), parts[1].trim());
applyFilters();
showToast('过滤规则已添加', 'success');
}
}
});
const bookmarkBtn = li.querySelector('.gfe-btn-bookmark');
bookmarkBtn.addEventListener('click', () => {
toggleBookmark(scriptId, scriptName, linkHref);
});
updateBookmarkButton(bookmarkBtn, scriptId);
});
// 卡片样式通过 CSS 变量自动应用,无需逐个设置
}
async function fetchScriptInfo(scriptId) {
const resp = await fetch(`/scripts/${scriptId}.json`);
if (!resp.ok) throw new Error('Network error');
return await resp.json();
}
// ========== 书签管理 ==========
const BOOKMARK_KEY = 'gfe_bookmarks';
function getBookmarks() { return Config.get(BOOKMARK_KEY, {}); }
function setBookmarks(bm) { Config.set(BOOKMARK_KEY, bm); }
function isBookmarked(scriptId) { return !!getBookmarks()[scriptId]; }
function toggleBookmark(scriptId, scriptName, url) {
const bm = getBookmarks();
if (bm[scriptId]) {
delete bm[scriptId];
setBookmarks(bm);
showToast('已移除书签', 'success');
} else {
bm[scriptId] = { name: scriptName || scriptId, url: url || `https://greasyfork.org/scripts/${scriptId}`, time: Date.now() };
setBookmarks(bm);
showToast('已添加书签', 'success');
}
updateAllBookmarkButtons();
}
function updateBookmarkButton(btn, scriptId) {
const active = isBookmarked(scriptId);
btn.classList.toggle('active', active);
btn.textContent = active ? '⭐' : '☆';
}
function updateAllBookmarkButtons() {
document.querySelectorAll('.gfe-btn-bookmark[data-script-id]').forEach(btn => {
const id = btn.dataset.scriptId;
const active = isBookmarked(id);
btn.classList.toggle('active', active);
btn.textContent = active ? '⭐' : '☆';
});
}
// ========== 代码页增强 ==========
function enhanceCodePage() {
if (!location.pathname.includes('/code')) return;
const container = document.querySelector('.code-container');
if (!container) return;
const pre = container.querySelector('pre');
if (!pre) return;
const toolbar = document.createElement('div');
toolbar.className = 'gfe-code-toolbar';
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制代码';
copyBtn.addEventListener('click', function() {
const text = pre.innerText || '';
copyTextWithFeedback(text, this, '复制成功', '复制失败');
});
toolbar.appendChild(copyBtn);
const text = pre.innerText || '';
const lines = text.split('\n').length;
const chars = text.length;
const statsSpan = document.createElement('span');
statsSpan.className = 'gfe-code-stats';
statsSpan.textContent = `代码统计: ${lines} 行, ${chars} 字符`;
toolbar.appendChild(statsSpan);
container.prepend(toolbar);
}
// ========== 版本页增强 ==========
function enhanceVersionsPage() {
if (!location.pathname.includes('/versions')) return;
document.querySelectorAll('.history_versions li').forEach(li => {
if (li.dataset.gfeEnhanced) return;
li.dataset.gfeEnhanced = '1';
const versionLink = li.querySelector('.version-number a');
if (!versionLink) return;
const date = li.querySelector('.version-date');
const changelog = li.querySelector('.version-changelog');
const versionText = versionLink.textContent.trim();
const scriptId = location.pathname.match(/\/scripts\/(\d+)/)?.[1] || '';
const installUrl = `https://update.${location.hostname}/scripts/${scriptId}/${versionText}/`;
const html = `
${changelog?.innerHTML || ''}
`;
li.innerHTML = html;
li.className = 'gfe-version-item';
});
document.addEventListener('click', async (e) => {
const btn = e.target.closest('.gfe-version-copy');
if (!btn) return;
try {
const resp = await fetch(btn.dataset.code);
const text = await resp.text();
copyTextWithFeedback(text, btn, '复制成功', '复制失败');
} catch {
showToast('复制失败', 'error');
}
});
}
// ========== 论坛增强 ==========
function enhanceForum() {
if (!location.pathname.includes('/discussions')) return;
const seen = new Set();
document.querySelectorAll('.discussion-list-container').forEach(container => {
const snippet = container.querySelector('.discussion-snippet')?.textContent?.trim();
if (!snippet) return;
if (seen.has(snippet) && Config.get('filter_duplicate', true)) {
container.style.display = 'none';
container.dataset.filtered = 'true';
} else {
seen.add(snippet);
}
});
document.querySelectorAll('.discussion-list-container').forEach(container => {
if (container.querySelector('.gfe-forum-filter-btn')) return;
const meta = container.querySelector('.discussion-meta');
if (!meta) return;
const filterBtn = document.createElement('button');
filterBtn.className = 'gfe-forum-filter-btn';
filterBtn.textContent = '过滤';
filterBtn.style.cssText = 'margin-left: 8px; padding: 2px 8px; font-size: 12px; cursor: pointer;';
filterBtn.addEventListener('click', () => {
const title = container.querySelector('.discussion-title')?.textContent?.trim() || '';
const val = prompt('输入过滤规则 (如 title=xxx)', 'title=' + title);
if (val) {
const parts = val.split('=');
if (parts.length === 2) {
addFilter(parts[0].trim(), parts[1].trim());
applyFilters();
showToast('过滤规则已添加', 'success');
}
}
});
meta.appendChild(filterBtn);
const reportBtn = document.createElement('button');
reportBtn.className = 'gfe-forum-report-btn';
reportBtn.textContent = '举报';
reportBtn.style.cssText = 'margin-left: 8px; padding: 2px 8px; font-size: 12px; cursor: pointer; color: #f56c6c;';
reportBtn.addEventListener('click', () => {
const discussionId = container.dataset.discussionId || '';
if (discussionId) {
window.open(`/reports/new?item_class=discussion&item_id=${discussionId}`, '_blank');
} else {
showToast('无法获取讨论ID', 'error');
}
});
meta.appendChild(reportBtn);
});
const hideRead = Config.get('hide_read_comments', false);
const italicRead = Config.get('italic_read_comments', false);
if (hideRead || italicRead) {
document.querySelectorAll('.discussion-list-container.discussion-read').forEach(container => {
if (hideRead) container.classList.add('discussion-read-hidden');
if (italicRead) container.classList.add('discussion-read-italic');
});
}
}
// ========== 图片查看器 ==========
function initImageViewer() {
if (typeof Viewer === 'undefined') {
log.warn('ViewerJS not loaded');
return;
}
document.addEventListener('click', (e) => {
const img = e.target.closest('img');
if (!img) return;
if (img.closest('.viewer-container') || img.closest('a')) return;
const container = img.closest('.user-content, #script-content, .gfe-note-panel') || document;
const images = container.querySelectorAll('img');
const srcs = Array.from(images).map(i => i.src || i.dataset.src);
const index = srcs.indexOf(img.src);
if (index === -1) return;
try {
const viewer = new Viewer.default(container, {
url: 'src',
zIndex: 99999,
hidden: () => viewer.destroy()
});
viewer.view(index);
} catch (err) {
log.error('Viewer error:', err);
}
});
}
// ========== Markdown 复制 ==========
function addMarkdownCopyButtons() {
document.querySelectorAll('.user-content pre, .markdown-body pre').forEach(pre => {
if (pre.querySelector('.gfe-md-copy')) return;
const btn = document.createElement('button');
btn.className = 'gfe-md-copy';
btn.textContent = '复制';
btn.style.cssText = 'position:absolute; top:4px; right:4px; padding:2px 8px; font-size:12px; background:#409eff; color:#fff; border:none; border-radius:3px; cursor:pointer; opacity:0.6; transition:opacity 0.2s;';
btn.addEventListener('mouseenter', () => btn.style.opacity = '1');
btn.addEventListener('mouseleave', () => btn.style.opacity = '0.6');
btn.addEventListener('click', function() {
const text = pre.innerText || '';
copyTextWithFeedback(text, this, '复制成功', '复制失败');
});
pre.style.position = 'relative';
pre.appendChild(btn);
});
}
// ========== 大纲导航 ==========
function initOutline() {
if (!Config.get('outline_enabled', true)) return;
const path = location.pathname;
if (!path.includes('/scripts') && !path.includes('/discussions')) return;
if (path.match(/\/scripts\/\d+/)) return;
const headings = document.querySelectorAll('body > div.width-constraint h1, h2, h3, h4, h5, h6');
if (headings.length < 3) return;
const outline = document.createElement('ul');
outline.className = 'gfe-outline';
headings.forEach(h => {
const id = h.id || h.textContent.trim().replace(/\s+/g, '-');
if (!h.id) h.id = id;
const li = document.createElement('li');
const a = document.createElement('a');
a.href = '#' + id;
a.textContent = h.textContent.trim();
li.appendChild(a);
outline.appendChild(li);
});
const container = document.querySelector('body > div.width-constraint > section');
if (container) container.prepend(outline);
}
// ========== 搜索语法 ==========
function initSearchSyntax() {
if (!Config.get('search_syntax', true)) return;
const searchInput = document.querySelector('input[name="q"][type="search"]');
if (!searchInput) return;
const form = searchInput.closest('form');
if (!form || form.method !== 'get') return;
form.addEventListener('submit', (e) => {
const raw = searchInput.value;
const pairs = raw.match(/\b(\w+:[^\s]+)\b/g) || [];
const cleaned = raw.replace(/\b\w+:[^\s]+\b/g, '').trim();
const parsed = {};
pairs.forEach(p => {
const [key, val] = p.split(':');
parsed[key.toLowerCase()] = val;
});
if (Object.keys(parsed).length === 0) return;
e.preventDefault();
const url = new URL(form.action, window.location.href);
url.searchParams.set('q', cleaned);
if (parsed.type) {
const types = { script: 'scripts', lib: 'scripts/libraries', library: 'scripts/libraries', user: 'users' };
if (types[parsed.type]) url.pathname = `/${types[parsed.type]}`;
}
if (parsed.lang) {
const langs = { js: '', javascript: '', css: 'css', all: 'all' };
if (langs[parsed.lang] !== undefined) {
if (langs[parsed.lang] === '') url.searchParams.delete('language');
else url.searchParams.set('language', langs[parsed.lang]);
}
}
if (parsed.sort) {
const sorts = { rel: '', relevant: '', day: 'daily_installs', total: 'total_installs', score: 'ratings', created: 'created', updated: 'updated', name: 'name' };
if (sorts[parsed.sort]) {
if (sorts[parsed.sort] === '') url.searchParams.delete('sort');
else url.searchParams.set('sort', sorts[parsed.sort]);
}
}
const rangeKeys = ['total', 'daily', 'rating', 'created', 'updated'];
rangeKeys.forEach(k => {
if (parsed[k]) {
const rawVal = parsed[k];
const op = rawVal[0];
const val = rawVal.slice(1);
if (['>','<','='].includes(op)) {
url.searchParams.set(`${k}_operator`, op === '>' ? 'gt' : op === '<' ? 'lt' : 'eq');
url.searchParams.set(k, val);
}
}
});
window.location.href = url.href;
});
}
// ========== 粘性分页 ==========
function initStickyPagination() {
if (!Config.get('sticky_pagination', true)) return;
const pagy = document.querySelector('.sidebarred-main-content > .pagy');
if (pagy) pagy.classList.add('gfe-sticky-pagination');
}
// ========== 总是显示通知 ==========
function initAlwaysNotify() {
if (!Config.get('always_notify', false)) return;
const nav = document.querySelector('#nav-user-info');
if (!nav) return;
const profile = nav.querySelector('.user-profile-link');
if (!profile) return;
const existing = nav.querySelector('.notification-widget');
if (existing && existing.textContent !== '0') return;
if (!existing) {
const a = document.createElement('a');
a.className = 'notification-widget';
a.textContent = '0';
a.href = profile.querySelector('a').href + '/notifications';
nav.insertBefore(a, profile);
}
}
// ========== 清理旧评论 ==========
function cleanOldComments() {
const days = Config.get('clean_old_comments_days', 30);
if (days < 0) return;
const now = Date.now();
document.querySelectorAll('#user-discussions-on-scripts-written > section > div').forEach(item => {
const relTime = item.querySelector('relative-time');
if (relTime) {
const date = new Date(relTime.date);
if (now - date.getTime() > days * 24 * 3600 * 1000) {
item.style.display = 'none';
}
}
});
}
// ========== 用户统计 ==========
function initUserStats() {
if (!location.pathname.includes('/users/')) return;
const userId = location.pathname.match(/\/users\/(\d+)/)?.[1];
if (!userId) return;
const header = document.querySelector('#about-user h2');
if (!header) return;
fetch(`/users/${userId}.json`)
.then(r => r.json())
.then(data => {
const scripts = data.scripts || [];
const js = scripts.filter(s => s.code_url.endsWith('.js')).length;
const css = scripts.filter(s => s.code_url.endsWith('.css')).length;
const total = scripts.length;
const daily = scripts.reduce((s, c) => s + (c.daily_installs || 0), 0);
const totalInstalls = scripts.reduce((s, c) => s + (c.total_installs || 0), 0);
const stats = document.createElement('div');
stats.style.cssText = 'font-size:14px; color:var(--gfe-text); margin:8px 0;';
stats.innerHTML = `
📦 ${total} 脚本
🟢 JS: ${js}
🟡 CSS: ${css}
📊 日安装: ${daily}
📈 总安装: ${totalInstalls}
`;
header.parentNode.insertBefore(stats, header.nextSibling);
})
.catch(() => {});
}
// ========== Webhook 增强 ==========
function enhanceWebhookPage() {
if (!location.pathname.includes('/webhook-info')) return;
document.querySelectorAll('.text-content dd, .text-content dd textarea').forEach(el => {
if (el.nodeName === 'TEXTAREA' && el.value) {
el.style.cssText = 'width:100%; height:80px; font-family:monospace;';
const btn = document.createElement('button');
btn.textContent = '复制';
btn.style.cssText = 'margin-top:4px; padding:2px 8px; cursor:pointer;';
btn.addEventListener('click', function() {
copyTextWithFeedback(el.value, this, '复制成功', '复制失败');
});
el.parentNode.appendChild(btn);
}
});
}
// ========== 显示版本号 ==========
function showVersionInList() {
if (!Config.get('show_version', false)) return;
document.querySelectorAll('.script-list > li[data-script-version]').forEach(li => {
const version = li.dataset.scriptVersion;
if (version) {
const span = document.createElement('span');
span.textContent = ` v${version}`;
span.style.cssText = 'font-size:12px; color:#909399; margin-left:4px;';
const title = li.querySelector('.script-link');
if (title) title.parentNode.insertBefore(span, title.nextSibling);
}
});
}
// ========== 扁平布局 ==========
function applyFlatLayout() {
if (Config.get('flat_layout', false)) {
document.documentElement.classList.add('gfe-flat-layout');
}
}
// ========== 书签页面 ==========
function showBookmarksPage() {
if (location.pathname.includes('/404?Bookmarks')) {
const bm = getBookmarks();
const container = document.querySelector('section.text-content');
if (!container) return;
container.innerHTML = '📖 书签
';
for (const id in bm) {
const item = bm[id];
container.innerHTML += `- ${item.name} ${new Date(item.time).toLocaleString()}
`;
}
container.innerHTML += '
';
}
}
// ========== 设置面板 ==========
function showSettings() {
const container = document.createElement('div');
container.style.cssText = 'padding: 16px; max-width: 650px; margin: 0 auto; font-family: system-ui, -apple-system, sans-serif;';
const fields = [
// 常规
{ key: 'theme', label: '主题', type: 'select', options: ['light', 'dark', 'system'], default: 'system' },
{ key: 'language', label: '语言', type: 'select', options: ['zh-CN', 'en-US'], default: 'zh-CN' },
{ key: 'auto_login', label: '自动登录', type: 'checkbox', default: true },
{ key: 'account', label: '账号', type: 'text', default: '' },
{ key: 'password', label: '密码', type: 'password', default: '' },
{ key: 'secret', label: '2FA 密钥', type: 'text', default: '' },
// 显示
{ key: 'dual_column', label: '双列显示', type: 'checkbox', default: true },
{ key: 'image_viewer', label: '图片浏览增强', type: 'checkbox', default: true },
{ key: 'markdown_copy', label: 'Markdown 复制按钮', type: 'checkbox', default: true },
{ key: 'show_version', label: '显示版本号', type: 'checkbox', default: false },
// 论坛
{ key: 'forum_filter', label: '论坛过滤', type: 'checkbox', default: true },
{ key: 'filter_duplicate', label: '过滤重复评论', type: 'checkbox', default: true },
// 高级
{ key: 'density', label: '密度', type: 'select', options: ['compact', 'comfortable', 'detailed'], default: 'comfortable' },
{ key: 'sticky_pagination', label: '粘性分页', type: 'checkbox', default: true },
{ key: 'flat_layout', label: '扁平布局', type: 'checkbox', default: false },
{ key: 'always_notify', label: '总是显示通知', type: 'checkbox', default: false },
{ key: 'hide_read_comments', label: '论坛阅读增强(隐藏/斜体已读)', type: 'checkbox', default: false },
{ key: 'italic_read_comments', label: '', type: 'hidden' },
{ key: 'regex_filter', label: '正则过滤', type: 'text', default: '' },
{ key: 'clean_old_comments_days', label: '清理旧评论(天)', type: 'number', default: 30 },
{ key: 'outline_enabled', label: '大纲导航', type: 'checkbox', default: true },
{ key: 'search_syntax', label: '搜索语法', type: 'checkbox', default: true },
{ key: 'code_enhance', label: '代码增强(复制+统计)', type: 'checkbox', default: true },
];
let html = `⚙️ 设置
`;
html += ``;
// 常规
html += `📋 常规
`;
['theme', 'language', 'auto_login'].forEach(k => {
const f = fields.find(f => f.key === k);
if (!f) return;
const val = Config.get(f.key, f.default);
const id = `gfe_${f.key}`;
if (f.type === 'select') {
html += `
`;
} else if (f.type === 'checkbox') {
html += `
`;
}
});
// 账号/密码/2FA
['account', 'password', 'secret'].forEach(k => {
const f = fields.find(f => f.key === k);
if (!f) return;
const val = Config.get(f.key, f.default);
const id = `gfe_${f.key}`;
const type = f.type === 'password' ? 'password' : 'text';
html += `
`;
});
html += `
`;
// 显示
html += `🖥️ 显示
`;
['dual_column', 'image_viewer', 'markdown_copy', 'show_version'].forEach(k => {
const f = fields.find(f => f.key === k);
if (!f) return;
const val = Config.get(f.key, f.default);
const id = `gfe_${f.key}`;
html += `
`;
});
html += `
`;
// 论坛
html += `💬 论坛
`;
['forum_filter', 'filter_duplicate'].forEach(k => {
const f = fields.find(f => f.key === k);
if (!f) return;
const val = Config.get(f.key, f.default);
const id = `gfe_${f.key}`;
html += `
`;
});
html += `
`;
// 高级(折叠)
html += `⚙️ 高级设置
`;
['density', 'sticky_pagination', 'flat_layout', 'always_notify', 'hide_read_comments', 'regex_filter', 'clean_old_comments_days', 'outline_enabled', 'search_syntax', 'code_enhance'].forEach(k => {
const f = fields.find(f => f.key === k);
if (!f) return;
const val = Config.get(f.key, f.default);
const id = `gfe_${f.key}`;
if (f.type === 'select') {
html += `
`;
} else if (f.type === 'checkbox') {
html += `
`;
} else if (f.type === 'text') {
html += `
`;
} else if (f.type === 'number') {
html += `
`;
}
});
html += `
`;
html += `
`;
html += ``;
// 渲染覆盖层
const overlay = document.createElement('div');
overlay.id = 'gfe-settings-overlay';
overlay.style.cssText = `position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:999999; display:flex; align-items:center; justify-content:center; animation:gfeFadeIn 0.2s ease;`;
const box = document.createElement('div');
box.style.cssText = `background:var(--gfe-card, #fff); color:var(--gfe-text, #333); border-radius:12px; padding:24px; max-width:650px; width:90%; max-height:80vh; overflow-y:auto; box-shadow:0 8px 32px rgba(0,0,0,0.3);`;
box.innerHTML = html;
overlay.appendChild(box);
document.body.appendChild(overlay);
// 搜索
const searchInput = box.querySelector('#gfe-settings-search');
searchInput.addEventListener('input', () => {
const q = searchInput.value.toLowerCase();
box.querySelectorAll('.gfe-setting-item').forEach(item => {
const label = item.querySelector('label')?.textContent?.toLowerCase() || '';
item.style.display = label.includes(q) ? '' : 'none';
});
});
// 保存
box.querySelector('#gfe-save-settings').addEventListener('click', () => {
fields.forEach(f => {
const el = document.getElementById(`gfe_${f.key}`);
if (!el) return;
let val;
if (f.type === 'checkbox') val = el.checked;
else if (f.type === 'select') val = el.value;
else if (f.type === 'number') val = parseInt(el.value) || 0;
else val = el.value;
Config.set(f.key, val);
if (f.key === 'theme') applyTheme(val);
if (f.key === 'flat_layout') document.documentElement.classList.toggle('gfe-flat-layout', val);
if (f.key === 'show_version') {
if (val) showVersionInList();
else document.querySelectorAll('.script-list > li[data-script-version] span.v-version').forEach(el => el.remove());
}
if (f.key === 'sticky_pagination') {
const pagy = document.querySelector('.sidebarred-main-content > .pagy');
if (pagy) pagy.classList.toggle('gfe-sticky-pagination', val);
}
if (f.key === 'always_notify') {
if (val) initAlwaysNotify();
else document.querySelector('.notification-widget')?.remove();
}
if (f.key === 'clean_old_comments_days') {
if (location.pathname.includes('/users/')) cleanOldComments();
}
if (f.key === 'hide_read_comments' || f.key === 'italic_read_comments') {
location.reload();
}
if (f.key === 'regex_filter') applyFilters();
});
showToast('设置已保存', 'success');
overlay.remove();
});
// 导出
box.querySelector('#gfe-export-settings').addEventListener('click', () => {
const data = Config.getAll();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `greasyfork_settings_${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
});
// 导入
box.querySelector('#gfe-import-settings').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = () => {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
const all = Config.getAll();
Object.assign(all, data);
Config.set(CONFIG_KEY, JSON.stringify(all));
showToast('配置导入成功', 'success');
box.querySelector('#gfe-close-settings')?.click();
setTimeout(() => showSettings(), 500);
} catch(err) {
showToast('配置导入失败', 'error');
}
};
reader.readAsText(file);
};
input.click();
});
box.querySelector('#gfe-close-settings').addEventListener('click', () => {
overlay.remove();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) overlay.remove();
});
}
// ========== 自动登录 ==========
async function autoLogin() {
if (!Config.get('auto_login', true)) return;
const account = Config.get('account', '');
const password = Config.get('password', '');
const secret = Config.get('secret', '');
if (!account || !password) return;
if (document.querySelector('#nav-user-info .user-profile-link')) return;
const csrf = document.querySelector('meta[name="csrf-token"]');
if (!csrf) return;
const token = csrf.getAttribute('content');
let otp = '';
if (secret && typeof OTPAuth !== 'undefined') {
try {
const totp = new OTPAuth.TOTP({ secret });
otp = totp.generate();
} catch {}
}
const data = new URLSearchParams();
data.append('authenticity_token', token);
data.append('user[email]', account);
data.append('user[password]', password);
if (otp) data.append('user[otp_attempt]', otp);
data.append('user[remember_me]', '1');
data.append('commit', '登录');
try {
// 使用相对路径,由服务端根据当前会话自动重定向
const resp = await fetch('/users/sign_in', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: data.toString(),
});
if (resp.ok && resp.url.includes('/users/')) {
showToast('登录成功,即将刷新', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast('登录失败,请检查账号密码', 'error');
}
} catch (err) {
log.error('Login error:', err);
}
}
// ========== 脚本同步 ==========
async function openMyScripts() {
if (!document.querySelector('#nav-user-info .user-profile-link')) {
showToast('请先登录', 'error');
return;
}
const profileLink = document.querySelector('#nav-user-info .user-profile-link a');
if (profileLink) {
window.location.href = profileLink.href + '/scripts';
} else {
showToast('无法获取用户页面', 'error');
}
}
// ========== 初始化 ==========
function init() {
log.info('Starting Companion v3.8.4...');
injectStyles();
initTheme();
initTooltip();
const path = location.pathname;
showBookmarksPage();
if (path.match(/\/scripts(\/|$)/) && !path.includes('/code') && !path.includes('/versions')) {
setTimeout(() => {
enhanceScriptList();
applyFilters();
showVersionInList();
applyFlatLayout();
}, 300);
const observer = Utils.observe(document.body, { childList: true, subtree: true }, () => {
if (document.querySelector('ol.script-list, ul.script-list, #browse-script-list')) {
enhanceScriptList();
applyFilters();
}
});
setTimeout(() => observer.disconnect(), 10000);
} else if (path.includes('/code')) {
if (Config.get('code_enhance', true)) enhanceCodePage();
} else if (path.includes('/versions')) {
if (Config.get('version_enhance', true)) enhanceVersionsPage();
} else if (path.includes('/discussions')) {
if (Config.get('forum_filter', true)) enhanceForum();
applyFilters();
} else if (path.includes('/users/')) {
initUserStats();
cleanOldComments();
} else if (path.includes('/webhook-info')) {
enhanceWebhookPage();
}
if (Config.get('image_viewer', true)) initImageViewer();
if (Config.get('markdown_copy', true)) {
setTimeout(addMarkdownCopyButtons, 500);
}
initOutline();
initSearchSyntax();
initStickyPagination();
initAlwaysNotify();
Utils.registerMenu('⚙️ 设置', showSettings);
Utils.registerMenu('📂 我的脚本', openMyScripts);
Utils.registerMenu('📖 书签', () => {
window.location.href = '/404?Bookmarks';
});
setTimeout(autoLogin, 500);
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
applyFilters();
if (location.pathname.match(/\/scripts(\/|$)/) && !location.pathname.includes('/code') && !location.pathname.includes('/versions')) {
setTimeout(enhanceScriptList, 300);
}
}
}, 1000);
log.info('Init complete');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();