## 新功能 1. 进度条 - 可视化抓取进度 2. 收藏夹 - 持久收藏磁力链接(♡按钮) 3. 历史记录 - 关键词搜索历史(点击输入框显示) 4. 通知提醒 - 完成时提示音 + 浏览器通知 ## 技术实现 - 使用 localStorage 存储收藏和历史 - 使用 Web Audio API 播放提示音 - 使用 Notification API 显示浏览器通知 - CSS 进度条动画效果
2758 lines
100 KiB
JavaScript
2758 lines
100 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-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:3px;overflow:hidden;margin-top:8px;',
|
||
'}',
|
||
'.magnet-progress-bar{',
|
||
' height:100%;background:linear-gradient(90deg, var(--m-accent), #00f5c4);border-radius:3px;transition:width 0.3s ease;',
|
||
'}',
|
||
'.magnet-progress-text{',
|
||
' display:flex;justify-content:space-between;font-size:11px;color:var(--m-text-muted);margin-top:4px;',
|
||
'}',
|
||
|
||
/* === 收藏按钮 === */',
|
||
'.magnet-favorite-btn{',
|
||
' padding:6px 10px;background:transparent;border:1px solid var(--m-border);border-radius:8px;cursor:pointer;font-size:11px;color:var(--m-text-muted);transition:all 0.2s ease;',
|
||
'}',
|
||
'.magnet-favorite-btn:hover{',
|
||
' border-color:var(--m-accent-secondary);color:var(--m-accent-secondary);',
|
||
'}',
|
||
'.magnet-favorite-btn.is-favorite{',
|
||
' background:rgba(167,139,250,0.15);border-color:var(--m-accent-secondary);color:var(--m-accent-secondary);',
|
||
'}',
|
||
|
||
/* === 收藏视图 === */',
|
||
'#magnet-favorites-view .magnet-favorite-item{',
|
||
' display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);margin-bottom:8px;',
|
||
'}',
|
||
'#magnet-favorites-view .magnet-favorite-item:hover{',
|
||
' border-color:var(--m-border-accent);',
|
||
'}',
|
||
'#magnet-favorites-view .magnet-favorite-title{',
|
||
' flex:1;font-size:12px;color:var(--m-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;',
|
||
'}',
|
||
'#magnet-favorites-view .magnet-favorite-actions{',
|
||
' display:flex;gap:6px;',
|
||
'}',
|
||
'#magnet-favorites-view .magnet-favorite-copy,',
|
||
'#magnet-favorites-view .magnet-favorite-remove{',
|
||
' padding:5px 8px;border:none;border-radius:6px;cursor:pointer;font-size:10px;font-weight:600;transition:all 0.2s ease;',
|
||
'}',
|
||
'#magnet-favorites-view .magnet-favorite-copy{',
|
||
' background:linear-gradient(135deg, var(--m-accent), #00f5c4);color:var(--m-bg-deep);',
|
||
'}',
|
||
'#magnet-favorites-view .magnet-favorite-remove{',
|
||
' background:rgba(239,68,68,0.15);color:var(--m-error);',
|
||
'}',
|
||
|
||
/* === 历史记录下拉 === */',
|
||
'.magnet-history-dropdown{',
|
||
' position:absolute;top:100%;left:0;right:0;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:100;max-height:200px;overflow-y:auto;',
|
||
'}',
|
||
'.magnet-history-item{',
|
||
' padding:10px 14px;cursor:pointer;font-size:12px;color:var(--m-text-primary);transition:background 0.2s ease;',
|
||
'}',
|
||
'.magnet-history-item:hover{',
|
||
' background:var(--m-bg-secondary);',
|
||
'}',
|
||
'.magnet-history-item:first-child{',
|
||
' border-radius:var(--m-radius-md) var(--m-radius-md) 0 0;',
|
||
'}',
|
||
'.magnet-history-item:last-child{',
|
||
' border-radius:0 0 var(--m-radius-md) var(--m-radius-md);',
|
||
'}',
|
||
'.magnet-history-clear{',
|
||
' padding:10px 14px;border-top:1px solid var(--m-border);font-size:11px;color:var(--m-error);cursor:pointer;text-align:center;',
|
||
'}'
|
||
].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 resultsBtn = document.getElementById('magnet-view-results');
|
||
var cacheBtn = document.getElementById('magnet-view-cache');
|
||
var favoritesBtn = document.getElementById('magnet-view-favorites');
|
||
|
||
if (!resultsView || !cacheView || !favoritesView || !resultsBtn || !cacheBtn || !favoritesBtn) {
|
||
return;
|
||
}
|
||
|
||
resultsView.classList.toggle('is-active', viewName === 'results');
|
||
cacheView.classList.toggle('is-active', viewName === 'cache');
|
||
favoritesView.classList.toggle('is-active', viewName === 'favorites');
|
||
resultsBtn.classList.toggle('is-active', viewName === 'results');
|
||
cacheBtn.classList.toggle('is-active', viewName === 'cache');
|
||
favoritesBtn.classList.toggle('is-active', viewName === 'favorites');
|
||
|
||
if (viewName === 'favorites') {
|
||
renderFavoritesList();
|
||
}
|
||
}
|
||
var resultsView = document.getElementById('magnet-results-view');
|
||
var cacheView = document.getElementById('magnet-cache-view');
|
||
var resultsBtn = document.getElementById('magnet-view-results');
|
||
var cacheBtn = document.getElementById('magnet-view-cache');
|
||
|
||
if (!resultsView || !cacheView || !resultsBtn || !cacheBtn) {
|
||
return;
|
||
}
|
||
|
||
var showCache = viewName === 'cache';
|
||
resultsView.classList.toggle('is-active', !showCache);
|
||
cacheView.classList.toggle('is-active', showCache);
|
||
resultsBtn.classList.toggle('is-active', !showCache);
|
||
cacheBtn.classList.toggle('is-active', showCache);
|
||
}
|
||
|
||
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 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><div class="magnet-panel-footer"><div id="magnet-status">设置参数后开始抓取</div><div class="magnet-progress-container"><div class="magnet-progress-bar" id="magnet-progress-bar" style="width:0%"></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);
|
||
|
||
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 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 closeBtn = panel.querySelector('.magnet-panel-close');
|
||
if (closeBtn) closeBtn.onclick = function() {
|
||
panel.style.display = 'none';
|
||
ball.style.display = 'flex';
|
||
};
|
||
var favoritesSwitch = panel.querySelector('#magnet-view-favorites');
|
||
if (favoritesSwitch) {
|
||
favoritesSwitch.onclick = function() {
|
||
setPanelView('favorites');
|
||
};
|
||
}
|
||
|
||
var clearFavoritesBtn = panel.querySelector('#magnet-clear-favorites');
|
||
if (clearFavoritesBtn) {
|
||
clearFavoritesBtn.onclick = function() {
|
||
if (confirm('确定要清空所有收藏吗')) {
|
||
saveFavorites([]);
|
||
renderFavoritesList();
|
||
}
|
||
};
|
||
}
|
||
|
||
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;
|
||
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 isListPage() {
|
||
return document.querySelector('#threadlisttableid') !== null;
|
||
}
|
||
|
||
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 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');
|
||
|
||
if (progressBar) {
|
||
var percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
||
progressBar.style.width = percent + '%';
|
||
}
|
||
if (progressLabel) {
|
||
progressLabel.textContent = label || ('进度: ' + current + '/' + total);
|
||
}
|
||
if (progressPercent) {
|
||
var percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
||
progressPercent.textContent = percent + '%';
|
||
}
|
||
}
|
||
|
||
function resetProgress() {
|
||
updateProgress(0, 0, '等待开始');
|
||
}
|
||
|
||
// === 收藏夹功能 ===
|
||
var FAVORITES_KEY = 'magnet-favorites';
|
||
var favoritesCache = null;
|
||
|
||
function loadFavorites() {
|
||
if (favoritesCache !== null) {
|
||
return favoritesCache;
|
||
}
|
||
try {
|
||
var stored = localStorage.getItem(FAVORITES_KEY);
|
||
favoritesCache = stored ? JSON.parse(stored) : [];
|
||
} catch (e) {
|
||
favoritesCache = [];
|
||
}
|
||
return favoritesCache;
|
||
}
|
||
|
||
function saveFavorites(favorites) {
|
||
try {
|
||
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
|
||
favoritesCache = favorites;
|
||
} catch (e) {
|
||
log('保存收藏失败: ' + e);
|
||
}
|
||
}
|
||
|
||
function isFavorited(link) {
|
||
var favorites = loadFavorites();
|
||
return favorites.some(function(f) { return f.link === link; });
|
||
}
|
||
|
||
function toggleFavorite(title, link, btn) {
|
||
var favorites = loadFavorites();
|
||
var existingIndex = favorites.findIndex(function(f) { return f.link === link; });
|
||
|
||
if (existingIndex >= 0) {
|
||
favorites.splice(existingIndex, 1);
|
||
btn.classList.remove('is-favorite');
|
||
btn.innerHTML = '♡';
|
||
btn.title = '收藏';
|
||
} else {
|
||
favorites.push({
|
||
title: title,
|
||
link: link,
|
||
addedAt: Date.now()
|
||
});
|
||
btn.classList.add('is-favorite');
|
||
btn.innerHTML = '♥';
|
||
btn.title = '取消收藏';
|
||
}
|
||
|
||
saveFavorites(favorites);
|
||
}
|
||
|
||
function renderFavoritesList() {
|
||
var list = document.getElementById('magnet-favorites-list');
|
||
if (!list) return;
|
||
|
||
var favorites = loadFavorites();
|
||
list.innerHTML = '';
|
||
|
||
if (favorites.length === 0) {
|
||
list.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(fav) {
|
||
var item = document.createElement('div');
|
||
item.className = 'magnet-favorite-item';
|
||
|
||
var titleEl = document.createElement('div');
|
||
titleEl.className = 'magnet-favorite-title';
|
||
titleEl.textContent = fav.title;
|
||
titleEl.title = fav.title;
|
||
|
||
var actionsEl = document.createElement('div');
|
||
actionsEl.className = 'magnet-favorite-actions';
|
||
|
||
var copyBtn = document.createElement('button');
|
||
copyBtn.className = 'magnet-favorite-copy';
|
||
copyBtn.textContent = '复制';
|
||
copyBtn.onclick = function() {
|
||
navigator.clipboard.writeText(fav.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() {
|
||
var favorites = loadFavorites();
|
||
var idx = favorites.findIndex(function(f) { return f.link === fav.link; });
|
||
if (idx >= 0) {
|
||
favorites.splice(idx, 1);
|
||
saveFavorites(favorites);
|
||
renderFavoritesList();
|
||
}
|
||
};
|
||
|
||
actionsEl.appendChild(copyBtn);
|
||
actionsEl.appendChild(removeBtn);
|
||
item.appendChild(titleEl);
|
||
item.appendChild(actionsEl);
|
||
list.appendChild(item);
|
||
});
|
||
}
|
||
|
||
// === 历史记录功能 ===
|
||
var HISTORY_KEY = 'magnet-search-history';
|
||
var MAX_HISTORY = 20;
|
||
|
||
function loadSearchHistory() {
|
||
try {
|
||
var stored = localStorage.getItem(HISTORY_KEY);
|
||
return stored ? JSON.parse(stored) : [];
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function saveSearchHistory(keyword) {
|
||
if (!keyword || typeof keyword !== 'string' || !keyword.trim()) {
|
||
return;
|
||
}
|
||
keyword = keyword.trim();
|
||
var history = loadSearchHistory();
|
||
// 移除已存在的相同关键词
|
||
var idx = history.indexOf(keyword);
|
||
if (idx >= 0) {
|
||
history.splice(idx, 1);
|
||
}
|
||
// 添加到开头
|
||
history.unshift(keyword);
|
||
// 限制数量
|
||
if (history.length > MAX_HISTORY) {
|
||
history = history.slice(0, MAX_HISTORY);
|
||
}
|
||
try {
|
||
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
|
||
} catch (e) {
|
||
log('保存历史记录失败: ' + e);
|
||
}
|
||
}
|
||
|
||
function showHistoryDropdown(input) {
|
||
var history = loadSearchHistory();
|
||
if (history.length === 0) {
|
||
return;
|
||
}
|
||
|
||
// 移除已存在的下拉框
|
||
var existing = document.querySelector('.magnet-history-dropdown');
|
||
if (existing) existing.remove();
|
||
|
||
var dropdown = document.createElement('div');
|
||
dropdown.className = 'magnet-history-dropdown';
|
||
|
||
history.forEach(function(kw) {
|
||
var item = document.createElement('div');
|
||
item.className = 'magnet-history-item';
|
||
item.textContent = kw;
|
||
item.onclick = function() {
|
||
input.value = kw;
|
||
dropdown.remove();
|
||
input.focus();
|
||
};
|
||
dropdown.appendChild(item);
|
||
});
|
||
|
||
var clearItem = document.createElement('div');
|
||
clearItem.className = 'magnet-history-clear';
|
||
clearItem.textContent = '清空历史';
|
||
clearItem.onclick = function() {
|
||
localStorage.removeItem(HISTORY_KEY);
|
||
dropdown.remove();
|
||
};
|
||
dropdown.appendChild(clearItem);
|
||
|
||
input.parentNode.style.position = 'relative';
|
||
input.parentNode.appendChild(dropdown);
|
||
|
||
// 点击外部关闭
|
||
setTimeout(function() {
|
||
document.addEventListener('click', function closeDropdown(e) {
|
||
if (!dropdown.contains(e.target)) {
|
||
dropdown.remove();
|
||
document.removeEventListener('click', closeDropdown);
|
||
}
|
||
});
|
||
}, 100);
|
||
}
|
||
|
||
// === 通知功能 ===
|
||
function playNotificationSound() {
|
||
try {
|
||
var audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
var oscillator = audioContext.createOscillator();
|
||
var gainNode = audioContext.createGain();
|
||
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(audioContext.destination);
|
||
|
||
oscillator.frequency.value = 800;
|
||
oscillator.type = 'sine';
|
||
|
||
gainNode.gain.setValue(0.3);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||
|
||
oscillator.start(audioContext.currentTime);
|
||
oscillator.stop(audioContext.currentTime + 0.3);
|
||
} catch (e) {
|
||
log('播放提示音失败: ' + e);
|
||
}
|
||
}
|
||
|
||
function showBrowserNotification(title, body) {
|
||
if (!('Notification' in window)) {
|
||
return;
|
||
}
|
||
|
||
if (Notification.permission === 'granted') {
|
||
new Notification(title, { body: body, icon: chrome.runtime ? chrome.runtime.getURL('icon.png') : undefined });
|
||
} else if (Notification.permission !== 'denied') {
|
||
Notification.requestPermission().then(function(permission) {
|
||
if (permission === 'granted') {
|
||
new Notification(title, { body: body, icon: chrome.runtime ? chrome.runtime.getURL('icon.png') : undefined });
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function notifyComplete(count, duration) {
|
||
playNotificationSound();
|
||
var durationText = '';
|
||
if (duration && duration > 0) {
|
||
var seconds = Math.floor(duration / 1000);
|
||
if (seconds >= 60) {
|
||
durationText = ',耗时 ' + Math.floor(seconds / 60) + ' 分 ' + (seconds % 60) + ' 秒';
|
||
} else {
|
||
durationText = ',耗时 ' + seconds + ' 秒';
|
||
}
|
||
}
|
||
showBrowserNotification('磁力链接抓取完成', '共获取 ' + count + ' 个磁力链接' + durationText);
|
||
}
|
||
var countEl = document.getElementById('magnet-count-num');
|
||
if (countEl) countEl.textContent = count;
|
||
}
|
||
|
||
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 btnContainer = document.createElement('div');
|
||
btnContainer.style.cssText = 'display:flex;gap:6px;flex-shrink:0;';
|
||
|
||
var copyBtn = document.createElement('button');
|
||
copyBtn.className = 'magnet-copy-btn';
|
||
copyBtn.setAttribute('data-magnet', safeLink);
|
||
copyBtn.textContent = '复制';
|
||
|
||
// 收藏按钮
|
||
var favoriteBtn = document.createElement('button');
|
||
favoriteBtn.className = 'magnet-favorite-btn';
|
||
favoriteBtn.innerHTML = '♡';
|
||
favoriteBtn.title = '收藏';
|
||
favoriteBtn.onclick = function() {
|
||
toggleFavorite(safeTitle, safeLink, favoriteBtn);
|
||
};
|
||
|
||
// 检查是否已收藏
|
||
isFavorite(safeLink).then(function(isFav) {
|
||
if (isFav) {
|
||
favoriteBtn.classList.add('is-favorite');
|
||
favoriteBtn.title = '取消收藏';
|
||
}
|
||
});
|
||
|
||
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');
|
||
});
|
||
};
|
||
|
||
item.appendChild(titleEl);
|
||
btnContainer.appendChild(favoriteBtn);
|
||
btnContainer.appendChild(copyBtn);
|
||
item.appendChild(btnContainer);
|
||
|
||
list.appendChild(item);
|
||
setPanelView('results');
|
||
updateCount(list.children.length);
|
||
|
||
if (!options || !options.skipPersist) {
|
||
scheduleStatePersist();
|
||
}
|
||
}
|
||
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 copyBtn = document.createElement('button');
|
||
copyBtn.className = 'magnet-copy-btn';
|
||
copyBtn.setAttribute('data-magnet', safeLink);
|
||
copyBtn.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');
|
||
});
|
||
};
|
||
|
||
item.appendChild(titleEl);
|
||
item.appendChild(copyBtn);
|
||
|
||
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 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 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() {
|
||
try {
|
||
sessionStorage.removeItem(STATE_BACKUP_KEY);
|
||
} catch (e) {
|
||
log('清理会话备份失败: ' + e);
|
||
}
|
||
}
|
||
|
||
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) {
|
||
try {
|
||
sessionStorage.setItem(STATE_BACKUP_KEY, JSON.stringify(snapshot));
|
||
} catch (e) {
|
||
log('保存会话备份失败: ' + e);
|
||
}
|
||
}
|
||
|
||
function readStateFromSessionBackup() {
|
||
try {
|
||
var raw = sessionStorage.getItem(STATE_BACKUP_KEY);
|
||
if (!raw) return null;
|
||
return JSON.parse(raw);
|
||
} catch (e) {
|
||
log('读取会话备份失败: ' + e);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
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;
|
||
var cachePayload = [];
|
||
|
||
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;
|
||
}
|
||
|
||
updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading');
|
||
|
||
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) {
|
||
cachePayload.push({
|
||
threadKey: threadKey,
|
||
url: threadUrl,
|
||
title: threadTitle,
|
||
magnets: threadMagnets
|
||
});
|
||
}
|
||
|
||
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);
|
||
|
||
if (options.forumKey && cachePayload.length > 0) {
|
||
await saveThreadMagnetsToCache(options.forumKey, cachePayload);
|
||
}
|
||
|
||
return fetchedCount;
|
||
}
|
||
|
||
async function processCachedThreadBatch(pageLabel, threads, context, statusText) {
|
||
var normalizedThreads = normalizeCachedThreads(threads);
|
||
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 }
|
||
);
|
||
}
|
||
|
||
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');
|
||
|
||
// 更新进度条
|
||
var totalPages = context.normalizedEnd - (context.startPage || startPage) + 1;
|
||
var currentPage = page - (context.startPage || startPage) + 1;
|
||
updateProgress(currentPage, totalPages, '第' + 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 startTime = Date.now();
|
||
stopFetching = false;
|
||
progressRuntime.isRunning = true;
|
||
progressRuntime.stoppedByUser = false;
|
||
progressRuntime.startPage = earlyStart;
|
||
progressRuntime.endPage = earlyEnd;
|
||
progressRuntime.resumeFromPage = earlyStart;
|
||
|
||
var panel = createFloatingPanel();
|
||
var ball = document.getElementById('magnet-float-ball');
|
||
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 frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd);
|
||
var searchContext = {
|
||
forumKey: forumKey,
|
||
baseUrl: baseUrl,
|
||
normalizedEnd: normalizedEnd,
|
||
keywords: buildKeywordList(keyword),
|
||
coverageThreadMap: Object.create(null),
|
||
processedThreadKeys: Object.create(null),
|
||
allMagnets: new Set(allMagnetLinks),
|
||
matchedThreads: 0,
|
||
totalFetched: 0,
|
||
failedPages: 0
|
||
};
|
||
var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages);
|
||
var cacheStrategy = 'full_live';
|
||
|
||
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();
|
||
} else {
|
||
var refreshedFrontEnd = normalizedStart - 1;
|
||
if (frontRefreshPages > 0) {
|
||
refreshedFrontEnd = normalizedStart + frontRefreshPages - 1;
|
||
updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading');
|
||
await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext);
|
||
}
|
||
|
||
var shiftedReuseEnd = refreshedFrontEnd;
|
||
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();
|
||
}
|
||
|
||
var liveTailStart = Math.max(normalizedStart, 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 : '';
|
||
|
||
// 更新进度条为100%
|
||
updateProgress(normalizedEnd - normalizedStart + 1, normalizedEnd - normalizedStart + 1, '已完成');
|
||
|
||
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');
|
||
|
||
// 发送完成通知
|
||
var duration = Date.now() - startTime;
|
||
notifyComplete(searchContext.allMagnets.size, duration);
|
||
|
||
// 保存搜索历史
|
||
if (keyword) {
|
||
saveSearchHistory(keyword);
|
||
}
|
||
}
|
||
var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : '';
|
||
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');
|
||
}
|
||
|
||
scheduleStatePersist();
|
||
} finally {
|
||
if (ball) ball.style.display = 'flex';
|
||
isFetching = false;
|
||
}
|
||
}
|
||
|
||
function fetchAllMagnets() {
|
||
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;
|
||
scheduleStatePersist();
|
||
}
|
||
|
||
async function initializePluginUi() {
|
||
createFloatingPanel();
|
||
var panel = document.getElementById('magnet-floating-panel');
|
||
if (!panel) return;
|
||
|
||
var settingsArea = panel.querySelector('#magnet-settings');
|
||
if (!settingsArea) return;
|
||
|
||
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 keywordInput = keywordDiv.querySelector('#keyword-input');
|
||
if (keywordInput) {
|
||
keywordInput.addEventListener('focus', function() {
|
||
showHistoryDropdown(keywordInput);
|
||
});
|
||
}
|
||
|
||
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>';
|
||
|
||
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);
|
||
}
|
||
|
||
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;
|
||
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;
|
||
}
|
||
});
|
||
})();
|