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 = '
MAGNET LINKS
智能抓取 · 缓存加速 · 一键复制
搜索结果
关键词命中的磁力链接
0
我的收藏
持久保存的磁力链接
缓存总览
数据统计与快照管理
'; + panel.innerHTML = '
MAGNET LINKS
智能抓取 · 缓存加速 · 一键复制
搜索结果
关键词命中的磁力链接
0
我的收藏
持久保存的磁力链接
缓存总览
数据统计与快照管理
云同步账号
注册 / 登录 / 私有保险柜
状态加载中
'; 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))