Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
3375 lines
129 KiB
JavaScript
3375 lines
129 KiB
JavaScript
(function() {
|
||
if (window.magnetPluginInitialized) return;
|
||
window.magnetPluginInitialized = true;
|
||
|
||
var DEBUG_MODE = localStorage.getItem('magnetDebugMode') === 'true';
|
||
function log(msg) { if (DEBUG_MODE) console.log('[MagnetPlugin] ' + msg); }
|
||
|
||
function escapeHtml(text) {
|
||
return String(text || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function ensurePanelStyles() {
|
||
if (document.getElementById('magnet-panel-style')) {
|
||
return;
|
||
}
|
||
|
||
var style = document.createElement('style');
|
||
style.id = 'magnet-panel-style';
|
||
style.textContent = [
|
||
/* === 字体引入 === */
|
||
'@import url("https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap");',
|
||
|
||
/* === CSS 变量 === */
|
||
':root {',
|
||
' --m-bg-deep: #0a0e14;',
|
||
' --m-bg-primary: #0f1419;',
|
||
' --m-bg-secondary: #1a1f2e;',
|
||
' --m-bg-card: rgba(26, 31, 46, 0.92);',
|
||
' --m-bg-card-hover: rgba(35, 41, 58, 0.95);',
|
||
' --m-accent: #00d4aa;',
|
||
' --m-accent-glow: rgba(0, 212, 170, 0.4);',
|
||
' --m-accent-secondary: #a78bfa;',
|
||
' --m-text-primary: #f0f4f8;',
|
||
' --m-text-secondary: #8892a4;',
|
||
' --m-text-muted: #5c6578;',
|
||
' --m-border: rgba(255, 255, 255, 0.06);',
|
||
' --m-border-accent: rgba(0, 212, 170, 0.3);',
|
||
' --m-success: #10b981;',
|
||
' --m-error: #ef4444;',
|
||
' --m-warning: #f59e0b;',
|
||
' --m-font-display: "Rajdhani", "Microsoft YaHei", sans-serif;',
|
||
' --m-font-body: "Noto Sans SC", "Microsoft YaHei", sans-serif;',
|
||
' --m-radius-sm: 8px;',
|
||
' --m-radius-md: 12px;',
|
||
' --m-radius-lg: 16px;',
|
||
' --m-radius-xl: 20px;',
|
||
' --m-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);',
|
||
' --m-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);',
|
||
' --m-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5);',
|
||
' --m-shadow-glow: 0 0 20px var(--m-accent-glow), 0 0 40px rgba(0, 212, 170, 0.15);',
|
||
'}',
|
||
|
||
/* === 悬浮球 === */
|
||
'#magnet-float-ball{',
|
||
' position:fixed;bottom:24px;right:24px;',
|
||
' width:60px;height:60px;',
|
||
' background:linear-gradient(135deg, #0f1419 0%, #1a1f2e 100%);',
|
||
' border:2px solid var(--m-accent);',
|
||
' border-radius:50%;',
|
||
' box-shadow:var(--m-shadow-glow), inset 0 0 20px rgba(0, 212, 170, 0.1);',
|
||
' z-index:2147483647;',
|
||
' cursor:pointer;',
|
||
' display:flex;align-items:center;justify-content:center;',
|
||
' color:var(--m-accent);',
|
||
' font-size:24px;',
|
||
' font-family:var(--m-font-display);',
|
||
' font-weight:700;',
|
||
' transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1);',
|
||
' animation:magnet-pulse 2.5s ease-in-out infinite;',
|
||
'}',
|
||
'#magnet-float-ball:hover{',
|
||
' transform:scale(1.08);',
|
||
' box-shadow:0 0 30px var(--m-accent-glow), 0 0 60px rgba(0, 212, 170, 0.2), inset 0 0 30px rgba(0, 212, 170, 0.15);',
|
||
'}',
|
||
'#magnet-float-ball:active{transform:scale(0.95);}',
|
||
|
||
/* === 脉冲动画 === */
|
||
'@keyframes magnet-pulse{',
|
||
' 0%, 100%{box-shadow:var(--m-shadow-glow), inset 0 0 20px rgba(0, 212, 170, 0.1);}',
|
||
' 50%{box-shadow:0 0 30px var(--m-accent-glow), 0 0 50px rgba(0, 212, 170, 0.2), inset 0 0 25px rgba(0, 212, 170, 0.15);}',
|
||
'}',
|
||
|
||
/* === 主面板 === */
|
||
'#magnet-floating-panel{',
|
||
' position:fixed;right:20px;bottom:20px;',
|
||
' width:min(800px, calc(100vw - 40px));',
|
||
' height:min(85vh, 860px);',
|
||
' background:var(--m-bg-primary);',
|
||
' border:1px solid var(--m-border-accent);',
|
||
' border-radius:var(--m-radius-xl);',
|
||
' box-shadow:var(--m-shadow-lg), 0 0 80px rgba(0, 212, 170, 0.08);',
|
||
' z-index:2147483647;',
|
||
' font-family:var(--m-font-body);',
|
||
' font-size:13px;',
|
||
' color:var(--m-text-primary);',
|
||
' display:none;flex-direction:column;',
|
||
' overflow:hidden;',
|
||
' backdrop-filter:blur(20px);',
|
||
' animation:magnet-panel-in 0.4s cubic-bezier(0.4, 0, 0.2, 1);',
|
||
'}',
|
||
'@keyframes magnet-panel-in{',
|
||
' from{opacity:0;transform:translateY(20px) scale(0.98);}',
|
||
' to{opacity:1;transform:translateY(0) scale(1);}',
|
||
'}',
|
||
|
||
/* === 面板头部 === */
|
||
'#magnet-floating-panel .magnet-panel-header{',
|
||
' display:flex;justify-content:space-between;align-items:flex-start;',
|
||
' padding:20px 24px 16px;',
|
||
' background:linear-gradient(180deg, rgba(0, 212, 170, 0.08) 0%, transparent 100%);',
|
||
' border-bottom:1px solid var(--m-border);',
|
||
' gap:16px;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-brand{',
|
||
' display:flex;flex-direction:column;gap:6px;min-width:0;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-title{',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:24px;font-weight:700;',
|
||
' letter-spacing:1px;',
|
||
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 50%, var(--m-accent-secondary) 100%);',
|
||
' -webkit-background-clip:text;',
|
||
' -webkit-text-fill-color:transparent;',
|
||
' background-clip:text;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-subtitle{',
|
||
' font-size:12px;color:var(--m-text-secondary);line-height:1.5;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-head-actions{',
|
||
' display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;',
|
||
'}',
|
||
|
||
/* === 切换按钮 === */
|
||
'#magnet-floating-panel .magnet-panel-switch{',
|
||
' padding:10px 18px;',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-lg);',
|
||
' background:var(--m-bg-card);',
|
||
' color:var(--m-text-secondary);',
|
||
' cursor:pointer;',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:13px;font-weight:600;',
|
||
' letter-spacing:0.5px;',
|
||
' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-switch:hover{',
|
||
' background:var(--m-bg-card-hover);',
|
||
' border-color:var(--m-accent);',
|
||
' color:var(--m-text-primary);',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-switch.is-active{',
|
||
' background:linear-gradient(135deg, rgba(0, 212, 170, 0.2) 0%, rgba(0, 212, 170, 0.1) 100%);',
|
||
' border-color:var(--m-accent);',
|
||
' color:var(--m-accent);',
|
||
' box-shadow:0 0 15px rgba(0, 212, 170, 0.2);',
|
||
'}',
|
||
|
||
/* === 关闭按钮 === */
|
||
'#magnet-floating-panel .magnet-panel-close{',
|
||
' width:36px;height:36px;',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-md);',
|
||
' background:var(--m-bg-card);',
|
||
' color:var(--m-text-secondary);',
|
||
' cursor:pointer;',
|
||
' font-size:20px;line-height:1;',
|
||
' transition:all 0.2s ease;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-panel-close:hover{',
|
||
' background:rgba(239, 68, 68, 0.15);',
|
||
' border-color:var(--m-error);',
|
||
' color:var(--m-error);',
|
||
'}',
|
||
|
||
/* === 设置区域 === */
|
||
'#magnet-settings{',
|
||
' padding:16px 20px;',
|
||
' background:var(--m-bg-secondary);',
|
||
' border-bottom:1px solid var(--m-border);',
|
||
' display:flex;flex-direction:column;gap:12px;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-control-row{',
|
||
' display:flex;gap:12px;align-items:center;flex-wrap:wrap;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-control-row > *{min-width:0;}',
|
||
|
||
/* === 输入框样式 === */
|
||
'#magnet-settings input[type="text"],',
|
||
'#magnet-settings input[type="number"],',
|
||
'#magnet-settings select{',
|
||
' padding:10px 14px;',
|
||
' background:var(--m-bg-primary);',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-md);',
|
||
' color:var(--m-text-primary);',
|
||
' font-family:var(--m-font-body);',
|
||
' font-size:13px;',
|
||
' transition:all 0.2s ease;',
|
||
'}',
|
||
'#magnet-settings input:focus,',
|
||
'#magnet-settings select:focus{',
|
||
' outline:none;',
|
||
' border-color:var(--m-accent);',
|
||
' box-shadow:0 0 0 3px rgba(0, 212, 170, 0.15);',
|
||
'}',
|
||
'#magnet-settings input::placeholder{color:var(--m-text-muted);}',
|
||
|
||
/* === 主按钮 === */
|
||
'#magnet-settings button:not(.magnet-panel-switch){',
|
||
' padding:10px 20px;',
|
||
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);',
|
||
' border:none;',
|
||
' border-radius:var(--m-radius-md);',
|
||
' color:var(--m-bg-deep);',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:14px;font-weight:700;',
|
||
' letter-spacing:0.5px;',
|
||
' cursor:pointer;',
|
||
' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);',
|
||
'}',
|
||
'#magnet-settings button:not(.magnet-panel-switch):hover{',
|
||
' transform:translateY(-2px);',
|
||
' box-shadow:0 8px 25px rgba(0, 212, 170, 0.4);',
|
||
'}',
|
||
'#magnet-settings button:not(.magnet-panel-switch):active{',
|
||
' transform:translateY(0);',
|
||
'}',
|
||
|
||
/* === 内容区域 === */
|
||
'#magnet-floating-panel .magnet-panel-content{',
|
||
' flex:1;min-height:0;',
|
||
' padding:16px 20px;',
|
||
' background:var(--m-bg-primary);',
|
||
' overflow:hidden;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-view{',
|
||
' display:none;height:100%;flex-direction:column;gap:14px;min-height:0;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-view.is-active{display:flex;}',
|
||
|
||
/* === 视图工具栏 === */
|
||
'#magnet-floating-panel .magnet-view-toolbar{',
|
||
' display:flex;justify-content:space-between;align-items:center;',
|
||
' gap:12px;padding:14px 18px;',
|
||
' background:var(--m-bg-card);',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-lg);',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-view-toolbar-actions{',
|
||
' display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-view-title{',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:16px;font-weight:700;',
|
||
' color:var(--m-text-primary);',
|
||
' letter-spacing:0.5px;',
|
||
'}',
|
||
'#magnet-floating-panel .magnet-view-meta{',
|
||
' font-size:12px;color:var(--m-text-secondary);',
|
||
'}',
|
||
|
||
/* === 磁力列表 === */
|
||
'#magnet-list{',
|
||
' display:flex;flex-direction:column;gap:10px;',
|
||
' min-height:0;overflow-y:auto;padding-right:6px;',
|
||
' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);',
|
||
'}',
|
||
'#magnet-list::-webkit-scrollbar{width:6px;}',
|
||
'#magnet-list::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}',
|
||
'#magnet-list::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}',
|
||
|
||
/* === 列表项 === */
|
||
'.magnet-item{',
|
||
' display:flex;align-items:flex-start;gap:14px;',
|
||
' padding:14px 16px;',
|
||
' background:var(--m-bg-card);',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-lg);',
|
||
' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);',
|
||
' animation:magnet-item-in 0.3s ease forwards;',
|
||
' opacity:0;',
|
||
'}',
|
||
'@keyframes magnet-item-in{',
|
||
' from{opacity:0;transform:translateX(-10px);}',
|
||
' to{opacity:1;transform:translateX(0);}',
|
||
'}',
|
||
'.magnet-item:hover{',
|
||
' background:var(--m-bg-card-hover);',
|
||
' border-color:var(--m-border-accent);',
|
||
' transform:translateX(4px);',
|
||
'}',
|
||
'.magnet-title{',
|
||
' flex:1;cursor:pointer;',
|
||
' color:var(--m-text-primary);',
|
||
' min-width:0;font-size:13px;line-height:1.6;',
|
||
' word-break:break-all;font-weight:500;',
|
||
' transition:color 0.2s ease;',
|
||
'}',
|
||
'.magnet-title:hover{color:var(--m-accent);}',
|
||
|
||
/* === 复制按钮 === */
|
||
'.magnet-copy-btn{',
|
||
' padding:8px 16px;',
|
||
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);',
|
||
' color:var(--m-bg-deep);',
|
||
' border:none;border-radius:var(--m-radius-md);',
|
||
' cursor:pointer;',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:12px;font-weight:700;',
|
||
' white-space:nowrap;flex-shrink:0;',
|
||
' transition:all 0.2s ease;',
|
||
'}',
|
||
'.magnet-copy-btn:hover{',
|
||
' transform:scale(1.05);',
|
||
' box-shadow:0 4px 15px rgba(0, 212, 170, 0.4);',
|
||
'}',
|
||
'.magnet-copy-btn:active{transform:scale(0.98);}',
|
||
'.magnet-download-btn{',
|
||
' padding:8px 16px;',
|
||
' background:linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);',
|
||
' color:#fff;',
|
||
' border:none;border-radius:var(--m-radius-md);',
|
||
' cursor:pointer;',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:12px;font-weight:700;',
|
||
' white-space:nowrap;flex-shrink:0;',
|
||
' transition:all 0.2s ease;',
|
||
'}',
|
||
'.magnet-download-btn:hover{',
|
||
' transform:scale(1.05);',
|
||
' box-shadow:0 4px 15px rgba(37, 99, 235, 0.35);',
|
||
'}',
|
||
'.magnet-download-btn:active{transform:scale(0.98);}',
|
||
|
||
/* === 缓存面板 === */
|
||
'#magnet-cache-panel{',
|
||
' flex:1;min-height:0;overflow-y:auto;padding-right:6px;',
|
||
' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);',
|
||
'}',
|
||
'#magnet-cache-panel::-webkit-scrollbar{width:6px;}',
|
||
'#magnet-cache-panel::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}',
|
||
'#magnet-cache-panel::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}',
|
||
|
||
/* === 缓存网格 === */
|
||
'.magnet-cache-grid{',
|
||
' display:grid;',
|
||
' grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));',
|
||
' gap:12px;margin-bottom:16px;',
|
||
'}',
|
||
'.magnet-cache-card{',
|
||
' padding:16px;',
|
||
' background:var(--m-bg-card);',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-lg);',
|
||
' transition:all 0.2s ease;',
|
||
'}',
|
||
'.magnet-cache-card:hover{',
|
||
' border-color:var(--m-border-accent);',
|
||
'}',
|
||
'.magnet-cache-card-label{',
|
||
' font-size:11px;color:var(--m-text-muted);margin-bottom:8px;',
|
||
' text-transform:uppercase;letter-spacing:0.5px;',
|
||
'}',
|
||
'.magnet-cache-card-value{',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:22px;font-weight:700;',
|
||
' color:var(--m-accent);',
|
||
'}',
|
||
|
||
/* === 缓存区块 === */
|
||
'.magnet-cache-section{margin-top:16px;}',
|
||
'.magnet-cache-section-title{',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:13px;font-weight:700;',
|
||
' color:var(--m-text-secondary);',
|
||
' margin-bottom:10px;letter-spacing:0.5px;',
|
||
'}',
|
||
'.magnet-cache-entry{',
|
||
' padding:12px 14px;margin-top:8px;',
|
||
' background:var(--m-bg-card);',
|
||
' border:1px solid var(--m-border);',
|
||
' border-radius:var(--m-radius-md);',
|
||
' transition:all 0.2s ease;',
|
||
'}',
|
||
'.magnet-cache-entry:hover{',
|
||
' border-color:var(--m-border-accent);',
|
||
'}',
|
||
'.magnet-cache-entry-title{',
|
||
' font-size:13px;font-weight:600;',
|
||
' color:var(--m-text-primary);',
|
||
' line-height:1.5;word-break:break-all;',
|
||
'}',
|
||
'.magnet-cache-entry-meta{',
|
||
' font-size:11px;color:var(--m-text-muted);',
|
||
' margin-top:6px;line-height:1.5;',
|
||
'}',
|
||
|
||
/* === 底部 === */
|
||
'#magnet-floating-panel .magnet-panel-footer{',
|
||
' padding:16px 20px 20px;',
|
||
' background:var(--m-bg-secondary);',
|
||
' border-top:1px solid var(--m-border);',
|
||
' display:flex;flex-direction:column;gap:12px;',
|
||
'}',
|
||
|
||
/* === 状态栏 === */
|
||
'#magnet-status{',
|
||
' padding:12px 16px;border-radius:var(--m-radius-md);',
|
||
' font-size:12px;line-height:1.6;',
|
||
' background:var(--m-bg-card);',
|
||
' border:1px solid var(--m-border);',
|
||
' color:var(--m-text-secondary);',
|
||
'}',
|
||
'#magnet-status[data-type="loading"]{',
|
||
' border-color:var(--m-warning);',
|
||
' color:var(--m-warning);',
|
||
' background:rgba(245, 158, 11, 0.1);',
|
||
'}',
|
||
'#magnet-status[data-type="error"]{',
|
||
' border-color:var(--m-error);',
|
||
' color:var(--m-error);',
|
||
' background:rgba(239, 68, 68, 0.1);',
|
||
'}',
|
||
'#magnet-status[data-type="done"]{',
|
||
' border-color:var(--m-success);',
|
||
' color:var(--m-success);',
|
||
' background:rgba(16, 185, 129, 0.1);',
|
||
'}',
|
||
|
||
/* === 一键复制按钮 === */
|
||
'#magnet-copy-all{',
|
||
' width:100%;padding:14px 20px;',
|
||
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);',
|
||
' color:var(--m-bg-deep);',
|
||
' border:none;border-radius:var(--m-radius-lg);',
|
||
' cursor:pointer;',
|
||
' font-family:var(--m-font-display);',
|
||
' font-size:15px;font-weight:700;',
|
||
' letter-spacing:0.5px;',
|
||
' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);',
|
||
'}',
|
||
'#magnet-copy-all:hover{',
|
||
' transform:translateY(-2px);',
|
||
' box-shadow:0 10px 30px rgba(0, 212, 170, 0.4);',
|
||
'}',
|
||
'#magnet-copy-all:active{transform:translateY(0);}',
|
||
|
||
/* === 调试菜单 === */
|
||
'#magnet-debug-menu{',
|
||
' background:var(--m-bg-card) !important;',
|
||
' border:1px solid var(--m-border-accent) !important;',
|
||
' border-radius:var(--m-radius-md) !important;',
|
||
' box-shadow:var(--m-shadow-lg) !important;',
|
||
'}',
|
||
'#magnet-debug-menu label{',
|
||
' color:var(--m-text-primary) !important;',
|
||
'}',
|
||
'#magnet-debug-menu input[type="checkbox"]{',
|
||
' accent-color:var(--m-accent);',
|
||
'}',
|
||
|
||
/* === 响应式 === */
|
||
'@media (max-width: 900px){',
|
||
' #magnet-floating-panel{',
|
||
' right:10px;bottom:10px;',
|
||
' width:calc(100vw - 20px);',
|
||
' height:min(88vh, 800px);',
|
||
' }',
|
||
' #magnet-floating-panel .magnet-panel-header{padding:16px 18px 14px;}',
|
||
' #magnet-settings,',
|
||
' #magnet-floating-panel .magnet-panel-content,',
|
||
' #magnet-floating-panel .magnet-panel-footer{padding-left:16px;padding-right:16px;}',
|
||
'}',
|
||
|
||
/* === 空状态 === */
|
||
'.magnet-empty-state{',
|
||
' display:flex;flex-direction:column;align-items:center;justify-content:center;',
|
||
' padding:40px 20px;text-align:center;',
|
||
' color:var(--m-text-muted);',
|
||
'}',
|
||
'.magnet-empty-state-icon{',
|
||
' font-size:48px;margin-bottom:16px;opacity:0.5;',
|
||
'}',
|
||
'.magnet-empty-state-text{',
|
||
' font-size:14px;line-height:1.6;',
|
||
'}',
|
||
|
||
/* === 进度条 === */
|
||
'.magnet-progress-container{',
|
||
' width:100%;height:6px;background:var(--m-bg-secondary);border-radius:999px;overflow:hidden;',
|
||
'}',
|
||
'.magnet-progress-bar{',
|
||
' width:0%;height:100%;background:linear-gradient(90deg, var(--m-accent), #00f5c4);transition:width 0.25s ease;',
|
||
'}',
|
||
'.magnet-progress-text{',
|
||
' display:flex;justify-content:space-between;gap:12px;font-size:11px;color:var(--m-text-muted);',
|
||
'}',
|
||
|
||
/* === 结果项操作 === */
|
||
'.magnet-item-actions{',
|
||
' display:flex;gap:6px;flex-shrink:0;',
|
||
'}',
|
||
'.magnet-favorite-btn{',
|
||
' width:36px;height:36px;flex:0 0 36px;padding:0;display:inline-flex;align-items:center;justify-content:center;line-height:1;background:transparent;border:1px solid var(--m-border);border-radius:var(--m-radius-md);cursor:pointer;font-size:16px;color:var(--m-text-muted);transition:all 0.2s ease;',
|
||
'}',
|
||
'.magnet-favorite-btn:hover{',
|
||
' border-color:rgba(239,68,68,0.5);color:#ff7b8a;background:rgba(239,68,68,0.08);',
|
||
'}',
|
||
'.magnet-favorite-btn.is-favorite{',
|
||
' color:#ff4d6d;border-color:rgba(255,77,109,0.55);background:rgba(255,77,109,0.12);box-shadow:0 0 12px rgba(255,77,109,0.18);',
|
||
'}',
|
||
|
||
/* === 收藏视图 === */
|
||
'#magnet-favorites-list{',
|
||
' flex:1;min-height:0;overflow-y:auto;padding-right:6px;',
|
||
' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);',
|
||
'}',
|
||
'#magnet-favorites-list::-webkit-scrollbar{width:6px;}',
|
||
'#magnet-favorites-list::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}',
|
||
'#magnet-favorites-list::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}',
|
||
'.magnet-favorite-item{',
|
||
' display:flex;align-items:center;gap:10px;padding:12px 14px;margin-bottom:8px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);',
|
||
'}',
|
||
'.magnet-favorite-item:hover{',
|
||
' border-color:var(--m-border-accent);',
|
||
'}',
|
||
'.magnet-favorite-title{',
|
||
' flex:1;min-width:0;font-size:12px;color:var(--m-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;',
|
||
'}',
|
||
'.magnet-favorite-actions{',
|
||
' display:flex;gap:6px;flex-shrink:0;',
|
||
'}',
|
||
'.magnet-favorite-copy,.magnet-favorite-remove{',
|
||
' padding:6px 10px;border:none;border-radius:8px;cursor:pointer;font-size:11px;font-weight:700;',
|
||
'}',
|
||
'.magnet-favorite-copy{',
|
||
' background:linear-gradient(135deg, var(--m-accent), #00f5c4);color:var(--m-bg-deep);',
|
||
'}',
|
||
'.magnet-favorite-remove{',
|
||
' background:rgba(239,68,68,0.14);color:var(--m-error);',
|
||
'}',
|
||
|
||
/* === 历史记录下拉 === */
|
||
'.magnet-keyword-wrap{position:relative;}',
|
||
'.magnet-history-dropdown{',
|
||
' position:absolute;left:0;right:0;top:calc(100% + 6px);background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);box-shadow:var(--m-shadow-lg);z-index:2147483647;overflow:hidden;',
|
||
' max-height:260px;display:flex;flex-direction:column;',
|
||
'}',
|
||
'.magnet-history-list{overflow-y:auto;max-height:214px;}',
|
||
'.magnet-history-item{',
|
||
' display:flex;align-items:center;gap:8px;padding:10px 12px;font-size:12px;color:var(--m-text-primary);cursor:pointer;transition:background 0.2s ease;',
|
||
'}',
|
||
'.magnet-history-item:hover{',
|
||
' background:var(--m-bg-secondary);',
|
||
'}',
|
||
'.magnet-history-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
|
||
'.magnet-history-delete{flex:0 0 auto;border:none;background:transparent;color:var(--m-text-muted);cursor:pointer;font-size:13px;line-height:1;padding:0 0 0 4px;font-weight:500;}',
|
||
'.magnet-history-delete:hover{color:var(--m-error);}',
|
||
'.magnet-history-clear{',
|
||
' padding:10px 12px;border-top:1px solid var(--m-border);font-size:11px;color:var(--m-error);cursor:pointer;text-align:center;position:sticky;bottom:0;background:var(--m-bg-card);',
|
||
'}',
|
||
|
||
/* === 云同步 === */
|
||
'.magnet-cloud-status{display:inline-flex;align-items:center;gap:6px;margin-left:8px;}',
|
||
'.magnet-cloud-dot{width:8px;height:8px;border-radius:50%;background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,.35);flex:0 0 8px;}',
|
||
'.magnet-cloud-dot.is-ok{background:#10b981;box-shadow:0 0 8px rgba(16,185,129,.35);}',
|
||
'.magnet-cloud-label{font-size:11px;color:var(--m-text-secondary);}',
|
||
'.magnet-cloud-auth-card{padding:12px 14px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-lg);display:flex;flex-direction:column;gap:10px;}',
|
||
'.magnet-cloud-auth-title{font-size:12px;font-weight:700;color:var(--m-text-primary);}',
|
||
'.magnet-cloud-auth-meta{font-size:11px;color:var(--m-text-secondary);line-height:1.6;}',
|
||
'.magnet-cloud-auth-row{display:flex;gap:8px;flex-wrap:wrap;}',
|
||
'.magnet-cloud-auth-row input[type="password"],.magnet-cloud-auth-row input[type="text"]{flex:1;min-width:0;}',
|
||
'.magnet-cloud-secondary-btn{padding:10px 14px;background:rgba(26,31,46,0.92);color:var(--m-text-secondary);border:1px solid var(--m-border);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;}',
|
||
'.magnet-cloud-secondary-btn:hover{border-color:var(--m-border-accent);color:var(--m-text-primary);}',
|
||
'.magnet-cloud-danger-btn{padding:10px 14px;background:rgba(239,68,68,0.14);color:var(--m-error);border:1px solid rgba(239,68,68,0.25);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;}',
|
||
'.magnet-cloud-danger-btn:hover{background:rgba(239,68,68,0.22);}',
|
||
'#magnet-settings input[type="password"]{padding:10px 14px;background:var(--m-bg-primary);border:1px solid var(--m-border);border-radius:var(--m-radius-md);color:var(--m-text-primary);font-family:var(--m-font-body);font-size:13px;transition:all 0.2s ease;}'
|
||
].join('');
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
function setPanelView(viewName) {
|
||
var resultsView = document.getElementById('magnet-results-view');
|
||
var cacheView = document.getElementById('magnet-cache-view');
|
||
var favoritesView = document.getElementById('magnet-favorites-view');
|
||
var cloudView = document.getElementById('magnet-cloud-view');
|
||
var resultsBtn = document.getElementById('magnet-view-results');
|
||
var cacheBtn = document.getElementById('magnet-view-cache');
|
||
var favoritesBtn = document.getElementById('magnet-view-favorites');
|
||
var cloudBtn = document.getElementById('magnet-view-cloud');
|
||
|
||
if (!resultsView || !cacheView || !favoritesView || !cloudView || !resultsBtn || !cacheBtn || !favoritesBtn || !cloudBtn) {
|
||
return;
|
||
}
|
||
|
||
resultsView.classList.toggle('is-active', viewName === 'results');
|
||
cacheView.classList.toggle('is-active', viewName === 'cache');
|
||
favoritesView.classList.toggle('is-active', viewName === 'favorites');
|
||
cloudView.classList.toggle('is-active', viewName === 'cloud');
|
||
resultsBtn.classList.toggle('is-active', viewName === 'results');
|
||
cacheBtn.classList.toggle('is-active', viewName === 'cache');
|
||
favoritesBtn.classList.toggle('is-active', viewName === 'favorites');
|
||
cloudBtn.classList.toggle('is-active', viewName === 'cloud');
|
||
|
||
if (viewName === 'favorites') {
|
||
renderFavoritesList();
|
||
} else if (viewName === 'cloud') {
|
||
renderCloudAuthSection();
|
||
}
|
||
}
|
||
|
||
function createFloatingPanel() {
|
||
var existing = document.getElementById('magnet-floating-panel');
|
||
if (existing) return existing;
|
||
|
||
ensurePanelStyles();
|
||
|
||
var ball = document.createElement('div');
|
||
ball.id = 'magnet-float-ball';
|
||
ball.innerHTML = '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>';
|
||
ball.oncontextmenu = function(e) {
|
||
e.preventDefault();
|
||
var menu = document.getElementById('magnet-debug-menu');
|
||
if (menu) menu.remove();
|
||
menu = document.createElement('div');
|
||
menu.id = 'magnet-debug-menu';
|
||
menu.style.cssText = 'position:fixed;bottom:90px;right:28px;background:var(--m-bg-card);border:1px solid var(--m-border-accent);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,0.5);z-index:999999;padding:8px 0;font-size:12px;font-family:var(--m-font-body);';
|
||
menu.innerHTML = '<label style="display:block;padding:10px 16px;cursor:pointer;color:var(--m-text-primary)"><input type="checkbox" id="debug-check" style="accent-color:var(--m-accent);margin-right:8px;"' + (DEBUG_MODE ? ' checked' : '') + '> 调试模式</label>';
|
||
document.body.appendChild(menu);
|
||
menu.querySelector('input').onchange = function() {
|
||
DEBUG_MODE = this.checked;
|
||
localStorage.setItem('magnetDebugMode', DEBUG_MODE);
|
||
};
|
||
setTimeout(function() {
|
||
document.addEventListener('click', function hideMenu() {
|
||
menu.remove();
|
||
document.removeEventListener('click', hideMenu);
|
||
});
|
||
}, 100);
|
||
};
|
||
document.body.appendChild(ball);
|
||
|
||
var panel = document.createElement('div');
|
||
panel.id = 'magnet-floating-panel';
|
||
panel.innerHTML = '<div class="magnet-panel-header"><div class="magnet-panel-brand"><div class="magnet-panel-title">MAGNET LINKS</div><div class="magnet-panel-subtitle">智能抓取 · 缓存加速 · 一键复制</div></div><div class="magnet-panel-head-actions"><button id="magnet-view-results" class="magnet-panel-switch is-active">结果</button><button id="magnet-view-favorites" class="magnet-panel-switch">收藏</button><button id="magnet-view-cache" class="magnet-panel-switch">缓存</button><button id="magnet-view-cloud" class="magnet-panel-switch">云同步</button><button class="magnet-panel-close" title="关闭">×</button></div></div><div class="magnet-settings" id="magnet-settings"></div><div class="magnet-panel-content"><div id="magnet-results-view" class="magnet-view is-active"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">搜索结果</div><div class="magnet-view-meta">关键词命中的磁力链接</div></div><div class="magnet-view-meta">共 <span id="magnet-count-num">0</span> 条</div></div><div class="magnet-list" id="magnet-list"></div></div><div id="magnet-favorites-view" class="magnet-view"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">我的收藏</div><div class="magnet-view-meta">持久保存的磁力链接</div></div><div class="magnet-view-toolbar-actions"><button id="magnet-clear-favorites" class="magnet-panel-switch">清空收藏</button></div></div><div id="magnet-favorites-list"></div></div><div id="magnet-cache-view" class="magnet-view"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">缓存总览</div><div class="magnet-view-meta">数据统计与快照管理</div></div><div class="magnet-view-toolbar-actions"><button id="magnet-refresh-cache" class="magnet-panel-switch is-active">刷新</button><button id="magnet-clear-cache-inline" class="magnet-panel-switch">清空</button></div></div><div id="magnet-cache-panel"></div></div><div id="magnet-cloud-view" class="magnet-view"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">云同步账号</div><div class="magnet-view-meta">注册 / 登录 / 私有保险柜</div></div><div class="magnet-view-meta" id="magnet-cloud-view-meta">状态加载中</div></div><div id="magnet-cloud-page"></div></div></div><div class="magnet-panel-footer"><div id="magnet-status">设置参数后开始抓取</div><div class="magnet-progress-container"><div id="magnet-progress-bar" class="magnet-progress-bar"></div></div><div class="magnet-progress-text"><span id="magnet-progress-label">等待开始</span><span id="magnet-progress-percent">0%</span></div><button id="magnet-copy-all">一键复制全部</button></div>';
|
||
document.body.appendChild(panel);
|
||
|
||
var subtitle = panel.querySelector('.magnet-panel-subtitle');
|
||
if (subtitle) {
|
||
subtitle.innerHTML = '智能抓取 · 缓存加速 · 一键复制 <span class="magnet-cloud-status"><span id="magnet-cloud-sync-dot" class="magnet-cloud-dot"></span><span id="magnet-cloud-sync-text" class="magnet-cloud-label">云同步未登录</span></span>';
|
||
}
|
||
|
||
setPanelView('results');
|
||
|
||
ball.onclick = function() {
|
||
panel.style.display = 'flex';
|
||
ball.style.display = 'none';
|
||
};
|
||
|
||
var resultsSwitch = panel.querySelector('#magnet-view-results');
|
||
if (resultsSwitch) {
|
||
resultsSwitch.onclick = function() {
|
||
setPanelView('results');
|
||
};
|
||
}
|
||
|
||
var cacheSwitch = panel.querySelector('#magnet-view-cache');
|
||
if (cacheSwitch) {
|
||
cacheSwitch.onclick = function() {
|
||
setPanelView('cache');
|
||
refreshCacheOverview({ showStatus: true });
|
||
};
|
||
}
|
||
|
||
var favoritesSwitch = panel.querySelector('#magnet-view-favorites');
|
||
if (favoritesSwitch) {
|
||
favoritesSwitch.onclick = function() {
|
||
setPanelView('favorites');
|
||
};
|
||
}
|
||
|
||
var cloudSwitch = panel.querySelector('#magnet-view-cloud');
|
||
if (cloudSwitch) {
|
||
cloudSwitch.onclick = function() {
|
||
setPanelView('cloud');
|
||
};
|
||
}
|
||
|
||
var refreshCacheBtn = panel.querySelector('#magnet-refresh-cache');
|
||
if (refreshCacheBtn) {
|
||
refreshCacheBtn.onclick = function() {
|
||
refreshCacheOverview({ showStatus: true });
|
||
};
|
||
}
|
||
|
||
var clearCacheInlineBtn = panel.querySelector('#magnet-clear-cache-inline');
|
||
if (clearCacheInlineBtn) {
|
||
clearCacheInlineBtn.onclick = clearAllCacheWithConfirm;
|
||
}
|
||
|
||
var clearFavoritesBtn = panel.querySelector('#magnet-clear-favorites');
|
||
if (clearFavoritesBtn) {
|
||
clearFavoritesBtn.onclick = function() {
|
||
if (!confirm('确认清空全部收藏吗?')) {
|
||
return;
|
||
}
|
||
saveFavorites([]);
|
||
renderFavoritesList();
|
||
};
|
||
}
|
||
|
||
var closeBtn = panel.querySelector('.magnet-panel-close');
|
||
if (closeBtn) closeBtn.onclick = function() {
|
||
panel.style.display = 'none';
|
||
ball.style.display = 'flex';
|
||
};
|
||
|
||
var copyAllBtn = panel.querySelector('#magnet-copy-all');
|
||
if (copyAllBtn) {
|
||
copyAllBtn.onclick = function() {
|
||
var links = allMagnetLinks.length > 0
|
||
? allMagnetLinks.slice()
|
||
: Array.from(document.querySelectorAll('.magnet-item .magnet-copy-btn'))
|
||
.map(function(btn) { return btn.getAttribute('data-magnet'); })
|
||
.filter(function(link) { return !!link; });
|
||
if (links.length === 0) {
|
||
alert('暂无可复制的磁力链接');
|
||
return;
|
||
}
|
||
var allLinks = links.join('\n');
|
||
navigator.clipboard.writeText(allLinks)
|
||
.then(function() {
|
||
alert('已复制 ' + links.length + ' 个磁力链接!');
|
||
})
|
||
.catch(function(err) {
|
||
var errorMsg = err && err.message ? err.message : '复制失败';
|
||
alert('复制失败:' + errorMsg);
|
||
});
|
||
};
|
||
}
|
||
|
||
return panel;
|
||
}
|
||
|
||
function isThreadPage() {
|
||
return /\/thread-\d+-/i.test(window.location.href) || /[?&]tid=\d+/i.test(window.location.href);
|
||
}
|
||
|
||
function isListPage() {
|
||
if (isThreadPage()) {
|
||
return false;
|
||
}
|
||
|
||
if (document.querySelector('#threadlisttableid')) {
|
||
return true;
|
||
}
|
||
|
||
if (document.querySelector('tbody[id^="normalthread_"]') || document.querySelector('tbody[id^="stickthread_"]')) {
|
||
return true;
|
||
}
|
||
|
||
if (/\/forum-\d+(?:-\d+)?\.html/i.test(window.location.href) || /[?&]mod=forumdisplay/i.test(window.location.href) || /[?&]fid=\d+/i.test(window.location.href)) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function getCurrentPage() {
|
||
var match = window.location.href.match(/[?&]page=(\d+)/);
|
||
if (match) return parseInt(match[1]);
|
||
match = window.location.href.match(/forum-\d+-(\d+)\.html/);
|
||
if (match) return parseInt(match[1]);
|
||
return 1;
|
||
}
|
||
|
||
function getForumOrigin(rawUrl) {
|
||
var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href;
|
||
var match = sourceUrl.match(/^(https?:\/\/[^\/]+)\/?/i);
|
||
if (match) return match[1];
|
||
return window.location.origin || 'https://www.sehuatang.net';
|
||
}
|
||
|
||
function getForumIdFromUrl(rawUrl) {
|
||
var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href;
|
||
var match = sourceUrl.match(/[?&]fid=(\d+)/i);
|
||
if (match) return match[1];
|
||
|
||
match = sourceUrl.match(/forum-(\d+)(?:-|\.html)/i);
|
||
if (match) return match[1];
|
||
|
||
return '2';
|
||
}
|
||
|
||
function getForumKey() {
|
||
return getForumOrigin(window.location.href) + '|fid:' + getForumIdFromUrl(window.location.href);
|
||
}
|
||
|
||
function getBaseUrl() {
|
||
return getForumOrigin(window.location.href) + '/forum-' + getForumIdFromUrl(window.location.href) + '-';
|
||
}
|
||
|
||
function getCurrentPage() {
|
||
var currentUrl = window.location.href;
|
||
var match = currentUrl.match(/forum-\d+-(\d+)\.html/i) || currentUrl.match(/[?&]page=(\d+)/i);
|
||
return match ? Math.max(1, Number(match[1]) || 1) : 1;
|
||
}
|
||
|
||
function getLastPage() {
|
||
var pageLinks = document.querySelectorAll('a[href*="forum-"][href$=".html"], .pg a, .pgt a');
|
||
var maxPage = getCurrentPage();
|
||
|
||
pageLinks.forEach(function(link) {
|
||
var href = link && link.href ? link.href : '';
|
||
var text = link && link.textContent ? link.textContent.trim() : '';
|
||
var match = href.match(/forum-\d+-(\d+)\.html/i) || href.match(/[?&]page=(\d+)/i);
|
||
var pageNum = match ? Number(match[1]) : Number(text);
|
||
if (Number.isFinite(pageNum) && pageNum > maxPage) {
|
||
maxPage = pageNum;
|
||
}
|
||
});
|
||
|
||
return Math.max(1, maxPage);
|
||
}
|
||
|
||
function normalizeThreadUrl(url) {
|
||
if (typeof url !== 'string' || !url) {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
var parsed = new URL(url, window.location.origin);
|
||
parsed.hash = '';
|
||
return parsed.href;
|
||
} catch (e) {
|
||
return url;
|
||
}
|
||
}
|
||
|
||
function getThreadKeyFromUrl(url) {
|
||
var normalizedUrl = normalizeThreadUrl(url);
|
||
if (!normalizedUrl) {
|
||
return '';
|
||
}
|
||
|
||
var match = normalizedUrl.match(/thread-(\d+)-/i) || normalizedUrl.match(/[?&]tid=(\d+)/i);
|
||
return match ? match[1] : normalizedUrl;
|
||
}
|
||
|
||
function normalizeCachedThreads(threads) {
|
||
var result = [];
|
||
var seen = Object.create(null);
|
||
(Array.isArray(threads) ? threads : []).forEach(function(thread) {
|
||
if (!thread || typeof thread !== 'object') return;
|
||
|
||
var normalizedUrl = normalizeThreadUrl(thread.url);
|
||
var normalizedTitle = typeof thread.title === 'string'
|
||
? thread.title.replace(/\s+/g, ' ').trim()
|
||
: '';
|
||
var threadKey = typeof thread.threadKey === 'string' && thread.threadKey
|
||
? thread.threadKey
|
||
: getThreadKeyFromUrl(normalizedUrl);
|
||
|
||
if (!normalizedUrl || !threadKey || seen[threadKey]) return;
|
||
|
||
seen[threadKey] = true;
|
||
result.push({
|
||
url: normalizedUrl,
|
||
title: normalizedTitle,
|
||
threadKey: threadKey
|
||
});
|
||
});
|
||
return result;
|
||
}
|
||
|
||
function normalizeMagnetList(magnets) {
|
||
var seen = Object.create(null);
|
||
var result = [];
|
||
|
||
(Array.isArray(magnets) ? magnets : []).forEach(function(magnet) {
|
||
if (typeof magnet !== 'string' || !magnet) return;
|
||
if (seen[magnet]) return;
|
||
seen[magnet] = true;
|
||
result.push(magnet);
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
function extractThreadsFromHtml(html) {
|
||
var parser = new DOMParser();
|
||
var doc = parser.parseFromString(html, 'text/html');
|
||
var threads = [];
|
||
var tbodies = doc.querySelectorAll('#threadlisttableid tbody[id^="normalthread_"]');
|
||
tbodies.forEach(function(tbody) {
|
||
var link = tbody.querySelector('th a[href*="thread-"]');
|
||
var title = tbody.querySelector('th a.xst') || tbody.querySelector('th .xst');
|
||
var titleText = title ? title.textContent : '';
|
||
var normalizedUrl = link && link.href ? normalizeThreadUrl(link.href) : '';
|
||
if (normalizedUrl) {
|
||
threads.push({
|
||
url: normalizedUrl,
|
||
title: titleText,
|
||
threadKey: getThreadKeyFromUrl(normalizedUrl)
|
||
});
|
||
}
|
||
});
|
||
return normalizeCachedThreads(threads);
|
||
}
|
||
|
||
function updateStatus(message, type) {
|
||
var status = document.getElementById('magnet-status');
|
||
if (status) {
|
||
status.textContent = message;
|
||
status.setAttribute('data-type', type || '');
|
||
// 默认样式
|
||
status.style.background = 'rgba(26,31,46,0.92)';
|
||
status.style.color = '#8892a4';
|
||
status.style.border = '1px solid rgba(255,255,255,0.06)';
|
||
if (type === 'loading') {
|
||
status.style.background = 'rgba(245,158,11,0.1)';
|
||
status.style.color = '#f59e0b';
|
||
status.style.borderColor = 'rgba(245,158,11,0.3)';
|
||
} else if (type === 'error') {
|
||
status.style.background = 'rgba(239,68,68,0.1)';
|
||
status.style.color = '#ef4444';
|
||
status.style.borderColor = 'rgba(239,68,68,0.3)';
|
||
} else if (type === 'done') {
|
||
status.style.background = 'rgba(16,185,129,0.1)';
|
||
status.style.color = '#10b981';
|
||
status.style.borderColor = 'rgba(16,185,129,0.3)';
|
||
}
|
||
|
||
if (type && type !== 'loading') {
|
||
scheduleStatePersist();
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateCount(count) {
|
||
var countEl = document.getElementById('magnet-count-num');
|
||
if (countEl) countEl.textContent = count;
|
||
}
|
||
|
||
function updateProgress(current, total, label) {
|
||
var progressBar = document.getElementById('magnet-progress-bar');
|
||
var progressLabel = document.getElementById('magnet-progress-label');
|
||
var progressPercent = document.getElementById('magnet-progress-percent');
|
||
var percent = total > 0 ? Math.max(0, Math.min(100, Math.round(current / total * 100))) : 0;
|
||
if (progressBar) {
|
||
progressBar.style.width = percent + '%';
|
||
}
|
||
if (progressLabel) {
|
||
progressLabel.textContent = label || ('进度 ' + current + '/' + total);
|
||
}
|
||
if (progressPercent) {
|
||
progressPercent.textContent = percent + '%';
|
||
}
|
||
}
|
||
|
||
function resetProgress() {
|
||
updateProgress(0, 0, '等待开始');
|
||
}
|
||
|
||
function buildRangeProgressLabel(rangeStart, rangeEnd, threadIndex, threadTotal) {
|
||
return '页' + rangeStart + '-' + rangeEnd + '/帖子' + threadIndex + '/' + threadTotal;
|
||
}
|
||
|
||
function updateRangeThreadProgress(context, rangeStart, rangeEnd, threadIndex, threadTotal) {
|
||
var totalPages = Math.max(1, Number(context.totalPageCount) || (Number(context.normalizedEnd || 0) - Number(context.startPage || 0) + 1));
|
||
var safeRangeStart = Math.max(Number(context.startPage) || 1, Number(rangeStart) || Number(context.startPage) || 1);
|
||
var safeRangeEnd = Math.max(safeRangeStart, Number(rangeEnd) || safeRangeStart);
|
||
var completedBefore = Math.max(0, safeRangeStart - (Number(context.startPage) || 1));
|
||
var coveredPages = Math.max(1, safeRangeEnd - safeRangeStart + 1);
|
||
var fractional = threadTotal > 0 ? Math.max(0, Math.min(1, Number(threadIndex) / Number(threadTotal))) : 0;
|
||
var completedPages = completedBefore + coveredPages * fractional;
|
||
updateProgress(completedPages, totalPages, buildRangeProgressLabel(safeRangeStart, safeRangeEnd, threadIndex, threadTotal));
|
||
}
|
||
|
||
var FAVORITES_KEY = 'magnet-favorites';
|
||
var SEARCH_HISTORY_KEY = 'magnet-search-history';
|
||
var MAX_SEARCH_HISTORY = 20;
|
||
var PRIVATE_STORAGE_KEY = 'magnet-private-state-v1';
|
||
var privateStateLoaded = false;
|
||
var favoritesCache = [];
|
||
var searchHistoryCache = [];
|
||
var sessionBackupState = null;
|
||
|
||
function cloneSimpleArray(value) {
|
||
return JSON.parse(JSON.stringify(Array.isArray(value) ? value : []));
|
||
}
|
||
|
||
function loadPrivateStateFromExtensionStorage() {
|
||
return new Promise(function(resolve) {
|
||
var migratedFavorites = [];
|
||
var migratedHistory = [];
|
||
var state = null;
|
||
if (privateStateLoaded) {
|
||
resolve({ favorites: cloneSimpleArray(favoritesCache), searchHistory: cloneSimpleArray(searchHistoryCache) });
|
||
return;
|
||
}
|
||
chrome.storage.local.get(PRIVATE_STORAGE_KEY, function(result) {
|
||
state = result && result[PRIVATE_STORAGE_KEY] && typeof result[PRIVATE_STORAGE_KEY] === 'object' ? result[PRIVATE_STORAGE_KEY] : {};
|
||
favoritesCache = Array.isArray(state.favorites) ? state.favorites : [];
|
||
searchHistoryCache = Array.isArray(state.searchHistory) ? state.searchHistory : [];
|
||
if (favoritesCache.length === 0) {
|
||
try {
|
||
migratedFavorites = JSON.parse(localStorage.getItem(FAVORITES_KEY) || '[]');
|
||
favoritesCache = Array.isArray(migratedFavorites) ? migratedFavorites : [];
|
||
localStorage.removeItem(FAVORITES_KEY);
|
||
} catch (e) {
|
||
favoritesCache = [];
|
||
}
|
||
}
|
||
if (searchHistoryCache.length === 0) {
|
||
try {
|
||
migratedHistory = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '[]');
|
||
searchHistoryCache = Array.isArray(migratedHistory) ? migratedHistory : [];
|
||
localStorage.removeItem(SEARCH_HISTORY_KEY);
|
||
} catch (e) {
|
||
searchHistoryCache = [];
|
||
}
|
||
}
|
||
privateStateLoaded = true;
|
||
persistPrivateState();
|
||
resolve({ favorites: cloneSimpleArray(favoritesCache), searchHistory: cloneSimpleArray(searchHistoryCache) });
|
||
});
|
||
});
|
||
}
|
||
|
||
function persistPrivateState() {
|
||
if (!chrome.storage || !chrome.storage.local) {
|
||
return;
|
||
}
|
||
chrome.storage.local.set((function() {
|
||
var data = {};
|
||
data[PRIVATE_STORAGE_KEY] = {
|
||
favorites: favoritesCache,
|
||
searchHistory: searchHistoryCache
|
||
};
|
||
return data;
|
||
})());
|
||
}
|
||
|
||
function loadFavorites() {
|
||
return cloneSimpleArray(favoritesCache);
|
||
}
|
||
|
||
function saveFavorites(favorites, options) {
|
||
options = options || {};
|
||
favoritesCache = Array.isArray(favorites) ? favorites : [];
|
||
persistPrivateState();
|
||
if (!options.skipCloudSync) {
|
||
scheduleCloudVaultSync();
|
||
}
|
||
}
|
||
|
||
function isFavorited(link) {
|
||
return loadFavorites().some(function(item) {
|
||
return item && item.link === link;
|
||
});
|
||
}
|
||
|
||
function toggleFavorite(title, link, favoriteBtn) {
|
||
var favorites = loadFavorites();
|
||
var foundIndex = favorites.findIndex(function(item) {
|
||
return item.link === link;
|
||
});
|
||
|
||
if (foundIndex >= 0) {
|
||
favorites.splice(foundIndex, 1);
|
||
if (favoriteBtn) {
|
||
favoriteBtn.classList.remove('is-favorite');
|
||
favoriteBtn.textContent = '❤';
|
||
favoriteBtn.title = '收藏';
|
||
}
|
||
} else {
|
||
favorites.unshift({
|
||
title: title || '未命名帖子',
|
||
link: link,
|
||
addedAt: Date.now()
|
||
});
|
||
if (favoriteBtn) {
|
||
favoriteBtn.classList.add('is-favorite');
|
||
favoriteBtn.textContent = '❤';
|
||
favoriteBtn.title = '取消收藏';
|
||
}
|
||
}
|
||
|
||
saveFavorites(favorites);
|
||
|
||
var favoritesView = document.getElementById('magnet-favorites-view');
|
||
if (favoritesView && favoritesView.classList.contains('is-active')) {
|
||
renderFavoritesList();
|
||
}
|
||
}
|
||
|
||
function renderFavoritesList() {
|
||
var favoritesList = document.getElementById('magnet-favorites-list');
|
||
if (!favoritesList) return;
|
||
|
||
var favorites = loadFavorites();
|
||
favoritesList.innerHTML = '';
|
||
|
||
if (favorites.length === 0) {
|
||
favoritesList.innerHTML = '<div class="magnet-empty-state"><div class="magnet-empty-state-icon">☆</div><div class="magnet-empty-state-text">暂无收藏<br>点击结果项右侧的♡即可加入收藏</div></div>';
|
||
return;
|
||
}
|
||
|
||
favorites.forEach(function(favorite) {
|
||
var item = document.createElement('div');
|
||
item.className = 'magnet-favorite-item';
|
||
|
||
var title = document.createElement('div');
|
||
title.className = 'magnet-favorite-title';
|
||
title.textContent = favorite.title || '未命名帖子';
|
||
title.title = favorite.title || '未命名帖子';
|
||
|
||
var actions = document.createElement('div');
|
||
actions.className = 'magnet-favorite-actions';
|
||
|
||
var copyBtn = document.createElement('button');
|
||
copyBtn.className = 'magnet-favorite-copy';
|
||
copyBtn.textContent = '复制';
|
||
copyBtn.onclick = function() {
|
||
navigator.clipboard.writeText(favorite.link).then(function() {
|
||
copyBtn.textContent = '已复制';
|
||
setTimeout(function() {
|
||
copyBtn.textContent = '复制';
|
||
}, 1000);
|
||
});
|
||
};
|
||
|
||
var removeBtn = document.createElement('button');
|
||
removeBtn.className = 'magnet-favorite-remove';
|
||
removeBtn.textContent = '删除';
|
||
removeBtn.onclick = function() {
|
||
saveFavorites(loadFavorites().filter(function(itemData) {
|
||
return itemData.link !== favorite.link;
|
||
}));
|
||
renderFavoritesList();
|
||
};
|
||
|
||
actions.appendChild(copyBtn);
|
||
actions.appendChild(removeBtn);
|
||
item.appendChild(title);
|
||
item.appendChild(actions);
|
||
favoritesList.appendChild(item);
|
||
});
|
||
}
|
||
|
||
function loadSearchHistory() {
|
||
return cloneSimpleArray(searchHistoryCache);
|
||
}
|
||
|
||
function removeSearchHistoryItem(keyword) {
|
||
var value = String(keyword || '').trim();
|
||
if (!value) {
|
||
return;
|
||
}
|
||
setSearchHistoryList(loadSearchHistory().filter(function(item) {
|
||
return item !== value;
|
||
}), { skipCloudSync: false });
|
||
}
|
||
|
||
function saveSearchHistory(keyword) {
|
||
var value = String(keyword || '').trim();
|
||
if (!value) return;
|
||
var history = loadSearchHistory().filter(function(item) {
|
||
return item !== value;
|
||
});
|
||
history.unshift(value);
|
||
setSearchHistoryList(history.slice(0, MAX_SEARCH_HISTORY));
|
||
}
|
||
|
||
function removeHistoryDropdown() {
|
||
var dropdown = document.getElementById('magnet-history-dropdown');
|
||
if (dropdown) {
|
||
dropdown.remove();
|
||
}
|
||
}
|
||
|
||
function showHistoryDropdown(input) {
|
||
if (!input) return;
|
||
removeHistoryDropdown();
|
||
|
||
var history = loadSearchHistory();
|
||
var list = null;
|
||
if (history.length === 0) return;
|
||
|
||
var wrap = input.parentNode;
|
||
if (!wrap) return;
|
||
wrap.classList.add('magnet-keyword-wrap');
|
||
|
||
var dropdown = document.createElement('div');
|
||
dropdown.id = 'magnet-history-dropdown';
|
||
dropdown.className = 'magnet-history-dropdown';
|
||
|
||
list = document.createElement('div');
|
||
list.className = 'magnet-history-list';
|
||
|
||
history.forEach(function(historyKeyword) {
|
||
var item = document.createElement('div');
|
||
item.className = 'magnet-history-item';
|
||
var text = document.createElement('div');
|
||
var removeBtn = document.createElement('button');
|
||
text.className = 'magnet-history-text';
|
||
text.textContent = historyKeyword;
|
||
removeBtn.className = 'magnet-history-delete';
|
||
removeBtn.type = 'button';
|
||
removeBtn.textContent = '×';
|
||
removeBtn.title = '删除这条历史记录';
|
||
item.onclick = function() {
|
||
input.value = historyKeyword;
|
||
removeHistoryDropdown();
|
||
};
|
||
removeBtn.onclick = function(event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
removeSearchHistoryItem(historyKeyword);
|
||
showHistoryDropdown(input);
|
||
};
|
||
item.appendChild(text);
|
||
item.appendChild(removeBtn);
|
||
list.appendChild(item);
|
||
});
|
||
dropdown.appendChild(list);
|
||
|
||
var clearItem = document.createElement('div');
|
||
clearItem.className = 'magnet-history-clear';
|
||
clearItem.textContent = '清空历史记录';
|
||
clearItem.onclick = function() {
|
||
setSearchHistoryList([], { skipCloudSync: false });
|
||
removeHistoryDropdown();
|
||
};
|
||
dropdown.appendChild(clearItem);
|
||
wrap.appendChild(dropdown);
|
||
|
||
setTimeout(function() {
|
||
document.addEventListener('click', function hideHistory(event) {
|
||
if (!dropdown.contains(event.target) && event.target !== input) {
|
||
removeHistoryDropdown();
|
||
document.removeEventListener('click', hideHistory);
|
||
}
|
||
});
|
||
}, 0);
|
||
}
|
||
|
||
function playDoneTone() {
|
||
try {
|
||
var AudioContextCtor = window.AudioContext || window.webkitAudioContext;
|
||
if (!AudioContextCtor) return;
|
||
var context = new AudioContextCtor();
|
||
var oscillator = context.createOscillator();
|
||
var gainNode = context.createGain();
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(context.destination);
|
||
oscillator.type = 'sine';
|
||
oscillator.frequency.value = 880;
|
||
gainNode.gain.setValueAtTime(0.0001, context.currentTime);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.18, context.currentTime + 0.01);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.35);
|
||
oscillator.start(context.currentTime);
|
||
oscillator.stop(context.currentTime + 0.35);
|
||
} catch (e) {
|
||
log('提示音播放失败: ' + e);
|
||
}
|
||
}
|
||
|
||
function showDoneNotification(title, body) {
|
||
if (!('Notification' in window)) return;
|
||
if (Notification.permission === 'granted') {
|
||
new Notification(title, { body: body });
|
||
return;
|
||
}
|
||
if (Notification.permission !== 'denied') {
|
||
Notification.requestPermission().then(function(permission) {
|
||
if (permission === 'granted') {
|
||
new Notification(title, { body: body });
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
var cloudSyncState = {
|
||
authenticated: false,
|
||
healthy: false,
|
||
color: 'red',
|
||
text: '云同步未登录',
|
||
email: '',
|
||
lastError: ''
|
||
};
|
||
var cloudVaultHydrated = false;
|
||
var cloudVaultSyncTimer = null;
|
||
|
||
function updateCloudSyncIndicator(status) {
|
||
var dot = document.getElementById('magnet-cloud-sync-dot');
|
||
var label = document.getElementById('magnet-cloud-sync-text');
|
||
cloudSyncState = Object.assign({}, cloudSyncState, status || {});
|
||
if (dot) {
|
||
dot.classList.toggle('is-ok', cloudSyncState.color === 'green');
|
||
}
|
||
if (label) {
|
||
label.textContent = cloudSyncState.text || (cloudSyncState.color === 'green' ? '云同步正常' : '云同步异常');
|
||
label.title = cloudSyncState.email ? (cloudSyncState.text + ' · ' + cloudSyncState.email) : (cloudSyncState.lastError || cloudSyncState.text || '云同步');
|
||
}
|
||
}
|
||
|
||
async function getCloudSyncStatus() {
|
||
try {
|
||
var response = await sendRuntimeMessage({ action: 'cloudGetSyncStatus' }, 12000);
|
||
if (response && response.ok) {
|
||
updateCloudSyncIndicator(response);
|
||
return response;
|
||
}
|
||
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: response && response.error ? response.error : '状态获取失败' });
|
||
return null;
|
||
} catch (e) {
|
||
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function getUiSettingsSnapshot() {
|
||
return {
|
||
speedMode: speedConfig[speedMode] ? speedMode : 'fast'
|
||
};
|
||
}
|
||
|
||
function applyUiSettingsSnapshot(snapshot) {
|
||
if (!snapshot || typeof snapshot !== 'object') {
|
||
return;
|
||
}
|
||
if (snapshot.speedMode && speedConfig[snapshot.speedMode]) {
|
||
speedMode = snapshot.speedMode;
|
||
var speedSelect = document.getElementById('speed-select');
|
||
if (speedSelect) {
|
||
speedSelect.value = speedMode;
|
||
}
|
||
}
|
||
}
|
||
|
||
function setSearchHistoryList(historyList, options) {
|
||
var normalized = [];
|
||
var seen = Object.create(null);
|
||
options = options || {};
|
||
(Array.isArray(historyList) ? historyList : []).forEach(function(item) {
|
||
var value = String(item || '').trim();
|
||
if (!value || seen[value]) {
|
||
return;
|
||
}
|
||
seen[value] = true;
|
||
normalized.push(value);
|
||
});
|
||
searchHistoryCache = normalized.slice(0, MAX_SEARCH_HISTORY);
|
||
persistPrivateState();
|
||
if (!options.skipCloudSync) {
|
||
scheduleCloudVaultSync();
|
||
}
|
||
}
|
||
|
||
function openCloudSyncCenter() {
|
||
sendRuntimeMessage({ action: 'openCloudSyncPage' }, 8000).catch(function(e) {
|
||
updateStatus('打开云同步中心失败:' + (e && e.message ? e.message : String(e)), 'error');
|
||
});
|
||
}
|
||
|
||
function triggerMagnetDownload(link, downloadBtn) {
|
||
var anchor = null;
|
||
var cleanup = null;
|
||
var fallbackCopy = function(message, type) {
|
||
navigator.clipboard.writeText(link).then(function() {
|
||
updateStatus(message || '未检测到本地下载器,已复制磁力链接。请在扩展页下载 qBittorrent(推荐节点1/2)', type || 'done');
|
||
}).catch(function() {
|
||
updateStatus('未能调用本地下载器,请手动复制磁力链接', 'error');
|
||
});
|
||
};
|
||
|
||
try {
|
||
updateStatus('正在调用本地下载器;如未安装,可到扩展页下载 qBittorrent(推荐节点1/2)', 'loading');
|
||
anchor = document.createElement('a');
|
||
anchor.href = link;
|
||
anchor.style.display = 'none';
|
||
anchor.rel = 'noreferrer noopener';
|
||
document.body.appendChild(anchor);
|
||
anchor.click();
|
||
if (downloadBtn) {
|
||
downloadBtn.textContent = '已发送';
|
||
cleanup = function() {
|
||
downloadBtn.textContent = '下载';
|
||
};
|
||
setTimeout(cleanup, 1200);
|
||
}
|
||
setTimeout(function() {
|
||
updateStatus('已发送到本地下载器,如未弹出请先安装并关联 qBittorrent', 'done');
|
||
}, 300);
|
||
} catch (e) {
|
||
fallbackCopy('未检测到本地下载器,已复制磁力链接。请在扩展页下载 qBittorrent(推荐节点1/2)', 'done');
|
||
} finally {
|
||
if (anchor && anchor.parentNode) {
|
||
anchor.parentNode.removeChild(anchor);
|
||
}
|
||
}
|
||
}
|
||
|
||
function mergeFavoritesList(localItems, remoteItems) {
|
||
var merged = [];
|
||
var seen = Object.create(null);
|
||
(Array.isArray(remoteItems) ? remoteItems : []).concat(Array.isArray(localItems) ? localItems : []).forEach(function(item) {
|
||
var link = item && typeof item.link === 'string' ? item.link : '';
|
||
if (!link || seen[link]) {
|
||
return;
|
||
}
|
||
seen[link] = true;
|
||
merged.push({
|
||
title: typeof item.title === 'string' ? item.title : '未命名帖子',
|
||
link: link,
|
||
addedAt: Number(item.addedAt) || Date.now()
|
||
});
|
||
});
|
||
return merged;
|
||
}
|
||
|
||
function mergeHistoryList(localItems, remoteItems) {
|
||
var merged = [];
|
||
var seen = Object.create(null);
|
||
(Array.isArray(remoteItems) ? remoteItems : []).concat(Array.isArray(localItems) ? localItems : []).forEach(function(item) {
|
||
var value = String(item || '').trim();
|
||
if (!value || seen[value]) {
|
||
return;
|
||
}
|
||
seen[value] = true;
|
||
merged.push(value);
|
||
});
|
||
return merged.slice(0, MAX_SEARCH_HISTORY);
|
||
}
|
||
|
||
async function pushCloudVaultItems(items) {
|
||
try {
|
||
var response = await sendRuntimeMessage({ action: 'cloudPushVaultItems', items: items }, 20000);
|
||
if (response && response.status) {
|
||
updateCloudSyncIndicator(response.status);
|
||
}
|
||
return response;
|
||
} catch (e) {
|
||
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function pullCloudVaultItems(itemTypes) {
|
||
try {
|
||
var response = await sendRuntimeMessage({ action: 'cloudPullVaultItems', itemTypes: itemTypes }, 20000);
|
||
if (response && response.ok) {
|
||
if (response.status) {
|
||
updateCloudSyncIndicator(response.status);
|
||
}
|
||
return response.items || [];
|
||
}
|
||
return [];
|
||
} catch (e) {
|
||
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function scheduleCloudVaultSync() {
|
||
if (cloudVaultSyncTimer) {
|
||
clearTimeout(cloudVaultSyncTimer);
|
||
}
|
||
cloudVaultSyncTimer = setTimeout(function() {
|
||
cloudVaultSyncTimer = null;
|
||
syncAllLocalVaultData();
|
||
}, 600);
|
||
}
|
||
|
||
async function syncAllLocalVaultData() {
|
||
if (!cloudSyncState.authenticated) {
|
||
return null;
|
||
}
|
||
return pushCloudVaultItems([
|
||
{ itemType: 'favorites', itemKey: 'default', data: loadFavorites() },
|
||
{ itemType: 'search_history', itemKey: 'default', data: loadSearchHistory() },
|
||
{ itemType: 'ui_settings', itemKey: 'default', data: getUiSettingsSnapshot() }
|
||
]);
|
||
}
|
||
|
||
function applyVaultItems(items, options) {
|
||
var remoteFavorites = [];
|
||
var remoteHistory = [];
|
||
var remoteSettings = null;
|
||
var remoteProgress = null;
|
||
options = options || {};
|
||
|
||
(Array.isArray(items) ? items : []).forEach(function(item) {
|
||
if (!item || typeof item !== 'object') {
|
||
return;
|
||
}
|
||
if (item.itemType === 'favorites' && Array.isArray(item.data)) {
|
||
remoteFavorites = item.data;
|
||
} else if (item.itemType === 'search_history' && Array.isArray(item.data)) {
|
||
remoteHistory = item.data;
|
||
} else if (item.itemType === 'ui_settings' && item.data && typeof item.data === 'object') {
|
||
remoteSettings = item.data;
|
||
} else if (item.itemType === 'progress_state' && item.data && typeof item.data === 'object') {
|
||
remoteProgress = item.data;
|
||
}
|
||
});
|
||
|
||
if (remoteFavorites.length > 0) {
|
||
saveFavorites(mergeFavoritesList(loadFavorites(), remoteFavorites), { skipCloudSync: true });
|
||
}
|
||
if (remoteHistory.length > 0) {
|
||
setSearchHistoryList(mergeHistoryList(loadSearchHistory(), remoteHistory), { skipCloudSync: true });
|
||
}
|
||
if (remoteSettings) {
|
||
applyUiSettingsSnapshot(remoteSettings);
|
||
}
|
||
if (options.restoreProgress && remoteProgress && allMagnetLinks.length === 0) {
|
||
applyStateSnapshot(remoteProgress);
|
||
}
|
||
cloudVaultHydrated = true;
|
||
}
|
||
|
||
async function refreshCloudStatusAndVault(options) {
|
||
var status = await getCloudSyncStatus();
|
||
if (!status) {
|
||
renderCloudAuthSection();
|
||
return null;
|
||
}
|
||
if (status.authenticated && !cloudVaultHydrated && !options?.skipVaultPull) {
|
||
applyVaultItems(await pullCloudVaultItems(['favorites', 'search_history', 'ui_settings', 'progress_state']), { restoreProgress: !!options?.restoreProgress });
|
||
}
|
||
renderCloudAuthSection();
|
||
return status;
|
||
}
|
||
|
||
async function logoutCloudAccount() {
|
||
try {
|
||
var response = await sendRuntimeMessage({ action: 'cloudLogout' }, 15000);
|
||
cloudVaultHydrated = false;
|
||
updateCloudSyncIndicator(response && response.status ? response.status : { color: 'red', text: '云同步未登录', authenticated: false, healthy: false, email: '' });
|
||
renderCloudAuthSection();
|
||
updateStatus('已退出云同步', 'done');
|
||
} catch (e) {
|
||
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
|
||
updateStatus('退出失败:' + (e && e.message ? e.message : String(e)), 'error');
|
||
}
|
||
}
|
||
|
||
function renderCloudAuthSection() {
|
||
var settingsArea = document.getElementById('magnet-cloud-page');
|
||
var existing = document.getElementById('magnet-cloud-auth-card');
|
||
var viewMeta = document.getElementById('magnet-cloud-view-meta');
|
||
var html = '';
|
||
var card = null;
|
||
var syncNowBtn = null;
|
||
var logoutBtn = null;
|
||
var openCenterBtn = null;
|
||
if (!settingsArea) {
|
||
return;
|
||
}
|
||
if (existing) {
|
||
existing.remove();
|
||
}
|
||
card = document.createElement('div');
|
||
card.id = 'magnet-cloud-auth-card';
|
||
card.className = 'magnet-cloud-auth-card';
|
||
if (viewMeta) {
|
||
viewMeta.textContent = cloudSyncState.email ? (cloudSyncState.text + ' · ' + cloudSyncState.email) : (cloudSyncState.text || '状态未知');
|
||
}
|
||
if (cloudSyncState.authenticated) {
|
||
html = '<div class="magnet-cloud-auth-title">云同步已开启</div><div class="magnet-cloud-auth-meta">账号:' + escapeHtml(cloudSyncState.email || '已登录') + '<br>' + escapeHtml(cloudSyncState.text || '云同步正常') + '</div><div class="magnet-cloud-auth-row"><button id="magnet-cloud-sync-now" class="magnet-cloud-secondary-btn" type="button">立即同步</button><button id="magnet-cloud-open-center" class="magnet-cloud-secondary-btn" type="button">打开云同步中心</button><button id="magnet-cloud-logout" class="magnet-cloud-danger-btn" type="button">退出登录</button></div>';
|
||
} else {
|
||
html = '<div class="magnet-cloud-auth-title">云同步账号</div><div class="magnet-cloud-auth-meta">为了安全,登录/注册已移到扩展独立页面中进行。网页本身不会再承载账号密码输入框。</div><div class="magnet-cloud-auth-row"><button id="magnet-cloud-open-center" type="button">打开云同步中心</button></div>';
|
||
}
|
||
card.innerHTML = html;
|
||
settingsArea.innerHTML = '';
|
||
settingsArea.appendChild(card);
|
||
if (cloudSyncState.authenticated) {
|
||
syncNowBtn = card.querySelector('#magnet-cloud-sync-now');
|
||
logoutBtn = card.querySelector('#magnet-cloud-logout');
|
||
openCenterBtn = card.querySelector('#magnet-cloud-open-center');
|
||
if (syncNowBtn) {
|
||
syncNowBtn.onclick = function() {
|
||
updateStatus('正在同步保险柜...', 'loading');
|
||
syncAllLocalVaultData().then(function() {
|
||
updateStatus('保险柜同步完成', 'done');
|
||
}).catch(function(e) {
|
||
updateStatus('保险柜同步失败:' + (e && e.message ? e.message : String(e)), 'error');
|
||
});
|
||
};
|
||
}
|
||
if (logoutBtn) {
|
||
logoutBtn.onclick = logoutCloudAccount;
|
||
}
|
||
if (openCenterBtn) {
|
||
openCenterBtn.onclick = openCloudSyncCenter;
|
||
}
|
||
} else {
|
||
openCenterBtn = card.querySelector('#magnet-cloud-open-center');
|
||
if (openCenterBtn) {
|
||
openCenterBtn.onclick = openCloudSyncCenter;
|
||
}
|
||
}
|
||
}
|
||
|
||
function clearMagnetList(skipPersist) {
|
||
var list = document.getElementById('magnet-list');
|
||
if (list) list.innerHTML = '';
|
||
allMagnetLinks = [];
|
||
allMagnetRecords = [];
|
||
magnetRecordMap = Object.create(null);
|
||
updateCount(0);
|
||
|
||
if (!skipPersist) {
|
||
scheduleStatePersist();
|
||
}
|
||
}
|
||
|
||
function addMagnetItem(title, link, options) {
|
||
var list = document.getElementById('magnet-list');
|
||
if (!list) return;
|
||
|
||
var safeTitle = typeof title === 'string' ? title : String(title || '');
|
||
var safeLink = typeof link === 'string' ? link : String(link || '');
|
||
if (!safeLink) return;
|
||
|
||
if (magnetRecordMap[safeLink]) {
|
||
return;
|
||
}
|
||
|
||
magnetRecordMap[safeLink] = safeTitle || '恢复记录';
|
||
allMagnetRecords.push({
|
||
title: magnetRecordMap[safeLink],
|
||
link: safeLink
|
||
});
|
||
allMagnetLinks.push(safeLink);
|
||
|
||
var item = document.createElement('div');
|
||
item.className = 'magnet-item';
|
||
|
||
var titleEl = document.createElement('span');
|
||
titleEl.className = 'magnet-title';
|
||
titleEl.title = safeTitle;
|
||
titleEl.textContent = safeTitle;
|
||
|
||
var actions = document.createElement('div');
|
||
actions.className = 'magnet-item-actions';
|
||
|
||
var favoriteBtn = document.createElement('button');
|
||
favoriteBtn.className = 'magnet-favorite-btn';
|
||
favoriteBtn.textContent = '❤';
|
||
favoriteBtn.title = '收藏';
|
||
if (isFavorited(safeLink)) {
|
||
favoriteBtn.classList.add('is-favorite');
|
||
favoriteBtn.textContent = '❤';
|
||
favoriteBtn.title = '取消收藏';
|
||
}
|
||
favoriteBtn.onclick = function() {
|
||
toggleFavorite(safeTitle, safeLink, favoriteBtn);
|
||
};
|
||
|
||
var copyBtn = document.createElement('button');
|
||
copyBtn.className = 'magnet-copy-btn';
|
||
copyBtn.setAttribute('data-magnet', safeLink);
|
||
copyBtn.textContent = '复制';
|
||
|
||
var downloadBtn = document.createElement('button');
|
||
downloadBtn.className = 'magnet-download-btn';
|
||
downloadBtn.setAttribute('data-magnet', safeLink);
|
||
downloadBtn.textContent = '下载';
|
||
|
||
titleEl.onclick = function() {
|
||
navigator.clipboard.writeText(safeLink)
|
||
.then(function() {
|
||
titleEl.textContent = '已复制: ' + safeTitle.substring(0, 20) + '...';
|
||
setTimeout(function() {
|
||
titleEl.textContent = safeTitle;
|
||
}, 1500);
|
||
})
|
||
.catch(function(err) {
|
||
var errorMsg = err && err.message ? err.message : '复制失败';
|
||
log('标题复制失败: ' + errorMsg);
|
||
updateStatus('复制失败,请检查剪贴板权限', 'error');
|
||
});
|
||
};
|
||
|
||
copyBtn.onclick = function() {
|
||
navigator.clipboard.writeText(safeLink)
|
||
.then(function() {
|
||
copyBtn.textContent = '已复制';
|
||
setTimeout(function() {
|
||
copyBtn.textContent = '复制';
|
||
}, 1000);
|
||
})
|
||
.catch(function(err) {
|
||
var errorMsg = err && err.message ? err.message : '复制失败';
|
||
log('按钮复制失败: ' + errorMsg);
|
||
updateStatus('复制失败,请检查剪贴板权限', 'error');
|
||
});
|
||
};
|
||
|
||
downloadBtn.onclick = function() {
|
||
triggerMagnetDownload(safeLink, downloadBtn);
|
||
};
|
||
|
||
item.appendChild(titleEl);
|
||
actions.appendChild(favoriteBtn);
|
||
actions.appendChild(copyBtn);
|
||
actions.appendChild(downloadBtn);
|
||
item.appendChild(actions);
|
||
|
||
list.appendChild(item);
|
||
setPanelView('results');
|
||
updateCount(list.children.length);
|
||
|
||
if (!options || !options.skipPersist) {
|
||
scheduleStatePersist();
|
||
}
|
||
}
|
||
|
||
function extractMagnets() {
|
||
var magnetPattern = /magnet:\?xt=urn:btih:[a-fA-F0-9]{32,40}/gi;
|
||
var links = new Set();
|
||
|
||
function walk(node) {
|
||
if (node.nodeType === Node.TEXT_NODE) {
|
||
var matches = node.textContent.match(magnetPattern);
|
||
if (matches) matches.forEach(function(m) { links.add(m); });
|
||
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
||
var tag = node.tagName.toLowerCase();
|
||
if (tag !== 'script' && tag !== 'style' && tag !== 'noscript') {
|
||
node.childNodes.forEach(walk);
|
||
}
|
||
}
|
||
}
|
||
walk(document.body);
|
||
document.querySelectorAll('a[href^="magnet:"]').forEach(function(a) { links.add(a.href); });
|
||
|
||
return Array.from(links);
|
||
}
|
||
|
||
var speedMode = 'fast';
|
||
var speedConfig = {
|
||
slow: {
|
||
thread: [300, 600],
|
||
page: [500, 900],
|
||
concurrency: 1,
|
||
fetchTimeout: 25000,
|
||
messageTimeout: 30000
|
||
},
|
||
medium: {
|
||
thread: [80, 180],
|
||
page: [150, 300],
|
||
concurrency: 2,
|
||
fetchTimeout: 18000,
|
||
messageTimeout: 22000
|
||
},
|
||
fast: {
|
||
thread: [0, 60],
|
||
page: [60, 150],
|
||
concurrency: 4,
|
||
fetchTimeout: 14000,
|
||
messageTimeout: 18000
|
||
},
|
||
ultrafast: {
|
||
thread: [0, 20],
|
||
page: [20, 80],
|
||
concurrency: 6,
|
||
fetchTimeout: 10000,
|
||
messageTimeout: 14000
|
||
}
|
||
};
|
||
|
||
function getSpeedProfile() {
|
||
return speedConfig[speedMode] || speedConfig.fast;
|
||
}
|
||
|
||
function getSpeedDelay(type) {
|
||
var profile = getSpeedProfile();
|
||
var range = profile[type] || [0, 0];
|
||
return Math.random() * (range[1] - range[0]) + range[0];
|
||
}
|
||
|
||
function getThreadConcurrency() {
|
||
return Math.max(1, getSpeedProfile().concurrency || 1);
|
||
}
|
||
|
||
function getFetchTimeout() {
|
||
return getSpeedProfile().fetchTimeout || 15000;
|
||
}
|
||
|
||
function getMessageTimeout() {
|
||
return getSpeedProfile().messageTimeout || 20000;
|
||
}
|
||
|
||
function sleep(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }
|
||
|
||
function sendRuntimeMessage(payload, timeoutMs) {
|
||
return new Promise(function(resolve, reject) {
|
||
if (!chrome.runtime || !chrome.runtime.id) {
|
||
reject(new Error('扩展已失效,请刷新页面'));
|
||
return;
|
||
}
|
||
|
||
var finished = false;
|
||
var safeTimeoutMs = Number(timeoutMs);
|
||
if (!Number.isFinite(safeTimeoutMs) || safeTimeoutMs <= 0) {
|
||
safeTimeoutMs = 15000;
|
||
}
|
||
var timer = setTimeout(function() {
|
||
if (finished) return;
|
||
finished = true;
|
||
reject(new Error('请求超时,请稍后重试'));
|
||
}, safeTimeoutMs);
|
||
|
||
try {
|
||
chrome.runtime.sendMessage(payload, function(response) {
|
||
if (finished) return;
|
||
finished = true;
|
||
clearTimeout(timer);
|
||
|
||
if (chrome.runtime.lastError) {
|
||
reject(new Error(chrome.runtime.lastError.message));
|
||
return;
|
||
}
|
||
|
||
resolve(response);
|
||
});
|
||
} catch (err) {
|
||
if (finished) return;
|
||
finished = true;
|
||
clearTimeout(timer);
|
||
reject(err);
|
||
}
|
||
});
|
||
}
|
||
|
||
function buildKeywordList(keyword) {
|
||
return String(keyword || '')
|
||
.split(',')
|
||
.map(function(item) { return item.trim(); })
|
||
.filter(function(item) { return !!item; });
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value || '').replace(/[&<>"']/g, function(character) {
|
||
return {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
}[character] || character;
|
||
});
|
||
}
|
||
|
||
function filterThreadsByKeywords(threadList, keywords) {
|
||
var normalizedThreads = normalizeCachedThreads(threadList);
|
||
if (!keywords || keywords.length === 0) {
|
||
return normalizedThreads;
|
||
}
|
||
|
||
return normalizedThreads.filter(function(thread) {
|
||
return keywords.some(function(keyword) {
|
||
return thread.title.indexOf(keyword) !== -1;
|
||
});
|
||
});
|
||
}
|
||
|
||
function mergeCoverageThreads(targetMap, threads) {
|
||
normalizeCachedThreads(threads).forEach(function(thread) {
|
||
if (!targetMap[thread.threadKey]) {
|
||
targetMap[thread.threadKey] = thread;
|
||
return;
|
||
}
|
||
|
||
if (!targetMap[thread.threadKey].title && thread.title) {
|
||
targetMap[thread.threadKey].title = thread.title;
|
||
}
|
||
if (!targetMap[thread.threadKey].url && thread.url) {
|
||
targetMap[thread.threadKey].url = thread.url;
|
||
}
|
||
});
|
||
}
|
||
|
||
function coverageMapToList(targetMap) {
|
||
return Object.keys(targetMap).map(function(threadKey) {
|
||
return targetMap[threadKey];
|
||
});
|
||
}
|
||
|
||
function getSmartFrontRefreshPages(startPage, endPage) {
|
||
if (startPage !== 1) {
|
||
return 0;
|
||
}
|
||
return Math.min(20, Math.max(0, endPage - startPage + 1));
|
||
}
|
||
|
||
async function probeForumUpdateDepth(forumKey, baseUrl, startPage, endPage) {
|
||
if (startPage !== 1) {
|
||
return { refreshPages: 0, probeThreads: null, reason: 'not_from_page1' };
|
||
}
|
||
|
||
var totalRange = endPage - startPage + 1;
|
||
var maxRefresh = Math.min(20, totalRange);
|
||
var probeUrl = baseUrl + '1.html';
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'fetchHtml',
|
||
url: probeUrl,
|
||
timeoutMs: getFetchTimeout()
|
||
}, getMessageTimeout());
|
||
|
||
if (!response || response.error || !response.html) {
|
||
log('[探针] 获取第1页失败,回退到固定刷新');
|
||
return { refreshPages: maxRefresh, probeThreads: null, reason: 'probe_fetch_failed' };
|
||
}
|
||
|
||
var liveThreads = extractThreadsFromHtml(response.html);
|
||
var threadsPerPage = liveThreads.length || 25;
|
||
|
||
if (liveThreads.length === 0) {
|
||
log('[探针] 第1页未提取到帖子,回退到固定刷新');
|
||
return { refreshPages: maxRefresh, probeThreads: null, reason: 'no_threads_extracted' };
|
||
}
|
||
|
||
var cachedPage = await sendRuntimeMessage({
|
||
action: 'cacheGetPageThreadKeys',
|
||
forumKey: forumKey,
|
||
page: 1
|
||
}, 5000);
|
||
|
||
if (!cachedPage || !cachedPage.ok || !cachedPage.threadKeys || cachedPage.threadKeys.length === 0) {
|
||
log('[探针] 无第1页缓存记录,首次抓取');
|
||
return { refreshPages: maxRefresh, probeThreads: liveThreads, probeHtml: response.html, reason: 'no_cached_page1' };
|
||
}
|
||
|
||
var cachedKeySet = Object.create(null);
|
||
cachedPage.threadKeys.forEach(function(key) {
|
||
var pureKey = String(key || '').split('::').pop();
|
||
cachedKeySet[pureKey] = true;
|
||
});
|
||
|
||
var newCount = 0;
|
||
liveThreads.forEach(function(thread) {
|
||
var key = thread.threadKey || getThreadKeyFromUrl(thread.url);
|
||
if (key && !cachedKeySet[key]) {
|
||
newCount++;
|
||
}
|
||
});
|
||
|
||
if (newCount === 0) {
|
||
log('[探针] 第1页与缓存完全一致,无需刷新');
|
||
return { refreshPages: 0, probeThreads: liveThreads, probeHtml: response.html, reason: 'no_change', newCount: 0 };
|
||
}
|
||
|
||
var estimatedShift = Math.ceil(newCount / threadsPerPage) + 1;
|
||
var refreshPages = Math.min(estimatedShift, maxRefresh);
|
||
|
||
log('[探针] 检测到 ' + newCount + ' 个新帖 (每页' + threadsPerPage + '帖),预估影响 ' + estimatedShift + ' 页,将刷新前 ' + refreshPages + ' 页');
|
||
|
||
return {
|
||
refreshPages: refreshPages,
|
||
probeThreads: liveThreads,
|
||
probeHtml: response.html,
|
||
newCount: newCount,
|
||
threadsPerPage: threadsPerPage,
|
||
reason: 'detected_' + newCount + '_new'
|
||
};
|
||
} catch (e) {
|
||
var errMsg = e && e.message ? e.message : String(e);
|
||
log('[探针] 异常: ' + errMsg + ',回退到固定刷新');
|
||
return { refreshPages: maxRefresh, probeThreads: null, reason: 'probe_error' };
|
||
}
|
||
}
|
||
|
||
async function getCachedCoveragePlan(forumKey, startPage, endPage, frontRefreshPages) {
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'cacheGetCoveragePlan',
|
||
forumKey: forumKey,
|
||
startPage: startPage,
|
||
endPage: endPage,
|
||
frontRefreshPages: frontRefreshPages
|
||
}, 8000);
|
||
|
||
if (!response || !response.ok) {
|
||
log('读取缓存计划失败: ' + (response && response.error ? response.error : '空响应'));
|
||
return null;
|
||
}
|
||
|
||
return response;
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('读取缓存计划异常: ' + errorMsg);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
async function saveCoverageSnapshot(forumKey, startPage, endPage, threads, frontRefreshPages, strategy) {
|
||
var normalizedThreads = normalizeCachedThreads(threads);
|
||
if (!forumKey || normalizedThreads.length === 0) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'cacheSaveCoverage',
|
||
forumKey: forumKey,
|
||
startPage: startPage,
|
||
endPage: endPage,
|
||
frontRefreshPages: frontRefreshPages,
|
||
strategy: strategy,
|
||
crawledAt: Date.now(),
|
||
threads: normalizedThreads
|
||
}, 12000);
|
||
|
||
if (!response || !response.ok) {
|
||
log('保存范围缓存失败: ' + (response && response.error ? response.error : '空响应'));
|
||
}
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('保存范围缓存异常: ' + errorMsg);
|
||
}
|
||
}
|
||
|
||
async function savePageCoverageSnapshot(forumKey, page, threads) {
|
||
var normalizedThreads = normalizeCachedThreads(threads);
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'cacheSavePageCoverage',
|
||
forumKey: forumKey,
|
||
page: page,
|
||
crawledAt: Date.now(),
|
||
threads: normalizedThreads
|
||
}, 12000);
|
||
|
||
if (!response || !response.ok) {
|
||
log('保存页缓存失败: ' + (response && response.error ? response.error : '空响应'));
|
||
}
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('保存页缓存异常: ' + errorMsg);
|
||
}
|
||
}
|
||
|
||
async function getCachedThreadMagnets(forumKey, threads) {
|
||
var normalizedThreads = normalizeCachedThreads(threads);
|
||
if (!forumKey || normalizedThreads.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'cacheGetThreadMagnets',
|
||
forumKey: forumKey,
|
||
threads: normalizedThreads
|
||
}, 12000);
|
||
|
||
if (!response || !response.ok || !Array.isArray(response.threads)) {
|
||
log('读取帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应'));
|
||
return [];
|
||
}
|
||
|
||
return response.threads.map(function(thread) {
|
||
return {
|
||
threadKey: typeof thread.threadKey === 'string' ? thread.threadKey : getThreadKeyFromUrl(thread.url),
|
||
url: normalizeThreadUrl(thread.url),
|
||
title: typeof thread.title === 'string' ? thread.title : '',
|
||
magnets: normalizeMagnetList(thread.magnets)
|
||
};
|
||
});
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('读取帖子磁链缓存异常: ' + errorMsg);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function saveThreadMagnetsToCache(forumKey, threads) {
|
||
var normalizedThreads = [];
|
||
|
||
(Array.isArray(threads) ? threads : []).forEach(function(thread) {
|
||
if (!thread || typeof thread !== 'object') {
|
||
return;
|
||
}
|
||
|
||
var normalizedUrl = normalizeThreadUrl(thread.url);
|
||
var threadKey = typeof thread.threadKey === 'string' && thread.threadKey
|
||
? thread.threadKey
|
||
: getThreadKeyFromUrl(normalizedUrl);
|
||
var magnets = normalizeMagnetList(thread.magnets);
|
||
|
||
if (!normalizedUrl || !threadKey || magnets.length === 0) {
|
||
return;
|
||
}
|
||
|
||
normalizedThreads.push({
|
||
threadKey: threadKey,
|
||
url: normalizedUrl,
|
||
title: typeof thread.title === 'string' ? thread.title : '',
|
||
magnets: magnets
|
||
});
|
||
});
|
||
|
||
if (!forumKey || normalizedThreads.length === 0) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'cacheSaveThreadMagnets',
|
||
forumKey: forumKey,
|
||
syncedAt: Date.now(),
|
||
threads: normalizedThreads
|
||
}, 12000);
|
||
|
||
if (!response || !response.ok) {
|
||
log('保存帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应'));
|
||
}
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('保存帖子磁链缓存异常: ' + errorMsg);
|
||
}
|
||
}
|
||
|
||
function formatTimeLabel(timestamp) {
|
||
var value = Number(timestamp);
|
||
if (!value) {
|
||
return '无';
|
||
}
|
||
|
||
var date = new Date(value);
|
||
var month = String(date.getMonth() + 1).padStart(2, '0');
|
||
var day = String(date.getDate()).padStart(2, '0');
|
||
var hour = String(date.getHours()).padStart(2, '0');
|
||
var minute = String(date.getMinutes()).padStart(2, '0');
|
||
return month + '-' + day + ' ' + hour + ':' + minute;
|
||
}
|
||
|
||
function formatBytesLabel(bytes) {
|
||
var size = Number(bytes);
|
||
if (!Number.isFinite(size) || size <= 0) {
|
||
return '0 B';
|
||
}
|
||
|
||
if (size < 1024) {
|
||
return size + ' B';
|
||
}
|
||
|
||
if (size < 1024 * 1024) {
|
||
return (size / 1024).toFixed(1) + ' KB';
|
||
}
|
||
|
||
return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
||
}
|
||
|
||
async function getCacheOverview(forumKey) {
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'cacheGetOverview',
|
||
forumKey: forumKey,
|
||
limit: 12
|
||
}, 12000);
|
||
|
||
if (!response || !response.ok) {
|
||
log('读取缓存总览失败: ' + (response && response.error ? response.error : '空响应'));
|
||
return null;
|
||
}
|
||
|
||
return response;
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('读取缓存总览异常: ' + errorMsg);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function renderCacheOverview(cacheOverview) {
|
||
var panel = document.getElementById('magnet-cache-panel');
|
||
if (!panel) {
|
||
return;
|
||
}
|
||
|
||
if (!cacheOverview || !cacheOverview.summary) {
|
||
panel.innerHTML = '<div class="magnet-cache-card"><div class="magnet-cache-card-label">缓存状态</div><div class="magnet-cache-card-value" style="font-size:14px">暂无缓存数据</div></div>';
|
||
return;
|
||
}
|
||
|
||
var summary = cacheOverview.summary;
|
||
var recentThreads = Array.isArray(cacheOverview.recentThreads) ? cacheOverview.recentThreads : [];
|
||
var recentCoverages = Array.isArray(cacheOverview.recentCoverages) ? cacheOverview.recentCoverages : [];
|
||
|
||
var html = '';
|
||
html += '<div class="magnet-cache-card" style="margin-bottom:12px;background:linear-gradient(135deg,#f7fbff,#eef6ff)"><div class="magnet-cache-card-label">说明</div><div class="magnet-cache-entry-meta">这里的帖子数是 <strong>唯一帖子数</strong>,同一板块重复搜索同一帖子不会重复累计;重复搜索只会新增/更新范围快照。</div></div>';
|
||
html += '<div class="magnet-cache-grid">';
|
||
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">当前板块唯一帖子</div><div class="magnet-cache-card-value">' + summary.forumThreadCount + '</div><div class="magnet-cache-entry-meta">范围快照 ' + summary.forumCoverageCount + ' 份 · 页缓存 ' + Number(summary.forumPageCoverageCount || 0) + ' 页</div></div>';
|
||
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">全部唯一帖子</div><div class="magnet-cache-card-value">' + summary.totalThreadCount + '</div><div class="magnet-cache-entry-meta">总快照 ' + summary.totalCoverageCount + ' 份 · 页缓存 ' + Number(summary.totalPageCoverageCount || 0) + ' 页</div></div>';
|
||
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">磁链缓存</div><div class="magnet-cache-card-value">' + summary.recentMagnetCount + '</div><div class="magnet-cache-entry-meta">最近命中 ' + summary.recentMagnetCachedThreads + ' 帖</div></div>';
|
||
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">存储占用</div><div class="magnet-cache-card-value" style="font-size:16px">' + formatBytesLabel(summary.storageUsage) + '</div><div class="magnet-cache-entry-meta">配额 ' + (summary.storageQuota ? formatBytesLabel(summary.storageQuota) : '未知') + '</div></div>';
|
||
html += '</div>';
|
||
|
||
html += '<div class="magnet-cache-section"><div class="magnet-cache-section-title">最近缓存帖子</div>';
|
||
if (recentThreads.length === 0) {
|
||
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">提示</div><div class="magnet-cache-entry-meta">当前板块还没有缓存帖子</div></div>';
|
||
} else {
|
||
recentThreads.forEach(function(item) {
|
||
var title = item.title || '未命名帖子';
|
||
html += '<div class="magnet-cache-entry">';
|
||
html += '<div class="magnet-cache-entry-title">' + escapeHtml(title) + '</div>';
|
||
html += '<div class="magnet-cache-entry-meta">磁链缓存:' + Number(item.magnetCount || 0) + ' 条 · 帖子缓存:' + formatTimeLabel(item.lastSeenAt) + ' · 磁链缓存:' + formatTimeLabel(item.lastMagnetSyncAt) + '</div>';
|
||
html += '</div>';
|
||
});
|
||
}
|
||
html += '</div>';
|
||
|
||
html += '<div class="magnet-cache-section"><div class="magnet-cache-section-title">最近范围快照</div>';
|
||
if (recentCoverages.length === 0) {
|
||
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">提示</div><div class="magnet-cache-entry-meta">暂无范围快照</div></div>';
|
||
} else {
|
||
recentCoverages.forEach(function(item) {
|
||
html += '<div class="magnet-cache-entry">';
|
||
html += '<div class="magnet-cache-entry-title">页码 ' + item.startPage + '-' + item.endPage + '</div>';
|
||
html += '<div class="magnet-cache-entry-meta">' + item.threadCount + ' 帖 · 策略 ' + item.strategy + ' · ' + formatTimeLabel(item.crawledAt) + '</div>';
|
||
html += '</div>';
|
||
});
|
||
}
|
||
html += '</div>';
|
||
|
||
panel.innerHTML = html;
|
||
}
|
||
|
||
async function refreshCacheOverview(options) {
|
||
options = options || {};
|
||
var cachePanel = document.getElementById('magnet-cache-panel');
|
||
if (cachePanel) {
|
||
cachePanel.innerHTML = '<div style="padding:8px;color:#8892a4;font-size:11px">正在读取缓存...</div>';
|
||
}
|
||
|
||
var overview = await getCacheOverview(getForumKey());
|
||
renderCacheOverview(overview);
|
||
|
||
if (options.showStatus) {
|
||
if (overview && overview.summary) {
|
||
updateStatus('已加载缓存:当前板块唯一帖子 ' + overview.summary.forumThreadCount + ' 帖,当前页缓存 ' + Number(overview.summary.forumPageCoverageCount || 0) + ' 页', 'done');
|
||
} else {
|
||
updateStatus('读取缓存失败,请稍后重试', 'error');
|
||
}
|
||
}
|
||
|
||
return overview;
|
||
}
|
||
|
||
async function clearAllCacheWithConfirm() {
|
||
if (!confirm('确认清空全部缓存吗?这不会删除当前页面已显示的结果。')) {
|
||
return;
|
||
}
|
||
|
||
if (!confirm('二次确认:真的要清空全部标题缓存、范围快照和磁链缓存吗?')) {
|
||
return;
|
||
}
|
||
|
||
updateStatus('正在清空缓存...', 'loading');
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({ action: 'cacheClearAll' }, 12000);
|
||
if (!response || !response.ok) {
|
||
updateStatus('清空缓存失败', 'error');
|
||
return;
|
||
}
|
||
|
||
await refreshCacheOverview();
|
||
updateStatus('已清空全部缓存', 'done');
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('清空缓存异常: ' + errorMsg);
|
||
updateStatus('清空缓存失败:' + errorMsg, 'error');
|
||
}
|
||
}
|
||
|
||
function toggleCachePanel() {
|
||
var cacheView = document.getElementById('magnet-cache-view');
|
||
var resultsView = document.getElementById('magnet-results-view');
|
||
if (!cacheView || !resultsView) {
|
||
return;
|
||
}
|
||
|
||
var isVisible = cacheView.classList.contains('is-active');
|
||
setPanelView(isVisible ? 'results' : 'cache');
|
||
|
||
if (!isVisible) {
|
||
refreshCacheOverview({ showStatus: true });
|
||
}
|
||
}
|
||
|
||
async function applyCachedMagnetHits(pageLabel, filteredThreads, context) {
|
||
var cachedEntries = await getCachedThreadMagnets(context.forumKey, filteredThreads);
|
||
var cachedMap = Object.create(null);
|
||
var threadsToFetch = [];
|
||
var cachedThreadCount = 0;
|
||
var cachedMagnetCount = 0;
|
||
|
||
cachedEntries.forEach(function(entry) {
|
||
if (!entry || !entry.threadKey) return;
|
||
cachedMap[entry.threadKey] = entry;
|
||
});
|
||
|
||
normalizeCachedThreads(filteredThreads).forEach(function(thread) {
|
||
var threadKey = thread.threadKey || getThreadKeyFromUrl(thread.url);
|
||
var cachedEntry = threadKey ? cachedMap[threadKey] : null;
|
||
var cachedMagnets = cachedEntry ? normalizeMagnetList(cachedEntry.magnets) : [];
|
||
|
||
if (cachedMagnets.length === 0) {
|
||
if (!context.processedThreadKeys[threadKey]) {
|
||
threadsToFetch.push(thread);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (context.processedThreadKeys[threadKey]) {
|
||
return;
|
||
}
|
||
|
||
context.processedThreadKeys[threadKey] = true;
|
||
cachedThreadCount += 1;
|
||
context.totalFetched += 1;
|
||
|
||
cachedMagnets.forEach(function(magnet) {
|
||
if (context.allMagnets.has(magnet)) {
|
||
return;
|
||
}
|
||
|
||
context.allMagnets.add(magnet);
|
||
cachedMagnetCount += 1;
|
||
addMagnetItem(thread.title || cachedEntry.title || '缓存帖子', magnet);
|
||
});
|
||
});
|
||
|
||
if (cachedThreadCount > 0) {
|
||
updateStatus(
|
||
'正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,直接读取缓存磁链 ' + cachedThreadCount + ' 帖(' + cachedMagnetCount + ' 条)',
|
||
'loading'
|
||
);
|
||
}
|
||
|
||
return {
|
||
threadsToFetch: threadsToFetch,
|
||
cachedThreadCount: cachedThreadCount,
|
||
cachedMagnetCount: cachedMagnetCount,
|
||
pageLabel: pageLabel
|
||
};
|
||
}
|
||
|
||
var STATE_BACKUP_KEY = 'magnetPluginStateBackupV1';
|
||
var MAX_STATE_ITEMS = 2000;
|
||
var statePersistTimer = null;
|
||
var hasRestoredProgress = false;
|
||
var lastRestoredState = null;
|
||
var manualClearRequested = false;
|
||
|
||
var isFetching = false;
|
||
var stopFetching = false;
|
||
var allMagnetLinks = [];
|
||
var allMagnetRecords = [];
|
||
var magnetRecordMap = Object.create(null);
|
||
var progressRuntime = {
|
||
isRunning: false,
|
||
stoppedByUser: false,
|
||
startPage: 1,
|
||
endPage: 1,
|
||
resumeFromPage: 1,
|
||
keyword: '',
|
||
speedMode: 'fast'
|
||
};
|
||
|
||
function resetProgressRuntimeToIdle() {
|
||
var startInput = document.getElementById('page-start');
|
||
var endInput = document.getElementById('page-end');
|
||
var keywordInput = document.getElementById('keyword-input');
|
||
var currentStart = startInput ? parseInt(startInput.value, 10) || 1 : 1;
|
||
var currentEnd = endInput ? parseInt(endInput.value, 10) || currentStart : currentStart;
|
||
|
||
progressRuntime.isRunning = false;
|
||
progressRuntime.stoppedByUser = true;
|
||
progressRuntime.startPage = currentStart;
|
||
progressRuntime.endPage = Math.max(currentStart, currentEnd);
|
||
progressRuntime.resumeFromPage = currentStart;
|
||
progressRuntime.keyword = keywordInput ? keywordInput.value.trim() : '';
|
||
progressRuntime.speedMode = speedConfig[speedMode] ? speedMode : 'fast';
|
||
}
|
||
|
||
function clearSessionBackupState() {
|
||
sessionBackupState = null;
|
||
}
|
||
|
||
async function clearAllResults() {
|
||
manualClearRequested = true;
|
||
stopFetching = true;
|
||
resetProgressRuntimeToIdle();
|
||
clearMagnetList(true);
|
||
clearSessionBackupState();
|
||
|
||
try {
|
||
await sendRuntimeMessage({ action: 'clearProgressState' }, 6000);
|
||
} catch (e) {
|
||
var clearErrorMsg = e && e.message ? e.message : String(e);
|
||
log('远端清理失败: ' + clearErrorMsg);
|
||
}
|
||
|
||
if (!isFetching) {
|
||
manualClearRequested = false;
|
||
updateStatus('已手动清空结果', 'done');
|
||
scheduleStatePersist();
|
||
} else {
|
||
updateStatus('正在停止任务并清空结果...', 'loading');
|
||
}
|
||
}
|
||
|
||
function buildStateSnapshot() {
|
||
var keywordInput = document.getElementById('keyword-input');
|
||
var startInput = document.getElementById('page-start');
|
||
var endInput = document.getElementById('page-end');
|
||
var speedSelect = document.getElementById('speed-select');
|
||
var statusEl = document.getElementById('magnet-status');
|
||
|
||
var startPage = startInput ? parseInt(startInput.value, 10) || 1 : 1;
|
||
var endPage = endInput ? parseInt(endInput.value, 10) || startPage : startPage;
|
||
var resumeFromPage = Number(progressRuntime.resumeFromPage);
|
||
if (!Number.isFinite(resumeFromPage) || resumeFromPage < 1) {
|
||
resumeFromPage = startPage;
|
||
}
|
||
|
||
return {
|
||
links: allMagnetLinks.slice(-MAX_STATE_ITEMS),
|
||
items: allMagnetRecords.slice(-MAX_STATE_ITEMS),
|
||
keyword: keywordInput ? keywordInput.value.trim() : '',
|
||
speedMode: speedConfig[speedMode] ? speedMode : 'fast',
|
||
startPage: startPage,
|
||
endPage: endPage,
|
||
resumeFromPage: resumeFromPage,
|
||
isRunning: !!progressRuntime.isRunning,
|
||
stoppedByUser: !!progressRuntime.stoppedByUser,
|
||
statusText: statusEl ? statusEl.textContent || '' : '',
|
||
statusType: statusEl ? statusEl.getAttribute('data-type') || '' : '',
|
||
pageUrl: window.location.href,
|
||
updatedAt: Date.now()
|
||
};
|
||
}
|
||
|
||
function saveStateToSessionBackup(snapshot) {
|
||
sessionBackupState = snapshot;
|
||
}
|
||
|
||
function readStateFromSessionBackup() {
|
||
return sessionBackupState;
|
||
}
|
||
|
||
function applyStateSnapshot(snapshot) {
|
||
if (!snapshot || typeof snapshot !== 'object') {
|
||
return false;
|
||
}
|
||
|
||
var speedSelect = document.getElementById('speed-select');
|
||
if (speedSelect && snapshot.speedMode && speedConfig[snapshot.speedMode]) {
|
||
speedSelect.value = snapshot.speedMode;
|
||
speedMode = snapshot.speedMode;
|
||
}
|
||
|
||
var keywordInput = document.getElementById('keyword-input');
|
||
if (keywordInput && typeof snapshot.keyword === 'string') {
|
||
keywordInput.value = snapshot.keyword;
|
||
}
|
||
|
||
var startInput = document.getElementById('page-start');
|
||
if (startInput && Number.isFinite(Number(snapshot.startPage))) {
|
||
startInput.value = Math.max(1, Number(snapshot.startPage));
|
||
}
|
||
|
||
var endInput = document.getElementById('page-end');
|
||
if (endInput && Number.isFinite(Number(snapshot.endPage))) {
|
||
endInput.value = Math.max(1, Number(snapshot.endPage));
|
||
}
|
||
|
||
progressRuntime.startPage = Number.isFinite(Number(snapshot.startPage))
|
||
? Math.max(1, Number(snapshot.startPage))
|
||
: 1;
|
||
progressRuntime.endPage = Number.isFinite(Number(snapshot.endPage))
|
||
? Math.max(progressRuntime.startPage, Number(snapshot.endPage))
|
||
: progressRuntime.startPage;
|
||
progressRuntime.resumeFromPage = Number.isFinite(Number(snapshot.resumeFromPage))
|
||
? Math.max(1, Number(snapshot.resumeFromPage))
|
||
: progressRuntime.startPage;
|
||
progressRuntime.isRunning = !!snapshot.isRunning;
|
||
progressRuntime.stoppedByUser = !!snapshot.stoppedByUser;
|
||
progressRuntime.keyword = typeof snapshot.keyword === 'string' ? snapshot.keyword : '';
|
||
progressRuntime.speedMode = speedConfig[snapshot.speedMode] ? snapshot.speedMode : 'fast';
|
||
|
||
var records = Array.isArray(snapshot.items) && snapshot.items.length > 0
|
||
? snapshot.items
|
||
: (Array.isArray(snapshot.links)
|
||
? snapshot.links.map(function(link) {
|
||
return {
|
||
title: '恢复记录',
|
||
link: link
|
||
};
|
||
})
|
||
: []);
|
||
|
||
clearMagnetList(true);
|
||
records.forEach(function(record) {
|
||
if (!record || typeof record !== 'object') return;
|
||
if (typeof record.link !== 'string' || !record.link) return;
|
||
var title = typeof record.title === 'string' ? record.title : '恢复记录';
|
||
addMagnetItem(title, record.link, { skipPersist: true });
|
||
});
|
||
|
||
if (allMagnetLinks.length > 0) {
|
||
var statusText = typeof snapshot.statusText === 'string' && snapshot.statusText
|
||
? '已恢复: ' + snapshot.statusText
|
||
: '已恢复上次抓取记录,共' + allMagnetLinks.length + '条';
|
||
updateStatus(statusText, snapshot.statusType || 'done');
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function saveStateNow() {
|
||
var snapshot = buildStateSnapshot();
|
||
saveStateToSessionBackup(snapshot);
|
||
|
||
sendRuntimeMessage({
|
||
action: 'saveProgressState',
|
||
state: snapshot
|
||
}, 6000).catch(function(e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('远端状态保存失败: ' + errorMsg);
|
||
});
|
||
}
|
||
|
||
function scheduleStatePersist() {
|
||
if (statePersistTimer) {
|
||
clearTimeout(statePersistTimer);
|
||
}
|
||
|
||
statePersistTimer = setTimeout(function() {
|
||
statePersistTimer = null;
|
||
saveStateNow();
|
||
}, 500);
|
||
}
|
||
|
||
async function restoreProgressState() {
|
||
if (hasRestoredProgress) {
|
||
return {
|
||
restored: allMagnetLinks.length > 0,
|
||
state: lastRestoredState
|
||
};
|
||
}
|
||
hasRestoredProgress = true;
|
||
|
||
var sessionState = readStateFromSessionBackup();
|
||
var sessionStateTime = sessionState && Number.isFinite(Number(sessionState.updatedAt))
|
||
? Number(sessionState.updatedAt)
|
||
: 0;
|
||
var remoteState = null;
|
||
var remoteStateTime = 0;
|
||
|
||
try {
|
||
var response = await sendRuntimeMessage({ action: 'loadProgressState' }, 6000);
|
||
if (response && response.ok && response.state) {
|
||
remoteState = response.state;
|
||
remoteStateTime = Number.isFinite(Number(response.state.updatedAt))
|
||
? Number(response.state.updatedAt)
|
||
: 0;
|
||
}
|
||
} catch (e) {
|
||
var errorMsg = e && e.message ? e.message : String(e);
|
||
log('远端状态恢复失败: ' + errorMsg);
|
||
}
|
||
|
||
var chosenState = null;
|
||
if (sessionState) {
|
||
chosenState = sessionState;
|
||
}
|
||
if (remoteState && (!chosenState || remoteStateTime >= sessionStateTime)) {
|
||
chosenState = remoteState;
|
||
}
|
||
|
||
lastRestoredState = chosenState;
|
||
var restored = chosenState ? applyStateSnapshot(chosenState) : false;
|
||
|
||
return {
|
||
restored: restored,
|
||
state: chosenState
|
||
};
|
||
}
|
||
|
||
window.addEventListener('pagehide', function() {
|
||
if (statePersistTimer) {
|
||
clearTimeout(statePersistTimer);
|
||
statePersistTimer = null;
|
||
}
|
||
saveStateNow();
|
||
});
|
||
|
||
async function fetchThreadsInParallel(page, filteredThreads, allMagnets, processedThreadKeys, options) {
|
||
if (!filteredThreads || filteredThreads.length === 0) {
|
||
return 0;
|
||
}
|
||
|
||
options = options || {};
|
||
|
||
var concurrency = Math.min(getThreadConcurrency(), filteredThreads.length);
|
||
var cursor = 0;
|
||
var fetchedCount = 0;
|
||
|
||
async function worker() {
|
||
while (!stopFetching) {
|
||
var currentIndex = cursor;
|
||
cursor += 1;
|
||
if (currentIndex >= filteredThreads.length) {
|
||
return;
|
||
}
|
||
|
||
var threadItem = filteredThreads[currentIndex];
|
||
var threadUrl = threadItem.url;
|
||
var threadTitle = threadItem.title || '未命名帖子';
|
||
var threadKey = threadItem.threadKey || getThreadKeyFromUrl(threadUrl);
|
||
|
||
if (processedThreadKeys && threadKey) {
|
||
if (processedThreadKeys[threadKey]) {
|
||
continue;
|
||
}
|
||
processedThreadKeys[threadKey] = true;
|
||
}
|
||
|
||
if (!options.suppressStatusProgress) {
|
||
updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading');
|
||
}
|
||
if (options.onProgress) {
|
||
options.onProgress(currentIndex + 1, filteredThreads.length);
|
||
}
|
||
|
||
try {
|
||
var threadResponse = await sendRuntimeMessage({
|
||
action: 'openAndFetch',
|
||
url: threadUrl,
|
||
timeoutMs: getFetchTimeout()
|
||
}, getMessageTimeout());
|
||
|
||
if (threadResponse && threadResponse.error) {
|
||
log('帖子请求失败: ' + threadUrl + ' - ' + threadResponse.error);
|
||
} else if (threadResponse && threadResponse.magnets) {
|
||
var threadMagnets = normalizeMagnetList(threadResponse.magnets);
|
||
if (threadMagnets.length > 0 && options.forumKey) {
|
||
try {
|
||
await saveThreadMagnetsToCache(options.forumKey, [{
|
||
threadKey: threadKey,
|
||
url: threadUrl,
|
||
title: threadTitle,
|
||
magnets: threadMagnets
|
||
}]);
|
||
} catch (saveErr) {
|
||
log('实时保存线程缓存失败: ' + (saveErr && saveErr.message ? saveErr.message : String(saveErr)));
|
||
}
|
||
}
|
||
|
||
threadMagnets.forEach(function(m) {
|
||
if (!allMagnets.has(m)) {
|
||
allMagnets.add(m);
|
||
addMagnetItem(threadTitle, m);
|
||
}
|
||
});
|
||
}
|
||
} catch (e) {
|
||
var threadErrorMsg = e && e.message ? e.message : String(e);
|
||
log('获取帖子失败: ' + threadUrl + ' - ' + threadErrorMsg);
|
||
}
|
||
|
||
fetchedCount += 1;
|
||
var delay = getSpeedDelay('thread');
|
||
if (delay > 0) {
|
||
await sleep(delay);
|
||
}
|
||
}
|
||
}
|
||
|
||
var workers = [];
|
||
for (var workerIndex = 0; workerIndex < concurrency; workerIndex++) {
|
||
workers.push(worker());
|
||
}
|
||
|
||
await Promise.all(workers);
|
||
|
||
return fetchedCount;
|
||
}
|
||
|
||
async function processCachedThreadBatch(pageLabel, threads, context, statusText) {
|
||
var normalizedThreads = normalizeCachedThreads(threads);
|
||
var rangeText = typeof pageLabel === 'string' ? pageLabel : String(pageLabel || '');
|
||
var rangeMatch = rangeText.match(/^(\d+)(?:-(\d+))?$/);
|
||
var rangeStart = rangeMatch ? Number(rangeMatch[1]) : Number(context.startPage || 1);
|
||
var rangeEnd = rangeMatch ? Math.max(rangeStart, Number(rangeMatch[2] || rangeMatch[1])) : rangeStart;
|
||
if (normalizedThreads.length === 0) {
|
||
return;
|
||
}
|
||
|
||
if (statusText) {
|
||
updateStatus(statusText, 'loading');
|
||
}
|
||
|
||
mergeCoverageThreads(context.coverageThreadMap, normalizedThreads);
|
||
var filteredThreads = filterThreadsByKeywords(normalizedThreads, context.keywords);
|
||
context.matchedThreads += filteredThreads.length;
|
||
|
||
if (filteredThreads.length === 0) {
|
||
updateStatus('正在搜索缓存:已检查缓存标题 ' + normalizedThreads.length + ' 帖,当前关键词未命中', 'loading');
|
||
return;
|
||
}
|
||
|
||
var cacheResult = await applyCachedMagnetHits(pageLabel, filteredThreads, context);
|
||
if (cacheResult.threadsToFetch.length === 0) {
|
||
updateStatus('正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,结果已全部由缓存秒出', 'loading');
|
||
return;
|
||
}
|
||
|
||
updateStatus(
|
||
'正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖',
|
||
'loading'
|
||
);
|
||
context.totalFetched += await fetchThreadsInParallel(
|
||
pageLabel,
|
||
cacheResult.threadsToFetch,
|
||
context.allMagnets,
|
||
context.processedThreadKeys,
|
||
{
|
||
forumKey: context.forumKey,
|
||
suppressStatusProgress: true,
|
||
onProgress: function(threadIndex, threadTotal) {
|
||
updateRangeThreadProgress(context, rangeStart, rangeEnd, threadIndex, threadTotal);
|
||
}
|
||
}
|
||
);
|
||
}
|
||
|
||
async function fetchLivePageRange(startPage, endPage, context) {
|
||
if (startPage > endPage) {
|
||
return;
|
||
}
|
||
|
||
for (var page = startPage; page <= endPage; page++) {
|
||
if (stopFetching) break;
|
||
|
||
progressRuntime.resumeFromPage = page;
|
||
scheduleStatePersist();
|
||
|
||
if (!chrome.runtime || !chrome.runtime.id) {
|
||
updateStatus('扩展已失效,请刷新页面', 'error');
|
||
break;
|
||
}
|
||
|
||
updateStatus('第' + page + '/' + context.normalizedEnd + '页...', 'loading');
|
||
updateProgress(page - context.startPage + 1, context.normalizedEnd - context.startPage + 1, '第' + page + '/' + context.normalizedEnd + '页');
|
||
|
||
var pageUrl = context.baseUrl + page + '.html';
|
||
try {
|
||
var response = await sendRuntimeMessage({
|
||
action: 'fetchHtml',
|
||
url: pageUrl,
|
||
timeoutMs: getFetchTimeout()
|
||
}, getMessageTimeout());
|
||
|
||
if (!response || response.error) {
|
||
context.failedPages += 1;
|
||
progressRuntime.resumeFromPage = page + 1;
|
||
scheduleStatePersist();
|
||
log('获取第 ' + page + ' 页失败: ' + (response && response.error ? response.error : '空响应'));
|
||
continue;
|
||
}
|
||
|
||
if (!response.html) {
|
||
progressRuntime.resumeFromPage = page + 1;
|
||
scheduleStatePersist();
|
||
continue;
|
||
}
|
||
|
||
var threadList = extractThreadsFromHtml(response.html);
|
||
mergeCoverageThreads(context.coverageThreadMap, threadList);
|
||
await savePageCoverageSnapshot(context.forumKey, page, threadList);
|
||
|
||
var filteredThreads = filterThreadsByKeywords(threadList, context.keywords);
|
||
context.matchedThreads += filteredThreads.length;
|
||
var cacheResult = await applyCachedMagnetHits(page, filteredThreads, context);
|
||
if (cacheResult.threadsToFetch.length > 0) {
|
||
if (cacheResult.cachedThreadCount > 0) {
|
||
updateStatus(
|
||
'第' + page + '/' + context.normalizedEnd + '页:缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖',
|
||
'loading'
|
||
);
|
||
}
|
||
|
||
context.totalFetched += await fetchThreadsInParallel(
|
||
page,
|
||
cacheResult.threadsToFetch,
|
||
context.allMagnets,
|
||
context.processedThreadKeys,
|
||
{ forumKey: context.forumKey }
|
||
);
|
||
}
|
||
progressRuntime.resumeFromPage = page + 1;
|
||
scheduleStatePersist();
|
||
} catch (e) {
|
||
context.failedPages += 1;
|
||
progressRuntime.resumeFromPage = page + 1;
|
||
scheduleStatePersist();
|
||
var pageErrorMsg = e && e.message ? e.message : String(e);
|
||
log('获取第 ' + page + ' 页异常: ' + pageErrorMsg);
|
||
}
|
||
|
||
if (stopFetching) break;
|
||
|
||
var pageDelay = getSpeedDelay('page');
|
||
if (pageDelay > 0) {
|
||
await sleep(pageDelay);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function fetchFromPage(startPage, endPage, options) {
|
||
if (isFetching) return;
|
||
|
||
options = options || {};
|
||
var preserveExisting = !!options.preserveExisting;
|
||
var earlyStart = Math.max(1, parseInt(startPage, 10) || 1);
|
||
var earlyEnd = Math.max(earlyStart, parseInt(endPage, 10) || earlyStart);
|
||
|
||
isFetching = true;
|
||
stopFetching = false;
|
||
progressRuntime.isRunning = true;
|
||
progressRuntime.stoppedByUser = false;
|
||
progressRuntime.startPage = earlyStart;
|
||
progressRuntime.endPage = earlyEnd;
|
||
progressRuntime.resumeFromPage = earlyStart;
|
||
resetProgress();
|
||
|
||
var panel = createFloatingPanel();
|
||
var ball = document.getElementById('magnet-float-ball');
|
||
var startTime = Date.now();
|
||
panel.style.display = 'flex';
|
||
if (ball) ball.style.display = 'none';
|
||
if (!preserveExisting) {
|
||
clearMagnetList(true);
|
||
}
|
||
|
||
try {
|
||
var normalizedStart = Math.max(1, parseInt(startPage, 10) || 1);
|
||
var normalizedEnd = Math.max(normalizedStart, parseInt(endPage, 10) || normalizedStart);
|
||
|
||
var baseUrl = getBaseUrl();
|
||
var keywordInput = document.getElementById('keyword-input');
|
||
var speedSelect = document.getElementById('speed-select');
|
||
|
||
var keyword = typeof options.keyword === 'string'
|
||
? options.keyword.trim()
|
||
: (keywordInput ? keywordInput.value.trim() : '');
|
||
|
||
var selectedSpeed = typeof options.speedMode === 'string'
|
||
? options.speedMode
|
||
: (speedSelect ? speedSelect.value : 'fast');
|
||
|
||
speedMode = speedConfig[selectedSpeed] ? selectedSpeed : 'fast';
|
||
if (speedSelect) {
|
||
speedSelect.value = speedMode;
|
||
}
|
||
if (keywordInput) {
|
||
keywordInput.value = keyword;
|
||
}
|
||
|
||
progressRuntime.isRunning = true;
|
||
progressRuntime.stoppedByUser = false;
|
||
progressRuntime.startPage = normalizedStart;
|
||
progressRuntime.endPage = normalizedEnd;
|
||
progressRuntime.resumeFromPage = normalizedStart;
|
||
progressRuntime.keyword = keyword;
|
||
progressRuntime.speedMode = speedMode;
|
||
|
||
if (!baseUrl) {
|
||
updateStatus('无法获取页面URL', 'error');
|
||
progressRuntime.isRunning = false;
|
||
scheduleStatePersist();
|
||
return;
|
||
}
|
||
|
||
log('开始获取第 ' + normalizedStart + ' 到 ' + normalizedEnd + ' 页, 关键词: ' + keyword + ', 速度:' + speedMode + ', 并发:' + getThreadConcurrency());
|
||
if (preserveExisting && allMagnetLinks.length > 0) {
|
||
updateStatus('检测到未完成任务,已恢复' + allMagnetLinks.length + '条,继续抓取...', 'loading');
|
||
} else {
|
||
updateStatus('开始获取...', 'loading');
|
||
}
|
||
scheduleStatePersist();
|
||
|
||
var forumKey = getForumKey();
|
||
var searchContext = {
|
||
forumKey: forumKey,
|
||
baseUrl: baseUrl,
|
||
startPage: normalizedStart,
|
||
normalizedEnd: normalizedEnd,
|
||
totalPageCount: normalizedEnd - normalizedStart + 1,
|
||
keywords: buildKeywordList(keyword),
|
||
coverageThreadMap: Object.create(null),
|
||
processedThreadKeys: Object.create(null),
|
||
allMagnets: new Set(allMagnetLinks),
|
||
matchedThreads: 0,
|
||
totalFetched: 0,
|
||
failedPages: 0
|
||
};
|
||
|
||
var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd);
|
||
|
||
updateStatus('正在规划缓存:检查本地缓存 / 云端规划 / 复用块...', 'loading');
|
||
var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages);
|
||
var cacheStrategy = 'full_live';
|
||
var probeResult = null;
|
||
|
||
if (cachePlan && cachePlan.exactCoverage && Array.isArray(cachePlan.exactCoverage.threads) && cachePlan.exactCoverage.threads.length > 0) {
|
||
cacheStrategy = 'exact_cache';
|
||
await processCachedThreadBatch('缓存', cachePlan.exactCoverage.threads, searchContext, '命中当前板块缓存,直接搜索...');
|
||
progressRuntime.resumeFromPage = normalizedEnd + 1;
|
||
scheduleStatePersist();
|
||
|
||
if (normalizedStart === 1) {
|
||
updateStatus('智能探测:检测论坛是否有更新...', 'loading');
|
||
probeResult = await probeForumUpdateDepth(forumKey, baseUrl, normalizedStart, normalizedEnd);
|
||
|
||
if (probeResult.refreshPages > 0) {
|
||
frontRefreshPages = probeResult.refreshPages;
|
||
log('[智能更新] 缓存命中,探针检测到' + (probeResult.newCount || 0) + '个新帖,刷新前' + frontRefreshPages + '页');
|
||
updateStatus('发现' + (probeResult.newCount || 0) + '个新帖,刷新前' + frontRefreshPages + '页...', 'loading');
|
||
|
||
if (probeResult.probeThreads) {
|
||
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
|
||
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
|
||
var probeFiltered = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
|
||
searchContext.matchedThreads += probeFiltered.length;
|
||
var probeCacheResult = await applyCachedMagnetHits(1, probeFiltered, searchContext);
|
||
if (probeCacheResult.threadsToFetch.length > 0) {
|
||
searchContext.totalFetched += await fetchThreadsInParallel(
|
||
1, probeCacheResult.threadsToFetch, searchContext.allMagnets,
|
||
searchContext.processedThreadKeys, { forumKey: forumKey }
|
||
);
|
||
}
|
||
}
|
||
|
||
var exactRefreshEnd = Math.min(normalizedEnd, normalizedStart + frontRefreshPages - 1);
|
||
if (exactRefreshEnd >= 2) {
|
||
await fetchLivePageRange(2, exactRefreshEnd, searchContext);
|
||
}
|
||
if (!stopFetching) {
|
||
await saveCoverageSnapshot(
|
||
forumKey, normalizedStart, normalizedEnd,
|
||
coverageMapToList(searchContext.coverageThreadMap),
|
||
frontRefreshPages, 'exact_cache_refreshed'
|
||
);
|
||
}
|
||
} else {
|
||
log('[智能更新] 缓存命中,探针未检测到变化,补充处理探针数据以填补缓存缺口');
|
||
if (probeResult.probeThreads && probeResult.probeThreads.length > 0) {
|
||
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
|
||
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
|
||
var gapFiltered = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
|
||
if (gapFiltered.length > 0) {
|
||
var gapCacheResult = await applyCachedMagnetHits(1, gapFiltered, searchContext);
|
||
if (gapCacheResult.threadsToFetch.length > 0) {
|
||
log('[智能更新] 探针填补缺口:发现' + gapCacheResult.threadsToFetch.length + '个缓存未覆盖的帖子,补抓中...');
|
||
searchContext.totalFetched += await fetchThreadsInParallel(
|
||
1, gapCacheResult.threadsToFetch, searchContext.allMagnets,
|
||
searchContext.processedThreadKeys, { forumKey: forumKey }
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
if (normalizedStart === 1) {
|
||
updateStatus('智能探测:检测论坛更新情况...', 'loading');
|
||
probeResult = await probeForumUpdateDepth(forumKey, baseUrl, normalizedStart, normalizedEnd);
|
||
frontRefreshPages = probeResult.refreshPages;
|
||
|
||
if (probeResult.reason === 'no_change') {
|
||
log('[智能更新] 探针未检测到变化,补充处理探针数据以填补缓存缺口');
|
||
if (probeResult.probeThreads && probeResult.probeThreads.length > 0) {
|
||
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
|
||
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
|
||
var gapFiltered3 = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
|
||
if (gapFiltered3.length > 0) {
|
||
var gapCacheResult3 = await applyCachedMagnetHits(1, gapFiltered3, searchContext);
|
||
if (gapCacheResult3.threadsToFetch.length > 0) {
|
||
log('[智能更新] 探针填补缺口:发现' + gapCacheResult3.threadsToFetch.length + '个缓存未覆盖的帖子,补抓中...');
|
||
searchContext.totalFetched += await fetchThreadsInParallel(
|
||
1, gapCacheResult3.threadsToFetch, searchContext.allMagnets,
|
||
searchContext.processedThreadKeys, { forumKey: forumKey }
|
||
);
|
||
}
|
||
}
|
||
}
|
||
} else if (frontRefreshPages > 0) {
|
||
log('[智能更新] 探针检测到更新,需刷新前' + frontRefreshPages + '页');
|
||
}
|
||
}
|
||
|
||
var refreshedFrontEnd = normalizedStart - 1;
|
||
var shiftedReuseEnd = normalizedStart - 1;
|
||
var cachedBlocks = cachePlan && Array.isArray(cachePlan.cachedBlocks) ? cachePlan.cachedBlocks.slice() : [];
|
||
var liveCursor = normalizedStart;
|
||
|
||
if (frontRefreshPages > 0) {
|
||
refreshedFrontEnd = Math.min(normalizedEnd, normalizedStart + frontRefreshPages - 1);
|
||
|
||
if (probeResult && probeResult.probeThreads) {
|
||
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
|
||
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
|
||
var probeFiltered2 = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
|
||
searchContext.matchedThreads += probeFiltered2.length;
|
||
var probeCacheResult2 = await applyCachedMagnetHits(1, probeFiltered2, searchContext);
|
||
if (probeCacheResult2.threadsToFetch.length > 0) {
|
||
searchContext.totalFetched += await fetchThreadsInParallel(
|
||
1, probeCacheResult2.threadsToFetch, searchContext.allMagnets,
|
||
searchContext.processedThreadKeys, { forumKey: forumKey }
|
||
);
|
||
}
|
||
|
||
if (refreshedFrontEnd >= 2) {
|
||
updateStatus('智能增量:刷新第2-' + refreshedFrontEnd + '页 (发现' + (probeResult.newCount || 0) + '个新帖)...', 'loading');
|
||
await fetchLivePageRange(2, refreshedFrontEnd, searchContext);
|
||
}
|
||
} else {
|
||
updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading');
|
||
await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext);
|
||
}
|
||
liveCursor = refreshedFrontEnd + 1;
|
||
}
|
||
|
||
cachedBlocks = cachedBlocks.filter(function(block) {
|
||
return block && Array.isArray(block.threads) && block.threads.length > 0 && Number(block.endPage || 0) >= liveCursor;
|
||
}).sort(function(a, b) {
|
||
return Number(a.startPage || 0) - Number(b.startPage || 0);
|
||
});
|
||
|
||
if (!stopFetching && cachedBlocks.length > 0) {
|
||
cacheStrategy = 'assembled_pages';
|
||
for (var blockIndex = 0; blockIndex < cachedBlocks.length; blockIndex++) {
|
||
var cachedBlock = cachedBlocks[blockIndex];
|
||
var blockStart = Math.max(liveCursor, Number(cachedBlock.startPage) || liveCursor);
|
||
var blockEnd = Math.min(normalizedEnd, Number(cachedBlock.endPage) || blockStart);
|
||
if (blockStart > blockEnd) {
|
||
continue;
|
||
}
|
||
if (liveCursor < blockStart) {
|
||
updateStatus('缓存复用:补抓缺口页 ' + liveCursor + '-' + (blockStart - 1), 'loading');
|
||
await fetchLivePageRange(liveCursor, blockStart - 1, searchContext);
|
||
}
|
||
await processCachedThreadBatch(
|
||
String(blockStart) + '-' + String(blockEnd),
|
||
cachedBlock.threads,
|
||
searchContext,
|
||
'缓存复用:直接使用页缓存 ' + blockStart + '-' + blockEnd + ' 页'
|
||
);
|
||
shiftedReuseEnd = Math.max(shiftedReuseEnd, blockEnd);
|
||
liveCursor = blockEnd + 1;
|
||
progressRuntime.resumeFromPage = liveCursor;
|
||
scheduleStatePersist();
|
||
}
|
||
} else if (!stopFetching && cachePlan && cachePlan.shiftedCoverage && Array.isArray(cachePlan.shiftedCoverage.threads) && cachePlan.shiftedCoverage.threads.length > 0) {
|
||
cacheStrategy = 'smart_incremental';
|
||
shiftedReuseEnd = Math.max(shiftedReuseEnd, Math.min(normalizedEnd, Number(cachePlan.shiftedCoverage.reusedEndPage) || refreshedFrontEnd));
|
||
await processCachedThreadBatch(
|
||
String(cachePlan.shiftedCoverage.reusedStartPage) + '-' + String(shiftedReuseEnd),
|
||
cachePlan.shiftedCoverage.threads,
|
||
searchContext,
|
||
'智能增量:复用当前板块历史缓存 ' + cachePlan.shiftedCoverage.reusedStartPage + '-' + shiftedReuseEnd + ' 页'
|
||
);
|
||
progressRuntime.resumeFromPage = shiftedReuseEnd + 1;
|
||
scheduleStatePersist();
|
||
liveCursor = shiftedReuseEnd + 1;
|
||
}
|
||
|
||
var liveTailStart = Math.max(normalizedStart, liveCursor, shiftedReuseEnd + 1);
|
||
if (!stopFetching && liveTailStart <= normalizedEnd) {
|
||
if (liveTailStart > normalizedStart) {
|
||
updateStatus('智能增量:补抓未覆盖页 ' + liveTailStart + '-' + normalizedEnd, 'loading');
|
||
}
|
||
await fetchLivePageRange(liveTailStart, normalizedEnd, searchContext);
|
||
}
|
||
|
||
if (!stopFetching) {
|
||
await saveCoverageSnapshot(
|
||
forumKey,
|
||
normalizedStart,
|
||
normalizedEnd,
|
||
coverageMapToList(searchContext.coverageThreadMap),
|
||
frontRefreshPages,
|
||
cacheStrategy
|
||
);
|
||
}
|
||
}
|
||
|
||
progressRuntime.isRunning = false;
|
||
|
||
if (manualClearRequested) {
|
||
manualClearRequested = false;
|
||
clearMagnetList(true);
|
||
clearSessionBackupState();
|
||
resetProgressRuntimeToIdle();
|
||
updateStatus('已手动清空结果', 'done');
|
||
scheduleStatePersist();
|
||
return;
|
||
}
|
||
|
||
var keywordMsg = keyword ? ' (关键词:' + keyword + ' 匹配:' + searchContext.matchedThreads + '帖)' : '';
|
||
var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : '';
|
||
updateProgress(normalizedEnd - normalizedStart + 1, normalizedEnd - normalizedStart + 1, stopFetching ? '已停止' : '已完成');
|
||
if (stopFetching) {
|
||
progressRuntime.stoppedByUser = true;
|
||
updateStatus('已停止 - 找到' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'error');
|
||
} else {
|
||
progressRuntime.stoppedByUser = false;
|
||
progressRuntime.resumeFromPage = normalizedEnd + 1;
|
||
updateStatus('完成! 共' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'done');
|
||
if (keyword) {
|
||
saveSearchHistory(keyword);
|
||
}
|
||
playDoneTone();
|
||
showDoneNotification('磁力链接抓取完成', '共获取 ' + searchContext.allMagnets.size + ' 个磁力链接,用时 ' + Math.max(1, Math.round((Date.now() - startTime) / 1000)) + ' 秒');
|
||
}
|
||
|
||
scheduleStatePersist();
|
||
} finally {
|
||
if (ball) ball.style.display = 'flex';
|
||
isFetching = false;
|
||
}
|
||
}
|
||
|
||
function fetchAllMagnets() {
|
||
if ('Notification' in window && Notification.permission === 'default') {
|
||
Notification.requestPermission().catch(function() {});
|
||
}
|
||
var startPage = parseInt(document.getElementById('page-start').value, 10) || 1;
|
||
var endPage = parseInt(document.getElementById('page-end').value, 10) || 1;
|
||
if (endPage < startPage) {
|
||
var temp = startPage;
|
||
startPage = endPage;
|
||
endPage = temp;
|
||
}
|
||
fetchFromPage(startPage, endPage);
|
||
}
|
||
|
||
function stopFetch() {
|
||
stopFetching = true;
|
||
progressRuntime.isRunning = false;
|
||
progressRuntime.stoppedByUser = true;
|
||
saveStateNow();
|
||
}
|
||
|
||
async function initializePluginUi() {
|
||
createFloatingPanel();
|
||
var panel = document.getElementById('magnet-floating-panel');
|
||
if (!panel) return;
|
||
|
||
var settingsArea = panel.querySelector('#magnet-settings');
|
||
if (!settingsArea) return;
|
||
|
||
await loadPrivateStateFromExtensionStorage();
|
||
|
||
renderCloudAuthSection();
|
||
|
||
if (isListPage() && !document.getElementById('keyword-input')) {
|
||
var currentPage = getCurrentPage();
|
||
|
||
var keywordDiv = document.createElement('div');
|
||
keywordDiv.className = 'magnet-control-row';
|
||
keywordDiv.innerHTML = '<input type="text" id="keyword-input" placeholder="关键词(逗号分隔多关键词)" style="width:100%;padding:11px 14px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;font-size:13px;box-sizing:border-box;background:#0f1419;color:#f0f4f8;box-shadow:inset 0 1px 2px rgba(0,0,0,0.2)">';
|
||
var keywordInputEl = keywordDiv.querySelector('#keyword-input');
|
||
if (keywordInputEl) {
|
||
keywordInputEl.addEventListener('focus', function() {
|
||
showHistoryDropdown(keywordInputEl);
|
||
});
|
||
keywordInputEl.addEventListener('input', function() {
|
||
removeHistoryDropdown();
|
||
});
|
||
}
|
||
|
||
var pageRange = document.createElement('div');
|
||
pageRange.className = 'magnet-control-row';
|
||
pageRange.style.cssText = 'font-size:12px;color:#8892a4;display:flex;align-items:center;gap:8px';
|
||
pageRange.innerHTML = '<span>页码范围</span><input type="number" id="page-start" value="' + currentPage + '" min="1" style="width:72px;padding:9px 10px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;text-align:center;font-size:13px;background:#0f1419;color:#f0f4f8"><span>到</span><input type="number" id="page-end" value="' + currentPage + '" min="1" style="width:72px;padding:9px 10px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;text-align:center;font-size:13px;background:#0f1419;color:#f0f4f8"><span>页</span><button id="page-range-all" type="button" style="padding:8px 12px;background:rgba(26,31,46,0.92);color:#00d4aa;border:1px solid rgba(0,212,170,0.3);border-radius:10px;cursor:pointer;font-size:12px;font-weight:700;">全页</button>';
|
||
|
||
var btnContainer = document.createElement('div');
|
||
btnContainer.className = 'magnet-control-row';
|
||
|
||
var btn = document.createElement('button');
|
||
btn.textContent = '开始';
|
||
btn.style.cssText = 'flex:1;padding:11px 14px;background:linear-gradient(135deg,#00d4aa,#00f5c4);color:#0a0e14;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 10px 20px rgba(0,212,170,.3)';
|
||
btn.onclick = fetchAllMagnets;
|
||
|
||
var stopBtn = document.createElement('button');
|
||
stopBtn.textContent = '停止';
|
||
stopBtn.style.cssText = 'padding:11px 14px;background:linear-gradient(135deg,#ef4444,#f87171);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700';
|
||
stopBtn.onclick = stopFetch;
|
||
|
||
var clearBtn = document.createElement('button');
|
||
clearBtn.textContent = '清结果';
|
||
clearBtn.style.cssText = 'padding:11px 14px;background:rgba(26,31,46,0.92);color:#8892a4;border:1px solid rgba(255,255,255,0.06);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700';
|
||
clearBtn.onclick = clearAllResults;
|
||
|
||
btnContainer.appendChild(btn);
|
||
btnContainer.appendChild(stopBtn);
|
||
btnContainer.appendChild(clearBtn);
|
||
|
||
var speedDiv = document.createElement('div');
|
||
speedDiv.className = 'magnet-control-row';
|
||
speedDiv.style.cssText = 'font-size:12px;color:#8892a4;display:flex;align-items:center;gap:8px';
|
||
speedDiv.innerHTML = '<span>抓取速度</span><select id="speed-select" style="flex:1;padding:10px 12px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;font-size:13px;background:#0f1419;color:#f0f4f8"><option value="slow">慢</option><option value="medium">中</option><option value="fast" selected>快</option><option value="ultrafast">超快</option></select>';
|
||
|
||
settingsArea.appendChild(keywordDiv);
|
||
settingsArea.appendChild(pageRange);
|
||
settingsArea.appendChild(btnContainer);
|
||
settingsArea.appendChild(speedDiv);
|
||
|
||
var pageStartEl = pageRange.querySelector('#page-start');
|
||
var pageEndEl = pageRange.querySelector('#page-end');
|
||
var pageRangeAllBtn = pageRange.querySelector('#page-range-all');
|
||
if (pageRangeAllBtn && pageStartEl && pageEndEl) {
|
||
pageRangeAllBtn.addEventListener('click', function() {
|
||
var lastPage = getLastPage();
|
||
pageStartEl.value = '1';
|
||
pageEndEl.value = String(lastPage);
|
||
updateStatus('已选择全页范围:1-' + lastPage, 'done');
|
||
});
|
||
}
|
||
|
||
var speedSelectEl = speedDiv.querySelector('#speed-select');
|
||
if (speedSelectEl) {
|
||
speedSelectEl.addEventListener('change', function() {
|
||
speedMode = speedConfig[speedSelectEl.value] ? speedSelectEl.value : 'fast';
|
||
scheduleCloudVaultSync();
|
||
});
|
||
}
|
||
}
|
||
|
||
if (!document.getElementById('magnet-status')) {
|
||
var footer = panel.querySelector('.magnet-panel-footer');
|
||
if (footer) {
|
||
var statusText = document.createElement('div');
|
||
statusText.id = 'magnet-status';
|
||
statusText.style.cssText = 'margin-bottom:8px;padding:10px 12px;background:rgba(26,31,46,0.92);border:1px solid rgba(0,212,170,0.3);border-radius:12px;font-size:12px;color:#8892a4';
|
||
footer.insertBefore(statusText, footer.firstChild);
|
||
}
|
||
}
|
||
|
||
var restoreResult = await restoreProgressState();
|
||
var restored = !!(restoreResult && restoreResult.restored);
|
||
var restoredState = restoreResult ? restoreResult.state : null;
|
||
await refreshCloudStatusAndVault({ restoreProgress: !restored });
|
||
if (isListPage()) {
|
||
var resumePage = Number.isFinite(Number(restoredState && restoredState.resumeFromPage))
|
||
? Math.max(1, Number(restoredState.resumeFromPage))
|
||
: Math.max(1, Number(restoredState && restoredState.startPage) || 1);
|
||
var resumeEndPage = Number.isFinite(Number(restoredState && restoredState.endPage))
|
||
? Math.max(1, Number(restoredState.endPage))
|
||
: resumePage;
|
||
|
||
var statusType = typeof (restoredState && restoredState.statusType) === 'string'
|
||
? restoredState.statusType
|
||
: '';
|
||
var statusText = typeof (restoredState && restoredState.statusText) === 'string'
|
||
? restoredState.statusText
|
||
: '';
|
||
|
||
var legacyRunningHint = statusType === 'loading' || /正在|开始|第\d+\/\d+页/.test(statusText);
|
||
|
||
var shouldAutoResume = !!(
|
||
restoredState &&
|
||
(restoredState.isRunning || legacyRunningHint) &&
|
||
!restoredState.stoppedByUser &&
|
||
resumePage <= resumeEndPage
|
||
);
|
||
if (shouldAutoResume && !isFetching) {
|
||
|
||
updateStatus('检测到未完成任务,正在自动继续...', 'loading');
|
||
fetchFromPage(resumePage, resumeEndPage, {
|
||
preserveExisting: true,
|
||
keyword: typeof restoredState.keyword === 'string' ? restoredState.keyword : '',
|
||
speedMode: typeof restoredState.speedMode === 'string' ? restoredState.speedMode : 'fast'
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (!restored) {
|
||
updateStatus('输入页码范围获取磁力', '');
|
||
}
|
||
return;
|
||
}
|
||
|
||
var pageTitle = document.title || '当前帖子';
|
||
var magnets = extractMagnets();
|
||
if (magnets.length > 0) {
|
||
magnets.forEach(function(m) { addMagnetItem(pageTitle, m); });
|
||
updateStatus('找到 ' + magnets.length + ' 个磁力', 'done');
|
||
} else if (!restored) {
|
||
updateStatus('未找到磁力链接', 'error');
|
||
}
|
||
}
|
||
|
||
var currentUrl = window.location.hostname;
|
||
if (currentUrl.includes('sehuatang')) {
|
||
initializePluginUi().catch(function(e) {
|
||
log('初始化失败: ' + e);
|
||
});
|
||
|
||
setInterval(function() {
|
||
var panelExists = !!document.getElementById('magnet-floating-panel');
|
||
var ballExists = !!document.getElementById('magnet-float-ball');
|
||
if (panelExists && ballExists) {
|
||
return;
|
||
}
|
||
|
||
hasRestoredProgress = false;
|
||
initializePluginUi().catch(function(e) {
|
||
log('自动恢复UI失败: ' + e);
|
||
});
|
||
}, 2500);
|
||
}
|
||
|
||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||
if (request.action === 'getMagnets') {
|
||
var magnets = extractMagnets();
|
||
sendResponse({
|
||
magnets: magnets,
|
||
found: magnets.length > 0,
|
||
count: magnets.length
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (request.action === 'copyMagnet') {
|
||
var magnetsToCopy = extractMagnets();
|
||
if (magnetsToCopy.length === 0) {
|
||
sendResponse({ found: false, count: 0 });
|
||
return;
|
||
}
|
||
|
||
navigator.clipboard.writeText(magnetsToCopy.join('\n'))
|
||
.then(function() {
|
||
sendResponse({ found: true, count: magnetsToCopy.length });
|
||
})
|
||
.catch(function(err) {
|
||
var errorMsg = err && err.message ? err.message : '复制失败';
|
||
sendResponse({ found: false, count: magnetsToCopy.length, error: errorMsg });
|
||
});
|
||
|
||
return true;
|
||
}
|
||
});
|
||
})();
|