diff --git a/content.js b/content.js
index fa2de5f..3007c58 100644
--- a/content.js
+++ b/content.js
@@ -22,13 +22,11 @@
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 变量 === */',
- '@import url("https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap");',
-
- ':root {'
+ /* === CSS 变量 === */
+ ':root {',
' --m-bg-deep: #0a0e14;',
' --m-bg-primary: #0f1419;',
' --m-bg-secondary: #1a1f2e;',
@@ -57,8 +55,8 @@
' --m-shadow-glow: 0 0 20px var(--m-accent-glow), 0 0 40px rgba(0, 212, 170, 0.15);',
'}',
- '/* === 悬浮球 === */',
- '#magnet-float-ball{'
+ /* === 悬浮球 === */
+ '#magnet-float-ball{',
' position:fixed;bottom:24px;right:24px;',
' width:60px;height:60px;',
' background:linear-gradient(135deg, #0f1419 0%, #1a1f2e 100%);',
@@ -75,15 +73,20 @@
' 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{'
+ /* === 脉冲动画 === */
+ '@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{'
+ /* === 主面板 === */
+ '#magnet-floating-panel{',
' position:fixed;right:20px;bottom:20px;',
' width:min(800px, calc(100vw - 40px));',
' height:min(85vh, 860px);',
@@ -100,9 +103,13 @@
' 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{'
+ /* === 面板头部 === */
+ '#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%);',
@@ -128,8 +135,8 @@
' display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;',
'}',
- '/* === 切换按钮 === */',
- '#magnet-floating-panel .magnet-panel-switch{'
+ /* === 切换按钮 === */
+ '#magnet-floating-panel .magnet-panel-switch{',
' padding:10px 18px;',
' border:1px solid var(--m-border);',
' border-radius:var(--m-radius-lg);',
@@ -153,8 +160,8 @@
' box-shadow:0 0 15px rgba(0, 212, 170, 0.2);',
'}',
- '/* === 关闭按钮 === */',
- '#magnet-floating-panel .magnet-panel-close{'
+ /* === 关闭按钮 === */
+ '#magnet-floating-panel .magnet-panel-close{',
' width:36px;height:36px;',
' border:1px solid var(--m-border);',
' border-radius:var(--m-radius-md);',
@@ -170,8 +177,8 @@
' color:var(--m-error);',
'}',
- '/* === 设置区域 === */',
- '#magnet-settings{'
+ /* === 设置区域 === */
+ '#magnet-settings{',
' padding:16px 20px;',
' background:var(--m-bg-secondary);',
' border-bottom:1px solid var(--m-border);',
@@ -182,8 +189,8 @@
'}',
'#magnet-floating-panel .magnet-control-row > *{min-width:0;}',
- '/* === 输入框样式 === */',
- '#magnet-settings input[type="text"],'
+ /* === 输入框样式 === */
+ '#magnet-settings input[type="text"],',
'#magnet-settings input[type="number"],',
'#magnet-settings select{',
' padding:10px 14px;',
@@ -203,8 +210,8 @@
'}',
'#magnet-settings input::placeholder{color:var(--m-text-muted);}',
- '/* === 主按钮 === */',
- '#magnet-settings button:not(.magnet-panel-switch){'
+ /* === 主按钮 === */
+ '#magnet-settings button:not(.magnet-panel-switch){',
' padding:10px 20px;',
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);',
' border:none;',
@@ -224,8 +231,8 @@
' transform:translateY(0);',
'}',
- '/* === 内容区域 === */',
- '#magnet-floating-panel .magnet-panel-content{'
+ /* === 内容区域 === */
+ '#magnet-floating-panel .magnet-panel-content{',
' flex:1;min-height:0;',
' padding:16px 20px;',
' background:var(--m-bg-primary);',
@@ -236,8 +243,8 @@
'}',
'#magnet-floating-panel .magnet-view.is-active{display:flex;}',
- '/* === 视图工具栏 === */',
- '#magnet-floating-panel .magnet-view-toolbar{'
+ /* === 视图工具栏 === */
+ '#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);',
@@ -257,8 +264,8 @@
' font-size:12px;color:var(--m-text-secondary);',
'}',
- '/* === 磁力列表 === */',
- '#magnet-list{'
+ /* === 磁力列表 === */
+ '#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);',
@@ -267,8 +274,8 @@
'#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{'
+ /* === 列表项 === */
+ '.magnet-item{',
' display:flex;align-items:flex-start;gap:14px;',
' padding:14px 16px;',
' background:var(--m-bg-card);',
@@ -296,8 +303,8 @@
'}',
'.magnet-title:hover{color:var(--m-accent);}',
- '/* === 复制按钮 === */',
- '.magnet-copy-btn{'
+ /* === 复制按钮 === */
+ '.magnet-copy-btn{',
' padding:8px 16px;',
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);',
' color:var(--m-bg-deep);',
@@ -313,9 +320,25 @@
' box-shadow:0 4px 15px rgba(0, 212, 170, 0.4);',
'}',
'.magnet-copy-btn:active{transform:scale(0.98);}',
+ '.magnet-download-btn{',
+ ' padding:8px 16px;',
+ ' background:linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);',
+ ' color:#fff;',
+ ' border:none;border-radius:var(--m-radius-md);',
+ ' cursor:pointer;',
+ ' font-family:var(--m-font-display);',
+ ' font-size:12px;font-weight:700;',
+ ' white-space:nowrap;flex-shrink:0;',
+ ' transition:all 0.2s ease;',
+ '}',
+ '.magnet-download-btn:hover{',
+ ' transform:scale(1.05);',
+ ' box-shadow:0 4px 15px rgba(37, 99, 235, 0.35);',
+ '}',
+ '.magnet-download-btn:active{transform:scale(0.98);}',
- '/* === 缓存面板 === */',
- '#magnet-cache-panel{'
+ /* === 缓存面板 === */
+ '#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);',
'}',
@@ -323,8 +346,8 @@
'#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{'
+ /* === 缓存网格 === */
+ '.magnet-cache-grid{',
' display:grid;',
' grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));',
' gap:12px;margin-bottom:16px;',
@@ -349,8 +372,8 @@
' color:var(--m-accent);',
'}',
- '/* === 缓存区块 === */',
- '.magnet-cache-section{margin-top:16px;}'
+ /* === 缓存区块 === */
+ '.magnet-cache-section{margin-top:16px;}',
'.magnet-cache-section-title{',
' font-family:var(--m-font-display);',
' font-size:13px;font-weight:700;',
@@ -377,16 +400,16 @@
' margin-top:6px;line-height:1.5;',
'}',
- '/* === 底部 === */',
- '#magnet-floating-panel .magnet-panel-footer{'
+ /* === 底部 === */
+ '#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{'
+ /* === 状态栏 === */
+ '#magnet-status{',
' padding:12px 16px;border-radius:var(--m-radius-md);',
' font-size:12px;line-height:1.6;',
' background:var(--m-bg-card);',
@@ -409,8 +432,8 @@
' background:rgba(16, 185, 129, 0.1);',
'}',
- '/* === 一键复制按钮 === */',
- '#magnet-copy-all{'
+ /* === 一键复制按钮 === */
+ '#magnet-copy-all{',
' width:100%;padding:14px 20px;',
' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);',
' color:var(--m-bg-deep);',
@@ -427,8 +450,8 @@
'}',
'#magnet-copy-all:active{transform:translateY(0);}',
- '/* === 调试菜单 === */',
- '#magnet-debug-menu{'
+ /* === 调试菜单 === */
+ '#magnet-debug-menu{',
' background:var(--m-bg-card) !important;',
' border:1px solid var(--m-border-accent) !important;',
' border-radius:var(--m-radius-md) !important;',
@@ -441,8 +464,8 @@
' accent-color:var(--m-accent);',
'}',
- '/* === 响应式 === */',
- '@media (max-width: 900px){'
+ /* === 响应式 === */
+ '@media (max-width: 900px){',
' #magnet-floating-panel{',
' right:10px;bottom:10px;',
' width:calc(100vw - 20px);',
@@ -454,7 +477,7 @@
' #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;',
@@ -467,71 +490,96 @@
' 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;',
+ ' width:100%;height:6px;background:var(--m-bg-secondary);border-radius:999px;overflow:hidden;',
'}',
'.magnet-progress-bar{',
- ' height:100%;background:linear-gradient(90deg, var(--m-accent), #00f5c4);border-radius:3px;transition:width 0.3s ease;',
+ ' width:0%;height:100%;background:linear-gradient(90deg, var(--m-accent), #00f5c4);transition:width 0.25s ease;',
'}',
'.magnet-progress-text{',
- ' display:flex;justify-content:space-between;font-size:11px;color:var(--m-text-muted);margin-top:4px;',
+ ' display:flex;justify-content:space-between;gap:12px;font-size:11px;color:var(--m-text-muted);',
'}',
- '/* === 收藏按钮 === */',
+ /* === 结果项操作 === */
+ '.magnet-item-actions{',
+ ' display:flex;gap:6px;flex-shrink:0;',
+ '}',
'.magnet-favorite-btn{',
- ' 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;',
+ ' width:36px;height:36px;flex:0 0 36px;padding:0;display:inline-flex;align-items:center;justify-content:center;line-height:1;background:transparent;border:1px solid var(--m-border);border-radius:var(--m-radius-md);cursor:pointer;font-size:16px;color:var(--m-text-muted);transition:all 0.2s ease;',
'}',
'.magnet-favorite-btn:hover{',
- ' border-color:var(--m-accent-secondary);color:var(--m-accent-secondary);',
+ ' border-color:rgba(239,68,68,0.5);color:#ff7b8a;background:rgba(239,68,68,0.08);',
'}',
'.magnet-favorite-btn.is-favorite{',
- ' background:rgba(167,139,250,0.15);border-color:var(--m-accent-secondary);color:var(--m-accent-secondary);',
+ ' color:#ff4d6d;border-color:rgba(255,77,109,0.55);background:rgba(255,77,109,0.12);box-shadow:0 0 12px rgba(255,77,109,0.18);',
'}',
- '/* === 收藏视图 === */',
- '#magnet-favorites-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-list{',
+ ' flex:1;min-height:0;overflow-y:auto;padding-right:6px;',
+ ' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);',
'}',
- '#magnet-favorites-view .magnet-favorite-item:hover{',
+ '#magnet-favorites-list::-webkit-scrollbar{width:6px;}',
+ '#magnet-favorites-list::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}',
+ '#magnet-favorites-list::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}',
+ '.magnet-favorite-item{',
+ ' display:flex;align-items:center;gap:10px;padding:12px 14px;margin-bottom:8px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);',
+ '}',
+ '.magnet-favorite-item:hover{',
' border-color:var(--m-border-accent);',
'}',
- '#magnet-favorites-view .magnet-favorite-title{',
- ' flex:1;font-size:12px;color:var(--m-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;',
+ '.magnet-favorite-title{',
+ ' flex:1;min-width:0;font-size:12px;color:var(--m-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;',
'}',
- '#magnet-favorites-view .magnet-favorite-actions{',
- ' display:flex;gap:6px;',
+ '.magnet-favorite-actions{',
+ ' display:flex;gap:6px;flex-shrink:0;',
'}',
- '#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-favorite-copy,.magnet-favorite-remove{',
+ ' padding:6px 10px;border:none;border-radius:8px;cursor:pointer;font-size:11px;font-weight:700;',
'}',
- '#magnet-favorites-view .magnet-favorite-copy{',
+ '.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-favorite-remove{',
+ ' background:rgba(239,68,68,0.14);color:var(--m-error);',
'}',
- '/* === 搜索记录下拉 === */',
+ /* === 历史记录下拉 === */
+ '.magnet-keyword-wrap{position:relative;}',
'.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;',
+ ' position:absolute;left:0;right:0;top:calc(100% + 6px);background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);box-shadow:var(--m-shadow-lg);z-index:2147483647;overflow:hidden;',
+ ' max-height:260px;display:flex;flex-direction:column;',
'}',
+ '.magnet-history-list{overflow-y:auto;max-height:214px;}',
'.magnet-history-item{',
- ' padding:10px 14px;cursor:pointer;font-size:12px;color:var(--m-text-primary);transition:background 0.2s ease;',
+ ' display:flex;align-items:center;gap:8px;padding:10px 12px;font-size:12px;color:var(--m-text-primary);cursor:pointer;transition:background 0.2s ease;',
'}',
'.magnet-history-item:hover{',
' background:var(--m-bg-secondary);',
'}',
- '.magnet-history-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-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
+ '.magnet-history-delete{flex:0 0 auto;border:none;background:transparent;color:var(--m-text-muted);cursor:pointer;font-size:13px;line-height:1;padding:0 0 0 4px;font-weight:500;}',
+ '.magnet-history-delete:hover{color:var(--m-error);}',
'.magnet-history-clear{',
- ' padding:10px 14px;border-top:1px solid var(--m-border);font-size:11px;color:var(--m-error);cursor:pointer;text-align:center;',
- '}'
+ ' padding:10px 12px;border-top:1px solid var(--m-border);font-size:11px;color:var(--m-error);cursor:pointer;text-align:center;position:sticky;bottom:0;background:var(--m-bg-card);',
+ '}',
+
+ /* === 云同步 === */
+ '.magnet-cloud-status{display:inline-flex;align-items:center;gap:6px;margin-left:8px;}',
+ '.magnet-cloud-dot{width:8px;height:8px;border-radius:50%;background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,.35);flex:0 0 8px;}',
+ '.magnet-cloud-dot.is-ok{background:#10b981;box-shadow:0 0 8px rgba(16,185,129,.35);}',
+ '.magnet-cloud-label{font-size:11px;color:var(--m-text-secondary);}',
+ '.magnet-cloud-auth-card{padding:12px 14px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-lg);display:flex;flex-direction:column;gap:10px;}',
+ '.magnet-cloud-auth-title{font-size:12px;font-weight:700;color:var(--m-text-primary);}',
+ '.magnet-cloud-auth-meta{font-size:11px;color:var(--m-text-secondary);line-height:1.6;}',
+ '.magnet-cloud-auth-row{display:flex;gap:8px;flex-wrap:wrap;}',
+ '.magnet-cloud-auth-row input[type="password"],.magnet-cloud-auth-row input[type="text"]{flex:1;min-width:0;}',
+ '.magnet-cloud-secondary-btn{padding:10px 14px;background:rgba(26,31,46,0.92);color:var(--m-text-secondary);border:1px solid var(--m-border);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;}',
+ '.magnet-cloud-secondary-btn:hover{border-color:var(--m-border-accent);color:var(--m-text-primary);}',
+ '.magnet-cloud-danger-btn{padding:10px 14px;background:rgba(239,68,68,0.14);color:var(--m-error);border:1px solid rgba(239,68,68,0.25);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;}',
+ '.magnet-cloud-danger-btn:hover{background:rgba(239,68,68,0.22);}',
+ '#magnet-settings input[type="password"]{padding:10px 14px;background:var(--m-bg-primary);border:1px solid var(--m-border);border-radius:var(--m-radius-md);color:var(--m-text-primary);font-family:var(--m-font-body);font-size:13px;transition:all 0.2s ease;}'
].join('');
document.head.appendChild(style);
}
@@ -540,28 +588,37 @@
var resultsView = document.getElementById('magnet-results-view');
var cacheView = document.getElementById('magnet-cache-view');
var favoritesView = document.getElementById('magnet-favorites-view');
+ var cloudView = document.getElementById('magnet-cloud-view');
var resultsBtn = document.getElementById('magnet-view-results');
var cacheBtn = document.getElementById('magnet-view-cache');
var favoritesBtn = document.getElementById('magnet-view-favorites');
+ var cloudBtn = document.getElementById('magnet-view-cloud');
- if (!resultsView || !cacheView || !favoritesView || !resultsBtn || !cacheBtn || !favoritesBtn) {
+ if (!resultsView || !cacheView || !favoritesView || !cloudView || !resultsBtn || !cacheBtn || !favoritesBtn || !cloudBtn) {
return;
}
resultsView.classList.toggle('is-active', viewName === 'results');
cacheView.classList.toggle('is-active', viewName === 'cache');
favoritesView.classList.toggle('is-active', viewName === 'favorites');
+ cloudView.classList.toggle('is-active', viewName === 'cloud');
resultsBtn.classList.toggle('is-active', viewName === 'results');
cacheBtn.classList.toggle('is-active', viewName === 'cache');
favoritesBtn.classList.toggle('is-active', viewName === 'favorites');
+ cloudBtn.classList.toggle('is-active', viewName === 'cloud');
if (viewName === 'favorites') {
renderFavoritesList();
+ } else if (viewName === 'cloud') {
+ renderCloudAuthSection();
}
}
function createFloatingPanel() {
var existing = document.getElementById('magnet-floating-panel');
+ if (existing) return existing;
+
+ ensurePanelStyles();
var ball = document.createElement('div');
ball.id = 'magnet-float-ball';
@@ -590,9 +647,14 @@
var panel = document.createElement('div');
panel.id = 'magnet-floating-panel';
- panel.innerHTML = '
';
+ panel.innerHTML = '';
document.body.appendChild(panel);
+ var subtitle = panel.querySelector('.magnet-panel-subtitle');
+ if (subtitle) {
+ subtitle.innerHTML = '智能抓取 · 缓存加速 · 一键复制 云同步未登录';
+ }
+
setPanelView('results');
ball.onclick = function() {
@@ -615,6 +677,20 @@
};
}
+ var favoritesSwitch = panel.querySelector('#magnet-view-favorites');
+ if (favoritesSwitch) {
+ favoritesSwitch.onclick = function() {
+ setPanelView('favorites');
+ };
+ }
+
+ var cloudSwitch = panel.querySelector('#magnet-view-cloud');
+ if (cloudSwitch) {
+ cloudSwitch.onclick = function() {
+ setPanelView('cloud');
+ };
+ }
+
var refreshCacheBtn = panel.querySelector('#magnet-refresh-cache');
if (refreshCacheBtn) {
refreshCacheBtn.onclick = function() {
@@ -627,53 +703,23 @@
clearCacheInlineBtn.onclick = clearAllCacheWithConfirm;
}
+ var clearFavoritesBtn = panel.querySelector('#magnet-clear-favorites');
+ if (clearFavoritesBtn) {
+ clearFavoritesBtn.onclick = function() {
+ if (!confirm('确认清空全部收藏吗?')) {
+ return;
+ }
+ saveFavorites([]);
+ renderFavoritesList();
+ };
+ }
+
var closeBtn = panel.querySelector('.magnet-panel-close');
if (closeBtn) closeBtn.onclick = function() {
panel.style.display = 'none';
ball.style.display = 'flex';
};
- var 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() {
@@ -701,8 +747,28 @@
return panel;
}
+ function isThreadPage() {
+ return /\/thread-\d+-/i.test(window.location.href) || /[?&]tid=\d+/i.test(window.location.href);
+ }
+
function isListPage() {
- return document.querySelector('#threadlisttableid') !== null;
+ if (isThreadPage()) {
+ return false;
+ }
+
+ if (document.querySelector('#threadlisttableid')) {
+ return true;
+ }
+
+ if (document.querySelector('tbody[id^="normalthread_"]') || document.querySelector('tbody[id^="stickthread_"]')) {
+ return true;
+ }
+
+ if (/\/forum-\d+(?:-\d+)?\.html/i.test(window.location.href) || /[?&]mod=forumdisplay/i.test(window.location.href) || /[?&]fid=\d+/i.test(window.location.href)) {
+ return true;
+ }
+
+ return false;
}
function getCurrentPage() {
@@ -739,6 +805,29 @@
return getForumOrigin(window.location.href) + '/forum-' + getForumIdFromUrl(window.location.href) + '-';
}
+ function getCurrentPage() {
+ var currentUrl = window.location.href;
+ var match = currentUrl.match(/forum-\d+-(\d+)\.html/i) || currentUrl.match(/[?&]page=(\d+)/i);
+ return match ? Math.max(1, Number(match[1]) || 1) : 1;
+ }
+
+ function getLastPage() {
+ var pageLinks = document.querySelectorAll('a[href*="forum-"][href$=".html"], .pg a, .pgt a');
+ var maxPage = getCurrentPage();
+
+ pageLinks.forEach(function(link) {
+ var href = link && link.href ? link.href : '';
+ var text = link && link.textContent ? link.textContent.trim() : '';
+ var match = href.match(/forum-\d+-(\d+)\.html/i) || href.match(/[?&]page=(\d+)/i);
+ var pageNum = match ? Number(match[1]) : Number(text);
+ if (Number.isFinite(pageNum) && pageNum > maxPage) {
+ maxPage = pageNum;
+ }
+ });
+
+ return Math.max(1, maxPage);
+ }
+
function normalizeThreadUrl(url) {
if (typeof url !== 'string' || !url) {
return '';
@@ -858,21 +947,18 @@
if (countEl) countEl.textContent = count;
}
- // === 进度条功能 ===
function updateProgress(current, total, label) {
var progressBar = document.getElementById('magnet-progress-bar');
var progressLabel = document.getElementById('magnet-progress-label');
var progressPercent = document.getElementById('magnet-progress-percent');
-
+ var percent = total > 0 ? Math.max(0, Math.min(100, Math.round(current / total * 100))) : 0;
if (progressBar) {
- var percent = total > 0 ? Math.round((current / total) * 100) : 0;
progressBar.style.width = percent + '%';
}
if (progressLabel) {
- progressLabel.textContent = label || ('进度: ' + current + '/' + total);
+ progressLabel.textContent = label || ('进度 ' + current + '/' + total);
}
if (progressPercent) {
- var percent = total > 0 ? Math.round((current / total) * 100) : 0;
progressPercent.textContent = percent + '%';
}
}
@@ -881,255 +967,647 @@
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 buildRangeProgressLabel(rangeStart, rangeEnd, threadIndex, threadTotal) {
+ return '页' + rangeStart + '-' + rangeEnd + '/帖子' + threadIndex + '/' + threadTotal;
}
- function saveFavorites(favorites) {
- try {
- localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
- favoritesCache = favorites;
- } catch (e) {
- log('保存收藏失败: ' + e);
+ function updateRangeThreadProgress(context, rangeStart, rangeEnd, threadIndex, threadTotal) {
+ var totalPages = Math.max(1, Number(context.totalPageCount) || (Number(context.normalizedEnd || 0) - Number(context.startPage || 0) + 1));
+ var safeRangeStart = Math.max(Number(context.startPage) || 1, Number(rangeStart) || Number(context.startPage) || 1);
+ var safeRangeEnd = Math.max(safeRangeStart, Number(rangeEnd) || safeRangeStart);
+ var completedBefore = Math.max(0, safeRangeStart - (Number(context.startPage) || 1));
+ var coveredPages = Math.max(1, safeRangeEnd - safeRangeStart + 1);
+ var fractional = threadTotal > 0 ? Math.max(0, Math.min(1, Number(threadIndex) / Number(threadTotal))) : 0;
+ var completedPages = completedBefore + coveredPages * fractional;
+ updateProgress(completedPages, totalPages, buildRangeProgressLabel(safeRangeStart, safeRangeEnd, threadIndex, threadTotal));
+ }
+
+ var FAVORITES_KEY = 'magnet-favorites';
+ var SEARCH_HISTORY_KEY = 'magnet-search-history';
+ var MAX_SEARCH_HISTORY = 20;
+ var PRIVATE_STORAGE_KEY = 'magnet-private-state-v1';
+ var privateStateLoaded = false;
+ var favoritesCache = [];
+ var searchHistoryCache = [];
+ var sessionBackupState = null;
+
+ function cloneSimpleArray(value) {
+ return JSON.parse(JSON.stringify(Array.isArray(value) ? value : []));
+ }
+
+ function loadPrivateStateFromExtensionStorage() {
+ return new Promise(function(resolve) {
+ var migratedFavorites = [];
+ var migratedHistory = [];
+ var state = null;
+ if (privateStateLoaded) {
+ resolve({ favorites: cloneSimpleArray(favoritesCache), searchHistory: cloneSimpleArray(searchHistoryCache) });
+ return;
+ }
+ chrome.storage.local.get(PRIVATE_STORAGE_KEY, function(result) {
+ state = result && result[PRIVATE_STORAGE_KEY] && typeof result[PRIVATE_STORAGE_KEY] === 'object' ? result[PRIVATE_STORAGE_KEY] : {};
+ favoritesCache = Array.isArray(state.favorites) ? state.favorites : [];
+ searchHistoryCache = Array.isArray(state.searchHistory) ? state.searchHistory : [];
+ if (favoritesCache.length === 0) {
+ try {
+ migratedFavorites = JSON.parse(localStorage.getItem(FAVORITES_KEY) || '[]');
+ favoritesCache = Array.isArray(migratedFavorites) ? migratedFavorites : [];
+ localStorage.removeItem(FAVORITES_KEY);
+ } catch (e) {
+ favoritesCache = [];
+ }
+ }
+ if (searchHistoryCache.length === 0) {
+ try {
+ migratedHistory = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '[]');
+ searchHistoryCache = Array.isArray(migratedHistory) ? migratedHistory : [];
+ localStorage.removeItem(SEARCH_HISTORY_KEY);
+ } catch (e) {
+ searchHistoryCache = [];
+ }
+ }
+ privateStateLoaded = true;
+ persistPrivateState();
+ resolve({ favorites: cloneSimpleArray(favoritesCache), searchHistory: cloneSimpleArray(searchHistoryCache) });
+ });
+ });
+ }
+
+ function persistPrivateState() {
+ if (!chrome.storage || !chrome.storage.local) {
+ return;
+ }
+ chrome.storage.local.set((function() {
+ var data = {};
+ data[PRIVATE_STORAGE_KEY] = {
+ favorites: favoritesCache,
+ searchHistory: searchHistoryCache
+ };
+ return data;
+ })());
+ }
+
+ function loadFavorites() {
+ return cloneSimpleArray(favoritesCache);
+ }
+
+ function saveFavorites(favorites, options) {
+ options = options || {};
+ favoritesCache = Array.isArray(favorites) ? favorites : [];
+ persistPrivateState();
+ if (!options.skipCloudSync) {
+ scheduleCloudVaultSync();
}
}
function isFavorited(link) {
- var favorites = loadFavorites();
- return favorites.some(function(f) { return f.link === link; });
+ return loadFavorites().some(function(item) {
+ return item && item.link === link;
+ });
}
- function toggleFavorite(title, link, btn) {
+ function toggleFavorite(title, link, favoriteBtn) {
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 = '收藏';
+ var foundIndex = favorites.findIndex(function(item) {
+ return item.link === link;
+ });
+
+ if (foundIndex >= 0) {
+ favorites.splice(foundIndex, 1);
+ if (favoriteBtn) {
+ favoriteBtn.classList.remove('is-favorite');
+ favoriteBtn.textContent = '❤';
+ favoriteBtn.title = '收藏';
+ }
} else {
- favorites.push({
- title: title,
+ favorites.unshift({
+ title: title || '未命名帖子',
link: link,
addedAt: Date.now()
});
- btn.classList.add('is-favorite');
- btn.innerHTML = '♥';
- btn.title = '取消收藏';
+ if (favoriteBtn) {
+ favoriteBtn.classList.add('is-favorite');
+ favoriteBtn.textContent = '❤';
+ favoriteBtn.title = '取消收藏';
+ }
}
-
+
saveFavorites(favorites);
+
+ var favoritesView = document.getElementById('magnet-favorites-view');
+ if (favoritesView && favoritesView.classList.contains('is-active')) {
+ renderFavoritesList();
+ }
}
function renderFavoritesList() {
- var list = document.getElementById('magnet-favorites-list');
- if (!list) return;
-
+ var favoritesList = document.getElementById('magnet-favorites-list');
+ if (!favoritesList) return;
+
var favorites = loadFavorites();
- list.innerHTML = '';
-
+ favoritesList.innerHTML = '';
+
if (favorites.length === 0) {
- list.innerHTML = '📭
暂无收藏\br>点击结果列表中的 ♡ 按钮添加收藏
';
+ favoritesList.innerHTML = '';
return;
}
-
- favorites.forEach(function(fav) {
+
+ favorites.forEach(function(favorite) {
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 title = document.createElement('div');
+ title.className = 'magnet-favorite-title';
+ title.textContent = favorite.title || '未命名帖子';
+ title.title = favorite.title || '未命名帖子';
+
+ var actions = document.createElement('div');
+ actions.className = 'magnet-favorite-actions';
+
var copyBtn = document.createElement('button');
copyBtn.className = 'magnet-favorite-copy';
copyBtn.textContent = '复制';
copyBtn.onclick = function() {
- navigator.clipboard.writeText(fav.link)
- .then(function() {
- copyBtn.textContent = '已复制';
- setTimeout(function() { copyBtn.textContent = '复制'; }, 1000);
- });
+ navigator.clipboard.writeText(favorite.link).then(function() {
+ copyBtn.textContent = '已复制';
+ setTimeout(function() {
+ copyBtn.textContent = '复制';
+ }, 1000);
+ });
};
-
+
var removeBtn = document.createElement('button');
removeBtn.className = 'magnet-favorite-remove';
removeBtn.textContent = '删除';
removeBtn.onclick = function() {
- var favorites = loadFavorites();
- var idx = favorites.findIndex(function(f) { return f.link === fav.link; });
- if (idx >= 0) {
- favorites.splice(idx, 1);
- saveFavorites(favorites);
- renderFavoritesList();
- }
+ saveFavorites(loadFavorites().filter(function(itemData) {
+ return itemData.link !== favorite.link;
+ }));
+ renderFavoritesList();
};
-
- actionsEl.appendChild(copyBtn);
- actionsEl.appendChild(removeBtn);
- item.appendChild(titleEl);
- item.appendChild(actionsEl);
- list.appendChild(item);
+
+ actions.appendChild(copyBtn);
+ actions.appendChild(removeBtn);
+ item.appendChild(title);
+ item.appendChild(actions);
+ favoritesList.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 [];
+ return cloneSimpleArray(searchHistoryCache);
+ }
+
+ function removeSearchHistoryItem(keyword) {
+ var value = String(keyword || '').trim();
+ if (!value) {
+ return;
}
+ setSearchHistoryList(loadSearchHistory().filter(function(item) {
+ return item !== value;
+ }), { skipCloudSync: false });
}
function saveSearchHistory(keyword) {
- 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);
+ var value = String(keyword || '').trim();
+ if (!value) return;
+ var history = loadSearchHistory().filter(function(item) {
+ return item !== value;
+ });
+ history.unshift(value);
+ setSearchHistoryList(history.slice(0, MAX_SEARCH_HISTORY));
+ }
+
+ function removeHistoryDropdown() {
+ var dropdown = document.getElementById('magnet-history-dropdown');
+ if (dropdown) {
+ dropdown.remove();
}
}
function showHistoryDropdown(input) {
+ if (!input) return;
+ removeHistoryDropdown();
+
var history = loadSearchHistory();
- if (history.length === 0) {
- return;
- }
-
- // 移除已存在的下拉框
- var existing = document.querySelector('.magnet-history-dropdown');
- if (existing) existing.remove();
-
+ var list = null;
+ if (history.length === 0) return;
+
+ var wrap = input.parentNode;
+ if (!wrap) return;
+ wrap.classList.add('magnet-keyword-wrap');
+
var dropdown = document.createElement('div');
+ dropdown.id = 'magnet-history-dropdown';
dropdown.className = 'magnet-history-dropdown';
-
- history.forEach(function(kw) {
+
+ list = document.createElement('div');
+ list.className = 'magnet-history-list';
+
+ history.forEach(function(historyKeyword) {
var item = document.createElement('div');
item.className = 'magnet-history-item';
- item.textContent = kw;
+ var text = document.createElement('div');
+ var removeBtn = document.createElement('button');
+ text.className = 'magnet-history-text';
+ text.textContent = historyKeyword;
+ removeBtn.className = 'magnet-history-delete';
+ removeBtn.type = 'button';
+ removeBtn.textContent = '×';
+ removeBtn.title = '删除这条历史记录';
item.onclick = function() {
- input.value = kw;
- dropdown.remove();
- input.focus();
+ input.value = historyKeyword;
+ removeHistoryDropdown();
};
- dropdown.appendChild(item);
+ removeBtn.onclick = function(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ removeSearchHistoryItem(historyKeyword);
+ showHistoryDropdown(input);
+ };
+ item.appendChild(text);
+ item.appendChild(removeBtn);
+ list.appendChild(item);
});
-
+ dropdown.appendChild(list);
+
var clearItem = document.createElement('div');
clearItem.className = 'magnet-history-clear';
- clearItem.textContent = '清空历史';
+ clearItem.textContent = '清空历史记录';
clearItem.onclick = function() {
- localStorage.removeItem(HISTORY_KEY);
- dropdown.remove();
+ setSearchHistoryList([], { skipCloudSync: false });
+ removeHistoryDropdown();
};
dropdown.appendChild(clearItem);
-
- input.parentNode.style.position = 'relative';
- input.parentNode.appendChild(dropdown);
-
- // 点击外部关闭
+ wrap.appendChild(dropdown);
+
setTimeout(function() {
- document.addEventListener('click', function closeDropdown(e) {
- if (!dropdown.contains(e.target)) {
- dropdown.remove();
- document.removeEventListener('click', closeDropdown);
+ document.addEventListener('click', function hideHistory(event) {
+ if (!dropdown.contains(event.target) && event.target !== input) {
+ removeHistoryDropdown();
+ document.removeEventListener('click', hideHistory);
}
});
- }, 100);
+ }, 0);
}
- // === 通知功能 ===
- function playNotificationSound() {
+ function playDoneTone() {
try {
- var audioContext = new (window.AudioContext || window.webkitAudioContext)();
- var oscillator = audioContext.createOscillator();
- var gainNode = audioContext.createGain();
-
+ var AudioContextCtor = window.AudioContext || window.webkitAudioContext;
+ if (!AudioContextCtor) return;
+ var context = new AudioContextCtor();
+ var oscillator = context.createOscillator();
+ var gainNode = context.createGain();
oscillator.connect(gainNode);
- gainNode.connect(audioContext.destination);
-
- oscillator.frequency.value = 800;
+ gainNode.connect(context.destination);
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);
+ oscillator.frequency.value = 880;
+ gainNode.gain.setValueAtTime(0.0001, context.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.18, context.currentTime + 0.01);
+ gainNode.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.35);
+ oscillator.start(context.currentTime);
+ oscillator.stop(context.currentTime + 0.35);
} catch (e) {
- log('播放提示音失败: ' + e);
+ log('提示音播放失败: ' + e);
}
}
- function showBrowserNotification(title, body) {
- if (!('Notification' in window)) {
+ function showDoneNotification(title, body) {
+ if (!('Notification' in window)) return;
+ if (Notification.permission === 'granted') {
+ new Notification(title, { body: body });
return;
}
-
- if (Notification.permission === 'granted') {
- new Notification(title, { body: body, icon: chrome.runtime ? chrome.runtime.getURL('icon.png') : undefined });
- } else if (Notification.permission !== 'denied') {
+ 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 });
+ new Notification(title, { body: body });
}
});
}
}
- 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 + ' 秒';
+ var cloudSyncState = {
+ authenticated: false,
+ healthy: false,
+ color: 'red',
+ text: '云同步未登录',
+ email: '',
+ lastError: ''
+ };
+ var cloudVaultHydrated = false;
+ var cloudVaultSyncTimer = null;
+
+ function updateCloudSyncIndicator(status) {
+ var dot = document.getElementById('magnet-cloud-sync-dot');
+ var label = document.getElementById('magnet-cloud-sync-text');
+ cloudSyncState = Object.assign({}, cloudSyncState, status || {});
+ if (dot) {
+ dot.classList.toggle('is-ok', cloudSyncState.color === 'green');
+ }
+ if (label) {
+ label.textContent = cloudSyncState.text || (cloudSyncState.color === 'green' ? '云同步正常' : '云同步异常');
+ label.title = cloudSyncState.email ? (cloudSyncState.text + ' · ' + cloudSyncState.email) : (cloudSyncState.lastError || cloudSyncState.text || '云同步');
+ }
+ }
+
+ async function getCloudSyncStatus() {
+ try {
+ var response = await sendRuntimeMessage({ action: 'cloudGetSyncStatus' }, 12000);
+ if (response && response.ok) {
+ updateCloudSyncIndicator(response);
+ return response;
+ }
+ updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: response && response.error ? response.error : '状态获取失败' });
+ return null;
+ } catch (e) {
+ updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
+ return null;
+ }
+ }
+
+ function getUiSettingsSnapshot() {
+ return {
+ speedMode: speedConfig[speedMode] ? speedMode : 'fast'
+ };
+ }
+
+ function applyUiSettingsSnapshot(snapshot) {
+ if (!snapshot || typeof snapshot !== 'object') {
+ return;
+ }
+ if (snapshot.speedMode && speedConfig[snapshot.speedMode]) {
+ speedMode = snapshot.speedMode;
+ var speedSelect = document.getElementById('speed-select');
+ if (speedSelect) {
+ speedSelect.value = speedMode;
}
}
- showBrowserNotification('磁力链接抓取完成', '共获取 ' + count + ' 个磁力链接' + durationText);
}
- var countEl = document.getElementById('magnet-count-num');
- if (countEl) countEl.textContent = count;
+
+ function setSearchHistoryList(historyList, options) {
+ var normalized = [];
+ var seen = Object.create(null);
+ options = options || {};
+ (Array.isArray(historyList) ? historyList : []).forEach(function(item) {
+ var value = String(item || '').trim();
+ if (!value || seen[value]) {
+ return;
+ }
+ seen[value] = true;
+ normalized.push(value);
+ });
+ searchHistoryCache = normalized.slice(0, MAX_SEARCH_HISTORY);
+ persistPrivateState();
+ if (!options.skipCloudSync) {
+ scheduleCloudVaultSync();
+ }
+ }
+
+ function openCloudSyncCenter() {
+ sendRuntimeMessage({ action: 'openCloudSyncPage' }, 8000).catch(function(e) {
+ updateStatus('打开云同步中心失败:' + (e && e.message ? e.message : String(e)), 'error');
+ });
+ }
+
+ function triggerMagnetDownload(link, downloadBtn) {
+ var anchor = null;
+ var cleanup = null;
+ var fallbackCopy = function(message, type) {
+ navigator.clipboard.writeText(link).then(function() {
+ updateStatus(message || '未检测到本地下载器,已复制磁力链接。请在扩展页下载 qBittorrent(推荐节点1/2)', type || 'done');
+ }).catch(function() {
+ updateStatus('未能调用本地下载器,请手动复制磁力链接', 'error');
+ });
+ };
+
+ try {
+ updateStatus('正在调用本地下载器;如未安装,可到扩展页下载 qBittorrent(推荐节点1/2)', 'loading');
+ anchor = document.createElement('a');
+ anchor.href = link;
+ anchor.style.display = 'none';
+ anchor.rel = 'noreferrer noopener';
+ document.body.appendChild(anchor);
+ anchor.click();
+ if (downloadBtn) {
+ downloadBtn.textContent = '已发送';
+ cleanup = function() {
+ downloadBtn.textContent = '下载';
+ };
+ setTimeout(cleanup, 1200);
+ }
+ setTimeout(function() {
+ updateStatus('已发送到本地下载器,如未弹出请先安装并关联 qBittorrent', 'done');
+ }, 300);
+ } catch (e) {
+ fallbackCopy('未检测到本地下载器,已复制磁力链接。请在扩展页下载 qBittorrent(推荐节点1/2)', 'done');
+ } finally {
+ if (anchor && anchor.parentNode) {
+ anchor.parentNode.removeChild(anchor);
+ }
+ }
+ }
+
+ function mergeFavoritesList(localItems, remoteItems) {
+ var merged = [];
+ var seen = Object.create(null);
+ (Array.isArray(remoteItems) ? remoteItems : []).concat(Array.isArray(localItems) ? localItems : []).forEach(function(item) {
+ var link = item && typeof item.link === 'string' ? item.link : '';
+ if (!link || seen[link]) {
+ return;
+ }
+ seen[link] = true;
+ merged.push({
+ title: typeof item.title === 'string' ? item.title : '未命名帖子',
+ link: link,
+ addedAt: Number(item.addedAt) || Date.now()
+ });
+ });
+ return merged;
+ }
+
+ function mergeHistoryList(localItems, remoteItems) {
+ var merged = [];
+ var seen = Object.create(null);
+ (Array.isArray(remoteItems) ? remoteItems : []).concat(Array.isArray(localItems) ? localItems : []).forEach(function(item) {
+ var value = String(item || '').trim();
+ if (!value || seen[value]) {
+ return;
+ }
+ seen[value] = true;
+ merged.push(value);
+ });
+ return merged.slice(0, MAX_SEARCH_HISTORY);
+ }
+
+ async function pushCloudVaultItems(items) {
+ try {
+ var response = await sendRuntimeMessage({ action: 'cloudPushVaultItems', items: items }, 20000);
+ if (response && response.status) {
+ updateCloudSyncIndicator(response.status);
+ }
+ return response;
+ } catch (e) {
+ updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
+ return null;
+ }
+ }
+
+ async function pullCloudVaultItems(itemTypes) {
+ try {
+ var response = await sendRuntimeMessage({ action: 'cloudPullVaultItems', itemTypes: itemTypes }, 20000);
+ if (response && response.ok) {
+ if (response.status) {
+ updateCloudSyncIndicator(response.status);
+ }
+ return response.items || [];
+ }
+ return [];
+ } catch (e) {
+ updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
+ return [];
+ }
+ }
+
+ function scheduleCloudVaultSync() {
+ if (cloudVaultSyncTimer) {
+ clearTimeout(cloudVaultSyncTimer);
+ }
+ cloudVaultSyncTimer = setTimeout(function() {
+ cloudVaultSyncTimer = null;
+ syncAllLocalVaultData();
+ }, 600);
+ }
+
+ async function syncAllLocalVaultData() {
+ if (!cloudSyncState.authenticated) {
+ return null;
+ }
+ return pushCloudVaultItems([
+ { itemType: 'favorites', itemKey: 'default', data: loadFavorites() },
+ { itemType: 'search_history', itemKey: 'default', data: loadSearchHistory() },
+ { itemType: 'ui_settings', itemKey: 'default', data: getUiSettingsSnapshot() }
+ ]);
+ }
+
+ function applyVaultItems(items, options) {
+ var remoteFavorites = [];
+ var remoteHistory = [];
+ var remoteSettings = null;
+ var remoteProgress = null;
+ options = options || {};
+
+ (Array.isArray(items) ? items : []).forEach(function(item) {
+ if (!item || typeof item !== 'object') {
+ return;
+ }
+ if (item.itemType === 'favorites' && Array.isArray(item.data)) {
+ remoteFavorites = item.data;
+ } else if (item.itemType === 'search_history' && Array.isArray(item.data)) {
+ remoteHistory = item.data;
+ } else if (item.itemType === 'ui_settings' && item.data && typeof item.data === 'object') {
+ remoteSettings = item.data;
+ } else if (item.itemType === 'progress_state' && item.data && typeof item.data === 'object') {
+ remoteProgress = item.data;
+ }
+ });
+
+ if (remoteFavorites.length > 0) {
+ saveFavorites(mergeFavoritesList(loadFavorites(), remoteFavorites), { skipCloudSync: true });
+ }
+ if (remoteHistory.length > 0) {
+ setSearchHistoryList(mergeHistoryList(loadSearchHistory(), remoteHistory), { skipCloudSync: true });
+ }
+ if (remoteSettings) {
+ applyUiSettingsSnapshot(remoteSettings);
+ }
+ if (options.restoreProgress && remoteProgress && allMagnetLinks.length === 0) {
+ applyStateSnapshot(remoteProgress);
+ }
+ cloudVaultHydrated = true;
+ }
+
+ async function refreshCloudStatusAndVault(options) {
+ var status = await getCloudSyncStatus();
+ if (!status) {
+ renderCloudAuthSection();
+ return null;
+ }
+ if (status.authenticated && !cloudVaultHydrated && !options?.skipVaultPull) {
+ applyVaultItems(await pullCloudVaultItems(['favorites', 'search_history', 'ui_settings', 'progress_state']), { restoreProgress: !!options?.restoreProgress });
+ }
+ renderCloudAuthSection();
+ return status;
+ }
+
+ async function logoutCloudAccount() {
+ try {
+ var response = await sendRuntimeMessage({ action: 'cloudLogout' }, 15000);
+ cloudVaultHydrated = false;
+ updateCloudSyncIndicator(response && response.status ? response.status : { color: 'red', text: '云同步未登录', authenticated: false, healthy: false, email: '' });
+ renderCloudAuthSection();
+ updateStatus('已退出云同步', 'done');
+ } catch (e) {
+ updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
+ updateStatus('退出失败:' + (e && e.message ? e.message : String(e)), 'error');
+ }
+ }
+
+ function renderCloudAuthSection() {
+ var settingsArea = document.getElementById('magnet-cloud-page');
+ var existing = document.getElementById('magnet-cloud-auth-card');
+ var viewMeta = document.getElementById('magnet-cloud-view-meta');
+ var html = '';
+ var card = null;
+ var syncNowBtn = null;
+ var logoutBtn = null;
+ var openCenterBtn = null;
+ if (!settingsArea) {
+ return;
+ }
+ if (existing) {
+ existing.remove();
+ }
+ card = document.createElement('div');
+ card.id = 'magnet-cloud-auth-card';
+ card.className = 'magnet-cloud-auth-card';
+ if (viewMeta) {
+ viewMeta.textContent = cloudSyncState.email ? (cloudSyncState.text + ' · ' + cloudSyncState.email) : (cloudSyncState.text || '状态未知');
+ }
+ if (cloudSyncState.authenticated) {
+ html = '云同步已开启
账号:' + escapeHtml(cloudSyncState.email || '已登录') + '
' + escapeHtml(cloudSyncState.text || '云同步正常') + '
';
+ } else {
+ html = '云同步账号
为了安全,登录/注册已移到扩展独立页面中进行。网页本身不会再承载账号密码输入框。
';
+ }
+ card.innerHTML = html;
+ settingsArea.innerHTML = '';
+ settingsArea.appendChild(card);
+ if (cloudSyncState.authenticated) {
+ syncNowBtn = card.querySelector('#magnet-cloud-sync-now');
+ logoutBtn = card.querySelector('#magnet-cloud-logout');
+ openCenterBtn = card.querySelector('#magnet-cloud-open-center');
+ if (syncNowBtn) {
+ syncNowBtn.onclick = function() {
+ updateStatus('正在同步保险柜...', 'loading');
+ syncAllLocalVaultData().then(function() {
+ updateStatus('保险柜同步完成', 'done');
+ }).catch(function(e) {
+ updateStatus('保险柜同步失败:' + (e && e.message ? e.message : String(e)), 'error');
+ });
+ };
+ }
+ if (logoutBtn) {
+ logoutBtn.onclick = logoutCloudAccount;
+ }
+ if (openCenterBtn) {
+ openCenterBtn.onclick = openCloudSyncCenter;
+ }
+ } else {
+ openCenterBtn = card.querySelector('#magnet-cloud-open-center');
+ if (openCenterBtn) {
+ openCenterBtn.onclick = openCloudSyncCenter;
+ }
+ }
}
function clearMagnetList(skipPersist) {
@@ -1172,104 +1650,31 @@
titleEl.title = safeTitle;
titleEl.textContent = safeTitle;
- var btnContainer = document.createElement('div');
- btnContainer.style.cssText = 'display:flex;gap:6px;flex-shrink:0;';
+ var actions = document.createElement('div');
+ actions.className = 'magnet-item-actions';
- 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.textContent = '❤';
favoriteBtn.title = '收藏';
+ if (isFavorited(safeLink)) {
+ favoriteBtn.classList.add('is-favorite');
+ favoriteBtn.textContent = '❤';
+ 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 = '复制';
+
+ var downloadBtn = document.createElement('button');
+ downloadBtn.className = 'magnet-download-btn';
+ downloadBtn.setAttribute('data-magnet', safeLink);
+ downloadBtn.textContent = '下载';
titleEl.onclick = function() {
navigator.clipboard.writeText(safeLink)
@@ -1301,8 +1706,15 @@
});
};
+ downloadBtn.onclick = function() {
+ triggerMagnetDownload(safeLink, downloadBtn);
+ };
+
item.appendChild(titleEl);
- item.appendChild(copyBtn);
+ actions.appendChild(favoriteBtn);
+ actions.appendChild(copyBtn);
+ actions.appendChild(downloadBtn);
+ item.appendChild(actions);
list.appendChild(item);
setPanelView('results');
@@ -1437,6 +1849,18 @@
.filter(function(item) { return !!item; });
}
+ function escapeHtml(value) {
+ return String(value || '').replace(/[&<>"']/g, function(character) {
+ return {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ }[character] || character;
+ });
+ }
+
function filterThreadsByKeywords(threadList, keywords) {
var normalizedThreads = normalizeCachedThreads(threadList);
if (!keywords || keywords.length === 0) {
@@ -1476,10 +1900,88 @@
if (startPage !== 1) {
return 0;
}
-
return Math.min(20, Math.max(0, endPage - startPage + 1));
}
+ async function probeForumUpdateDepth(forumKey, baseUrl, startPage, endPage) {
+ if (startPage !== 1) {
+ return { refreshPages: 0, probeThreads: null, reason: 'not_from_page1' };
+ }
+
+ var totalRange = endPage - startPage + 1;
+ var maxRefresh = Math.min(20, totalRange);
+ var probeUrl = baseUrl + '1.html';
+
+ try {
+ var response = await sendRuntimeMessage({
+ action: 'fetchHtml',
+ url: probeUrl,
+ timeoutMs: getFetchTimeout()
+ }, getMessageTimeout());
+
+ if (!response || response.error || !response.html) {
+ log('[探针] 获取第1页失败,回退到固定刷新');
+ return { refreshPages: maxRefresh, probeThreads: null, reason: 'probe_fetch_failed' };
+ }
+
+ var liveThreads = extractThreadsFromHtml(response.html);
+ var threadsPerPage = liveThreads.length || 25;
+
+ if (liveThreads.length === 0) {
+ log('[探针] 第1页未提取到帖子,回退到固定刷新');
+ return { refreshPages: maxRefresh, probeThreads: null, reason: 'no_threads_extracted' };
+ }
+
+ var cachedPage = await sendRuntimeMessage({
+ action: 'cacheGetPageThreadKeys',
+ forumKey: forumKey,
+ page: 1
+ }, 5000);
+
+ if (!cachedPage || !cachedPage.ok || !cachedPage.threadKeys || cachedPage.threadKeys.length === 0) {
+ log('[探针] 无第1页缓存记录,首次抓取');
+ return { refreshPages: maxRefresh, probeThreads: liveThreads, probeHtml: response.html, reason: 'no_cached_page1' };
+ }
+
+ var cachedKeySet = Object.create(null);
+ cachedPage.threadKeys.forEach(function(key) {
+ var pureKey = String(key || '').split('::').pop();
+ cachedKeySet[pureKey] = true;
+ });
+
+ var newCount = 0;
+ liveThreads.forEach(function(thread) {
+ var key = thread.threadKey || getThreadKeyFromUrl(thread.url);
+ if (key && !cachedKeySet[key]) {
+ newCount++;
+ }
+ });
+
+ if (newCount === 0) {
+ log('[探针] 第1页与缓存完全一致,无需刷新');
+ return { refreshPages: 0, probeThreads: liveThreads, probeHtml: response.html, reason: 'no_change', newCount: 0 };
+ }
+
+ var estimatedShift = Math.ceil(newCount / threadsPerPage) + 1;
+ var refreshPages = Math.min(estimatedShift, maxRefresh);
+
+ log('[探针] 检测到 ' + newCount + ' 个新帖 (每页' + threadsPerPage + '帖),预估影响 ' + estimatedShift + ' 页,将刷新前 ' + refreshPages + ' 页');
+
+ return {
+ refreshPages: refreshPages,
+ probeThreads: liveThreads,
+ probeHtml: response.html,
+ newCount: newCount,
+ threadsPerPage: threadsPerPage,
+ reason: 'detected_' + newCount + '_new'
+ };
+ } catch (e) {
+ var errMsg = e && e.message ? e.message : String(e);
+ log('[探针] 异常: ' + errMsg + ',回退到固定刷新');
+ return { refreshPages: maxRefresh, probeThreads: null, reason: 'probe_error' };
+ }
+ }
+
async function getCachedCoveragePlan(forumKey, startPage, endPage, frontRefreshPages) {
try {
var response = await sendRuntimeMessage({
@@ -1897,11 +2399,7 @@
}
function clearSessionBackupState() {
- try {
- sessionStorage.removeItem(STATE_BACKUP_KEY);
- } catch (e) {
- log('清理会话备份失败: ' + e);
- }
+ sessionBackupState = null;
}
async function clearAllResults() {
@@ -1959,22 +2457,11 @@
}
function saveStateToSessionBackup(snapshot) {
- try {
- sessionStorage.setItem(STATE_BACKUP_KEY, JSON.stringify(snapshot));
- } catch (e) {
- log('保存会话备份失败: ' + e);
- }
+ sessionBackupState = snapshot;
}
function readStateFromSessionBackup() {
- try {
- var raw = sessionStorage.getItem(STATE_BACKUP_KEY);
- if (!raw) return null;
- return JSON.parse(raw);
- } catch (e) {
- log('读取会话备份失败: ' + e);
- return null;
- }
+ return sessionBackupState;
}
function applyStateSnapshot(snapshot) {
@@ -2135,7 +2622,6 @@
var concurrency = Math.min(getThreadConcurrency(), filteredThreads.length);
var cursor = 0;
var fetchedCount = 0;
- var cachePayload = [];
async function worker() {
while (!stopFetching) {
@@ -2157,7 +2643,12 @@
processedThreadKeys[threadKey] = true;
}
- updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading');
+ if (!options.suppressStatusProgress) {
+ updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading');
+ }
+ if (options.onProgress) {
+ options.onProgress(currentIndex + 1, filteredThreads.length);
+ }
try {
var threadResponse = await sendRuntimeMessage({
@@ -2171,12 +2662,16 @@
} 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
- });
+ try {
+ await saveThreadMagnetsToCache(options.forumKey, [{
+ threadKey: threadKey,
+ url: threadUrl,
+ title: threadTitle,
+ magnets: threadMagnets
+ }]);
+ } catch (saveErr) {
+ log('实时保存线程缓存失败: ' + (saveErr && saveErr.message ? saveErr.message : String(saveErr)));
+ }
}
threadMagnets.forEach(function(m) {
@@ -2206,15 +2701,15 @@
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);
+ var rangeText = typeof pageLabel === 'string' ? pageLabel : String(pageLabel || '');
+ var rangeMatch = rangeText.match(/^(\d+)(?:-(\d+))?$/);
+ var rangeStart = rangeMatch ? Number(rangeMatch[1]) : Number(context.startPage || 1);
+ var rangeEnd = rangeMatch ? Math.max(rangeStart, Number(rangeMatch[2] || rangeMatch[1])) : rangeStart;
if (normalizedThreads.length === 0) {
return;
}
@@ -2247,7 +2742,13 @@
cacheResult.threadsToFetch,
context.allMagnets,
context.processedThreadKeys,
- { forumKey: context.forumKey }
+ {
+ forumKey: context.forumKey,
+ suppressStatusProgress: true,
+ onProgress: function(threadIndex, threadTotal) {
+ updateRangeThreadProgress(context, rangeStart, rangeEnd, threadIndex, threadTotal);
+ }
+ }
);
}
@@ -2268,11 +2769,7 @@
}
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 + '页');
+ updateProgress(page - context.startPage + 1, context.normalizedEnd - context.startPage + 1, '第' + page + '/' + context.normalizedEnd + '页');
var pageUrl = context.baseUrl + page + '.html';
try {
@@ -2353,21 +2850,11 @@
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');
+ var startTime = Date.now();
panel.style.display = 'flex';
if (ball) ball.style.display = 'none';
if (!preserveExisting) {
@@ -2422,11 +2909,12 @@
scheduleStatePersist();
var forumKey = getForumKey();
- var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd);
var searchContext = {
forumKey: forumKey,
baseUrl: baseUrl,
+ startPage: normalizedStart,
normalizedEnd: normalizedEnd,
+ totalPageCount: normalizedEnd - normalizedStart + 1,
keywords: buildKeywordList(keyword),
coverageThreadMap: Object.create(null),
processedThreadKeys: Object.create(null),
@@ -2435,24 +2923,164 @@
totalFetched: 0,
failedPages: 0
};
+
+ var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd);
+
+ updateStatus('正在规划缓存:检查本地缓存 / 云端规划 / 复用块...', 'loading');
var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages);
var cacheStrategy = 'full_live';
+ var probeResult = null;
if (cachePlan && cachePlan.exactCoverage && Array.isArray(cachePlan.exactCoverage.threads) && cachePlan.exactCoverage.threads.length > 0) {
cacheStrategy = 'exact_cache';
await processCachedThreadBatch('缓存', cachePlan.exactCoverage.threads, searchContext, '命中当前板块缓存,直接搜索...');
progressRuntime.resumeFromPage = normalizedEnd + 1;
scheduleStatePersist();
+
+ if (normalizedStart === 1) {
+ updateStatus('智能探测:检测论坛是否有更新...', 'loading');
+ probeResult = await probeForumUpdateDepth(forumKey, baseUrl, normalizedStart, normalizedEnd);
+
+ if (probeResult.refreshPages > 0) {
+ frontRefreshPages = probeResult.refreshPages;
+ log('[智能更新] 缓存命中,探针检测到' + (probeResult.newCount || 0) + '个新帖,刷新前' + frontRefreshPages + '页');
+ updateStatus('发现' + (probeResult.newCount || 0) + '个新帖,刷新前' + frontRefreshPages + '页...', 'loading');
+
+ if (probeResult.probeThreads) {
+ mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
+ await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
+ var probeFiltered = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
+ searchContext.matchedThreads += probeFiltered.length;
+ var probeCacheResult = await applyCachedMagnetHits(1, probeFiltered, searchContext);
+ if (probeCacheResult.threadsToFetch.length > 0) {
+ searchContext.totalFetched += await fetchThreadsInParallel(
+ 1, probeCacheResult.threadsToFetch, searchContext.allMagnets,
+ searchContext.processedThreadKeys, { forumKey: forumKey }
+ );
+ }
+ }
+
+ var exactRefreshEnd = Math.min(normalizedEnd, normalizedStart + frontRefreshPages - 1);
+ if (exactRefreshEnd >= 2) {
+ await fetchLivePageRange(2, exactRefreshEnd, searchContext);
+ }
+ if (!stopFetching) {
+ await saveCoverageSnapshot(
+ forumKey, normalizedStart, normalizedEnd,
+ coverageMapToList(searchContext.coverageThreadMap),
+ frontRefreshPages, 'exact_cache_refreshed'
+ );
+ }
+ } else {
+ log('[智能更新] 缓存命中,探针未检测到变化,补充处理探针数据以填补缓存缺口');
+ if (probeResult.probeThreads && probeResult.probeThreads.length > 0) {
+ mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
+ await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
+ var gapFiltered = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
+ if (gapFiltered.length > 0) {
+ var gapCacheResult = await applyCachedMagnetHits(1, gapFiltered, searchContext);
+ if (gapCacheResult.threadsToFetch.length > 0) {
+ log('[智能更新] 探针填补缺口:发现' + gapCacheResult.threadsToFetch.length + '个缓存未覆盖的帖子,补抓中...');
+ searchContext.totalFetched += await fetchThreadsInParallel(
+ 1, gapCacheResult.threadsToFetch, searchContext.allMagnets,
+ searchContext.processedThreadKeys, { forumKey: forumKey }
+ );
+ }
+ }
+ }
+ }
+ }
} else {
- var refreshedFrontEnd = normalizedStart - 1;
- if (frontRefreshPages > 0) {
- refreshedFrontEnd = normalizedStart + frontRefreshPages - 1;
- updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading');
- await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext);
+ if (normalizedStart === 1) {
+ updateStatus('智能探测:检测论坛更新情况...', 'loading');
+ probeResult = await probeForumUpdateDepth(forumKey, baseUrl, normalizedStart, normalizedEnd);
+ frontRefreshPages = probeResult.refreshPages;
+
+ if (probeResult.reason === 'no_change') {
+ log('[智能更新] 探针未检测到变化,补充处理探针数据以填补缓存缺口');
+ if (probeResult.probeThreads && probeResult.probeThreads.length > 0) {
+ mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
+ await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
+ var gapFiltered3 = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
+ if (gapFiltered3.length > 0) {
+ var gapCacheResult3 = await applyCachedMagnetHits(1, gapFiltered3, searchContext);
+ if (gapCacheResult3.threadsToFetch.length > 0) {
+ log('[智能更新] 探针填补缺口:发现' + gapCacheResult3.threadsToFetch.length + '个缓存未覆盖的帖子,补抓中...');
+ searchContext.totalFetched += await fetchThreadsInParallel(
+ 1, gapCacheResult3.threadsToFetch, searchContext.allMagnets,
+ searchContext.processedThreadKeys, { forumKey: forumKey }
+ );
+ }
+ }
+ }
+ } else if (frontRefreshPages > 0) {
+ log('[智能更新] 探针检测到更新,需刷新前' + frontRefreshPages + '页');
+ }
}
- var shiftedReuseEnd = refreshedFrontEnd;
- if (!stopFetching && cachePlan && cachePlan.shiftedCoverage && Array.isArray(cachePlan.shiftedCoverage.threads) && cachePlan.shiftedCoverage.threads.length > 0) {
+ var refreshedFrontEnd = normalizedStart - 1;
+ var shiftedReuseEnd = normalizedStart - 1;
+ var cachedBlocks = cachePlan && Array.isArray(cachePlan.cachedBlocks) ? cachePlan.cachedBlocks.slice() : [];
+ var liveCursor = normalizedStart;
+
+ if (frontRefreshPages > 0) {
+ refreshedFrontEnd = Math.min(normalizedEnd, normalizedStart + frontRefreshPages - 1);
+
+ if (probeResult && probeResult.probeThreads) {
+ mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
+ await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
+ var probeFiltered2 = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
+ searchContext.matchedThreads += probeFiltered2.length;
+ var probeCacheResult2 = await applyCachedMagnetHits(1, probeFiltered2, searchContext);
+ if (probeCacheResult2.threadsToFetch.length > 0) {
+ searchContext.totalFetched += await fetchThreadsInParallel(
+ 1, probeCacheResult2.threadsToFetch, searchContext.allMagnets,
+ searchContext.processedThreadKeys, { forumKey: forumKey }
+ );
+ }
+
+ if (refreshedFrontEnd >= 2) {
+ updateStatus('智能增量:刷新第2-' + refreshedFrontEnd + '页 (发现' + (probeResult.newCount || 0) + '个新帖)...', 'loading');
+ await fetchLivePageRange(2, refreshedFrontEnd, searchContext);
+ }
+ } else {
+ updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading');
+ await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext);
+ }
+ liveCursor = refreshedFrontEnd + 1;
+ }
+
+ cachedBlocks = cachedBlocks.filter(function(block) {
+ return block && Array.isArray(block.threads) && block.threads.length > 0 && Number(block.endPage || 0) >= liveCursor;
+ }).sort(function(a, b) {
+ return Number(a.startPage || 0) - Number(b.startPage || 0);
+ });
+
+ if (!stopFetching && cachedBlocks.length > 0) {
+ cacheStrategy = 'assembled_pages';
+ for (var blockIndex = 0; blockIndex < cachedBlocks.length; blockIndex++) {
+ var cachedBlock = cachedBlocks[blockIndex];
+ var blockStart = Math.max(liveCursor, Number(cachedBlock.startPage) || liveCursor);
+ var blockEnd = Math.min(normalizedEnd, Number(cachedBlock.endPage) || blockStart);
+ if (blockStart > blockEnd) {
+ continue;
+ }
+ if (liveCursor < blockStart) {
+ updateStatus('缓存复用:补抓缺口页 ' + liveCursor + '-' + (blockStart - 1), 'loading');
+ await fetchLivePageRange(liveCursor, blockStart - 1, searchContext);
+ }
+ await processCachedThreadBatch(
+ String(blockStart) + '-' + String(blockEnd),
+ cachedBlock.threads,
+ searchContext,
+ '缓存复用:直接使用页缓存 ' + blockStart + '-' + blockEnd + ' 页'
+ );
+ shiftedReuseEnd = Math.max(shiftedReuseEnd, blockEnd);
+ liveCursor = blockEnd + 1;
+ progressRuntime.resumeFromPage = liveCursor;
+ scheduleStatePersist();
+ }
+ } else if (!stopFetching && cachePlan && cachePlan.shiftedCoverage && Array.isArray(cachePlan.shiftedCoverage.threads) && cachePlan.shiftedCoverage.threads.length > 0) {
cacheStrategy = 'smart_incremental';
shiftedReuseEnd = Math.max(shiftedReuseEnd, Math.min(normalizedEnd, Number(cachePlan.shiftedCoverage.reusedEndPage) || refreshedFrontEnd));
await processCachedThreadBatch(
@@ -2463,9 +3091,10 @@
);
progressRuntime.resumeFromPage = shiftedReuseEnd + 1;
scheduleStatePersist();
+ liveCursor = shiftedReuseEnd + 1;
}
- var liveTailStart = Math.max(normalizedStart, shiftedReuseEnd + 1);
+ var liveTailStart = Math.max(normalizedStart, liveCursor, shiftedReuseEnd + 1);
if (!stopFetching && liveTailStart <= normalizedEnd) {
if (liveTailStart > normalizedStart) {
updateStatus('智能增量:补抓未覆盖页 ' + liveTailStart + '-' + normalizedEnd, 'loading');
@@ -2499,10 +3128,7 @@
var keywordMsg = keyword ? ' (关键词:' + keyword + ' 匹配:' + searchContext.matchedThreads + '帖)' : '';
var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : '';
-
- // 更新进度条为100%
- updateProgress(normalizedEnd - normalizedStart + 1, normalizedEnd - normalizedStart + 1, '已完成');
-
+ updateProgress(normalizedEnd - normalizedStart + 1, normalizedEnd - normalizedStart + 1, stopFetching ? '已停止' : '已完成');
if (stopFetching) {
progressRuntime.stoppedByUser = true;
updateStatus('已停止 - 找到' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'error');
@@ -2510,24 +3136,11 @@
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');
+ playDoneTone();
+ showDoneNotification('磁力链接抓取完成', '共获取 ' + searchContext.allMagnets.size + ' 个磁力链接,用时 ' + Math.max(1, Math.round((Date.now() - startTime) / 1000)) + ' 秒');
}
scheduleStatePersist();
@@ -2538,6 +3151,9 @@
}
function fetchAllMagnets() {
+ if ('Notification' in window && Notification.permission === 'default') {
+ Notification.requestPermission().catch(function() {});
+ }
var startPage = parseInt(document.getElementById('page-start').value, 10) || 1;
var endPage = parseInt(document.getElementById('page-end').value, 10) || 1;
if (endPage < startPage) {
@@ -2548,11 +3164,11 @@
fetchFromPage(startPage, endPage);
}
- function stopFetch() {
- stopFetching = true;
+ function stopFetch() {
+ stopFetching = true;
progressRuntime.isRunning = false;
progressRuntime.stoppedByUser = true;
- scheduleStatePersist();
+ saveStateNow();
}
async function initializePluginUi() {
@@ -2563,25 +3179,30 @@
var settingsArea = panel.querySelector('#magnet-settings');
if (!settingsArea) return;
+ await loadPrivateStateFromExtensionStorage();
+
+ renderCloudAuthSection();
+
if (isListPage() && !document.getElementById('keyword-input')) {
var currentPage = getCurrentPage();
var keywordDiv = document.createElement('div');
keywordDiv.className = 'magnet-control-row';
keywordDiv.innerHTML = '';
-
- // 添加历史记录下拉功能
- var keywordInput = keywordDiv.querySelector('#keyword-input');
- if (keywordInput) {
- keywordInput.addEventListener('focus', function() {
- showHistoryDropdown(keywordInput);
+ var keywordInputEl = keywordDiv.querySelector('#keyword-input');
+ if (keywordInputEl) {
+ keywordInputEl.addEventListener('focus', function() {
+ showHistoryDropdown(keywordInputEl);
+ });
+ keywordInputEl.addEventListener('input', function() {
+ removeHistoryDropdown();
});
}
var pageRange = document.createElement('div');
pageRange.className = 'magnet-control-row';
pageRange.style.cssText = 'font-size:12px;color:#8892a4;display:flex;align-items:center;gap:8px';
- pageRange.innerHTML = '页码范围到页';
+ pageRange.innerHTML = '页码范围到页';
var btnContainer = document.createElement('div');
btnContainer.className = 'magnet-control-row';
@@ -2614,6 +3235,26 @@
settingsArea.appendChild(pageRange);
settingsArea.appendChild(btnContainer);
settingsArea.appendChild(speedDiv);
+
+ var pageStartEl = pageRange.querySelector('#page-start');
+ var pageEndEl = pageRange.querySelector('#page-end');
+ var pageRangeAllBtn = pageRange.querySelector('#page-range-all');
+ if (pageRangeAllBtn && pageStartEl && pageEndEl) {
+ pageRangeAllBtn.addEventListener('click', function() {
+ var lastPage = getLastPage();
+ pageStartEl.value = '1';
+ pageEndEl.value = String(lastPage);
+ updateStatus('已选择全页范围:1-' + lastPage, 'done');
+ });
+ }
+
+ var speedSelectEl = speedDiv.querySelector('#speed-select');
+ if (speedSelectEl) {
+ speedSelectEl.addEventListener('change', function() {
+ speedMode = speedConfig[speedSelectEl.value] ? speedSelectEl.value : 'fast';
+ scheduleCloudVaultSync();
+ });
+ }
}
if (!document.getElementById('magnet-status')) {
@@ -2629,6 +3270,7 @@
var restoreResult = await restoreProgressState();
var restored = !!(restoreResult && restoreResult.restored);
var restoredState = restoreResult ? restoreResult.state : null;
+ await refreshCloudStatusAndVault({ restoreProgress: !restored });
if (isListPage()) {
var resumePage = Number.isFinite(Number(restoredState && restoredState.resumeFromPage))
? Math.max(1, Number(restoredState.resumeFromPage))