// ==UserScript== // @name LinuxDo 收藏夹 // @namespace https://linux.do/ // @version 1.0.0 // @description 自定义收藏夹功能 // @author eamooooon // @match https://linux.do/* // @match https://linux.do/t/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'linux_do_favorites'; const FOLDERS_KEY = 'linux_do_folders'; const THEME_KEY = 'linux_do_theme'; const MODE_KEY = 'linux_do_mode'; const defaultFolders = ['默认收藏']; const themes = { 'orange': { name: '橙红色', color: '#e5573c' }, 'blue': { name: '蓝色', color: '#3b82f6' }, 'green': { name: '绿色', color: '#22c55e' }, 'purple': { name: '紫色', color: '#a855f7' }, 'pink': { name: '粉色', color: '#ec4899' }, 'teal': { name: '青色', color: '#14b8a6' } }; function getTheme() { return GM_getValue(THEME_KEY, 'orange'); } function saveTheme(theme) { GM_setValue(THEME_KEY, theme); } function getMode() { return GM_getValue(MODE_KEY, 'default'); } function saveMode(mode) { GM_setValue(MODE_KEY, mode); } function hexToRgba(hex, alpha) { const r = parseInt(hex.slice(1, 3), 16); const g = parseInt(hex.slice(3, 5), 16); const b = parseInt(hex.slice(5, 7), 16); return `rgba(${r}, ${g}, ${b}, ${alpha})`; } function applyTheme(theme) { const themeColor = themes[theme]?.color || themes.orange.color; document.documentElement.style.setProperty('--ld-theme-color', themeColor); document.documentElement.style.setProperty('--ld-theme-bg-light', hexToRgba(themeColor, 0.1)); document.documentElement.style.setProperty('--ld-theme-bg-dark', hexToRgba(themeColor, 0.2)); } function getFolders() { const saved = GM_getValue(FOLDERS_KEY, null); let folders = saved ? JSON.parse(saved) : [...defaultFolders]; if (!folders.includes('默认收藏')) { folders.unshift('默认收藏'); } return folders; } function saveFolders(folders) { GM_setValue(FOLDERS_KEY, JSON.stringify(folders)); } function getFavorites() { const saved = GM_getValue(STORAGE_KEY, null); return saved ? JSON.parse(saved) : {}; } function saveFavorites(favorites) { GM_setValue(STORAGE_KEY, JSON.stringify(favorites)); } function getTopicId() { const match = window.location.pathname.match(/\/t\/.*?\/(\d+)/); console.log('[LinuxDo Favorites] getTopicId:', match ? match[1] : null); return match ? match[1] : null; } function getPostId(postElement) { if (!postElement) return null; const postId = postElement.getAttribute('data-post-id'); const postNumber = postElement.getAttribute('data-post-number'); return postId ? { id: postId, number: postNumber } : null; } function getTopicInfo() { const topicId = getTopicId(); if (!topicId) return null; const titleEl = document.querySelector('.header-title .topic-link span span') || document.querySelector('#topic-title h1 .topic-link') || document.querySelector('.fancy-title') || document.querySelector('.header-title'); const title = titleEl ? titleEl.textContent.trim() : document.title; const categoryEl = document.querySelector('.badge-category__name') || document.querySelector('.category-name'); const category = categoryEl ? categoryEl.textContent.trim() : '未分类'; const tagsEls = document.querySelectorAll('.discourse-tag.box, .tag'); const tags = [...new Set(Array.from(tagsEls).map(t => t.textContent.trim()))]; console.log('[LinuxDo Favorites] getTopicInfo:', { id: topicId, title, category, tags }); return { id: topicId, title: title, url: window.location.href.split('?')[0], category: category, tags: tags, addedAt: Date.now() }; } function getPostInfo(postElement) { const topicId = getTopicId(); if (!topicId || !postElement) return null; const postInfo = getPostId(postElement); if (!postInfo) return null; const titleEl = document.querySelector('.header-title .topic-link span span') || document.querySelector('#topic-title h1 .topic-link') || document.querySelector('.fancy-title') || document.querySelector('.header-title'); const topicTitle = titleEl ? titleEl.textContent.trim() : document.title; const categoryEl = document.querySelector('.badge-category__name') || document.querySelector('.category-name'); const category = categoryEl ? categoryEl.textContent.trim() : '未分类'; const tagsEls = document.querySelectorAll('.discourse-tag.box, .tag'); const tags = [...new Set(Array.from(tagsEls).map(t => t.textContent.trim()))]; const cookedEl = postElement.querySelector('.cooked'); const postContent = cookedEl ? cookedEl.textContent.trim().substring(0, 50) : ''; const uniqueId = `${topicId}_${postInfo.id}`; return { id: uniqueId, topicId: topicId, postId: postInfo.id, postNumber: postInfo.number, title: `${topicTitle} - #${postInfo.number}`, url: `${window.location.pathname}/${postInfo.number}`, category: category, tags: tags, addedAt: Date.now(), isPost: true }; } GM_addStyle(` #ld-fav-btn { background: none; border: none; cursor: pointer; padding: 8px; color: var(--primary-high, #333); font-size: 18px; display: inline-flex; align-items: center; gap: 4px; opacity: 0.7; transition: opacity 0.2s; } #ld-fav-btn svg { width: 1.2em; height: 1.2em; } #ld-fav-btn:hover { opacity: 1; } #ld-fav-btn.favorited { color: var(--ld-theme-color, #e5573c); opacity: 1; } #ld-fav-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; justify-content: center; align-items: center; } #ld-fav-modal.show { display: flex; } .ld-fav-content { background: var(--secondary, #fff); color: var(--primary, #333); border-radius: 12px; width: 500px; max-height: 80vh; overflow: hidden; display: flex; flex-direction: column; } .ld-fav-header { padding: 16px 20px; border-bottom: 1px solid var(--primary-low, #eee); display: flex; justify-content: space-between; align-items: center; } .ld-fav-header h3 { margin: 0; font-size: 18px; } .ld-fav-close { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--primary, #333); } .ld-fav-tabs { display: flex; border-bottom: 1px solid var(--primary-low, #eee); overflow-x: auto; } .ld-fav-tab { padding: 10px 16px; cursor: pointer; white-space: nowrap; border-bottom: 2px solid transparent; font-size: 14px; color: var(--primary-medium, #666); } .ld-fav-tab.active { color: var(--ld-theme-color, #e5573c); border-bottom-color: var(--ld-theme-color, #e5573c); } .ld-fav-tab:hover { background: var(--highlight-low, #f5f5f5); } .ld-fav-body { flex: 1; overflow-y: auto; padding: 12px; } .ld-fav-item { padding: 10px 12px; border-radius: 8px; margin-bottom: 6px; display: flex; justify-content: space-between; align-items: center; background: var(--primary-low, #f8f8f8); } .ld-fav-item:hover { background: var(--highlight-low, #f0f0f0); } .ld-fav-item a { color: var(--primary, #333); text-decoration: none; flex: 1; font-size: 14px; line-height: 1.4; } .ld-fav-item a:hover { color: var(--ld-theme-color, #e5573c); } .ld-fav-item-meta { font-size: 12px; color: var(--primary-medium, #888); margin-top: 4px; } .ld-fav-remove { background: none; border: none; color: var(--danger, var(--ld-theme-color, #e5573c)); cursor: pointer; font-size: 16px; padding: 4px 8px; border-radius: 4px; opacity: 0; transition: opacity 0.2s; } .ld-fav-item:hover .ld-fav-remove { opacity: 1; } .ld-fav-footer { padding: 12px 16px; border-top: 1px solid var(--primary-low, #eee); display: flex; gap: 8px; } .ld-fav-footer button { padding: 8px 16px; border-radius: 6px; border: 1px solid var(--primary-low, #ddd); background: var(--secondary, #fff); color: var(--primary, #333); cursor: pointer; font-size: 13px; } .ld-fav-footer button:hover { background: var(--highlight-low, #f0f0f0); } .ld-fav-empty { text-align: center; padding: 40px 20px; color: var(--primary-medium, #888); } .ld-fav-dialog { display: none; position: absolute; background: var(--secondary, #fff); padding: 12px; border-radius: 8px; z-index: 10001; box-shadow: 0 4px 20px rgba(0,0,0,0.15); min-width: 200px; max-height: 300px; overflow-y: auto; border: 2px solid var(--primary-low, #ddd); } .ld-fav-dialog.show { display: block; } .ld-fav-folder-item { padding: 8px 12px; cursor: pointer; border-radius: 4px; display: flex; align-items: center; gap: 8px; color: var(--primary, #333); font-size: 14px; } .ld-fav-folder-item:hover { background: var(--highlight-low, #f0f0f0); } .ld-fav-folder-item.selected { background: var(--highlight-low, #e8e8e8); color: var(--ld-theme-color, #e5573c); } .ld-fav-new-folder { padding: 8px 12px; cursor: pointer; border-radius: 4px; display: flex; align-items: center; gap: 8px; color: var(--primary-medium, #888); font-size: 14px; border-top: 1px solid var(--primary-low, #eee); margin-top: 4px; } .ld-fav-new-folder:hover { background: var(--highlight-low, #f0f0f0); color: var(--primary, #333); } .ld-fav-dialog h4 { margin: 0 0 16px 0; font-size: 16px; } .ld-fav-dialog input, .ld-fav-dialog select { width: 100%; padding: 10px; border: 1px solid var(--primary-low, #ddd); border-radius: 6px; font-size: 14px; margin-bottom: 12px; background: var(--secondary, #fff); color: var(--primary, #333); box-sizing: border-box; } .ld-fav-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; } .ld-fav-dialog-actions button { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; } .ld-fav-btn-primary { background: var(--ld-theme-color, #e5573c); color: white; } .ld-fav-btn-secondary { background: var(--primary-low, #eee); color: var(--primary, #333); } .ld-fav-inline-btn { background: none; border: 1px dashed var(--primary-low, #ccc); color: var(--primary-medium, #888); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px; width: 100%; margin-top: 4px; } .ld-fav-inline-btn:hover { border-color: var(--ld-theme-color, #e5573c); color: var(--ld-theme-color, #e5573c); } #ld-fav-panel { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10002; justify-content: center; align-items: center; } #ld-fav-panel.show { display: flex; } .ld-fav-panel-close { position: absolute; top: 16px; right: 16px; background: none; border: none; font-size: 24px; cursor: pointer; color: var(--primary-medium, #888); padding: 8px; line-height: 1; z-index: 1; } .ld-fav-panel-close:hover { color: var(--primary, #333); } .ld-fav-panel-body { display: flex; flex: 1; overflow: hidden; background: var(--secondary, #fff); border-radius: 12px; width: 80%; max-width: 1000px; height: 80vh; box-shadow: 0 10px 40px rgba(0,0,0,0.2); flex-direction: row; } .ld-fav-panel-sidebar { width: 200px; min-width: 200px; border-right: 1px solid var(--primary-low, #eee); overflow-y: auto; padding: 8px 0; background: var(--primary-low, #f0f0f0); } .ld-fav-panel-sidebar::-webkit-scrollbar { width: 6px; } .ld-fav-panel-sidebar::-webkit-scrollbar-track { background: transparent; } .ld-fav-panel-sidebar::-webkit-scrollbar-thumb { background: var(--primary-low, #ccc); border-radius: 3px; } .ld-fav-panel-sidebar::-webkit-scrollbar-thumb:hover { background: var(--primary-medium, #999); } .ld-fav-panel-folder { display: flex; justify-content: space-between; align-items: center; padding: 10px 16px; cursor: pointer; color: var(--primary, #333); font-size: 14px; position: relative; } .ld-fav-panel-folder:hover { background: var(--highlight-low, #f0f0f0); } .ld-fav-panel-folder.active { background: var(--secondary, #fff); color: var(--ld-theme-color, #e5573c); font-weight: 600; } .ld-fav-panel-folder-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ld-fav-panel-folder-count { background: var(--primary-low, #eee); color: var(--primary-medium, #888); padding: 2px 8px; border-radius: 10px; font-size: 12px; margin-left: 8px; } .ld-fav-panel-folder-actions { display: none; gap: 4px; margin-left: 8px; } .ld-fav-panel-folder:hover .ld-fav-panel-folder-actions { display: flex; } .ld-fav-panel-folder-btn { background: none; border: none; cursor: pointer; padding: 2px 4px; color: var(--primary-medium, #888); font-size: 12px; border-radius: 3px; } .ld-fav-panel-folder-btn:hover { background: var(--primary-low, #eee); color: var(--primary, #333); } .ld-fav-panel-folder-btn.delete:hover { color: var(--ld-theme-color, #e5573c); } .ld-fav-panel-folder-drag-handle { cursor: grab; color: var(--primary-medium, #888); margin-right: 8px; font-size: 12px; } .ld-fav-panel-folder.dragging { opacity: 0.5; background: var(--highlight-low, #f0f0f0); } .ld-fav-panel-folder.drag-over { border-top: 2px solid var(--ld-theme-color, #e5573c); } .ld-fav-panel-add-folder { padding: 10px 16px; cursor: pointer; color: var(--primary-medium, #888); font-size: 14px; border-top: 1px solid var(--primary-low, #eee); margin-top: 8px; } .ld-fav-panel-add-folder:hover { background: var(--highlight-low, #f0f0f0); color: var(--primary, #333); } .ld-fav-panel-settings { padding: 10px 16px; cursor: pointer; color: var(--primary-medium, #888); font-size: 14px; border-top: 1px solid var(--primary-low, #eee); margin-top: auto; } .ld-fav-panel-settings:hover { background: var(--highlight-low, #f0f0f0); color: var(--primary, #333); } .ld-fav-settings-panel { display: none; padding: 12px 16px; border-top: 1px solid var(--primary-low, #eee); background: var(--primary-low, #f0f0f0); } .ld-fav-settings-panel.show { display: block; } .ld-fav-settings-title { font-size: 13px; color: var(--primary-medium, #888); margin-bottom: 10px; } .ld-fav-mode-options { display: flex; flex-direction: column; gap: 8px; margin-bottom: 4px; } .ld-fav-mode-option { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--primary, #333); cursor: pointer; } .ld-fav-mode-option input[type="radio"] { accent-color: var(--ld-theme-color, #e5573c); } .ld-fav-theme-options { display: flex; flex-wrap: wrap; gap: 8px; } .ld-fav-theme-option { width: 28px; height: 28px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; transition: transform 0.2s; } .ld-fav-theme-option:hover { transform: scale(1.1); } .ld-fav-theme-option.active { border-color: var(--primary, #333); box-shadow: 0 0 0 2px var(--secondary, #fff); } .ld-fav-panel-rename-input { width: 100%; padding: 4px 8px; border: 1px solid var(--ld-theme-color, #e5573c); border-radius: 4px; font-size: 14px; background: var(--secondary, #fff); color: var(--primary, #333); } .ld-fav-inline-dialog { display: none; position: absolute; background: var(--secondary, #fff); border: 2px solid var(--primary-low, #ddd); border-radius: 8px; padding: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 10002; min-width: 200px; } .ld-fav-inline-dialog.show { display: block; } .ld-fav-inline-dialog-title { font-size: 14px; color: var(--primary, #333); margin-bottom: 10px; } .ld-fav-inline-dialog-input { width: 100%; padding: 8px; border: 1px solid var(--primary-low, #ddd); border-radius: 4px; font-size: 14px; margin-bottom: 10px; box-sizing: border-box; } .ld-fav-inline-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; } .ld-fav-inline-dialog-btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; } .ld-fav-inline-dialog-btn.confirm { background: var(--ld-theme-color, #e5573c); color: white; } .ld-fav-inline-dialog-btn.cancel { background: var(--primary-low, #eee); color: var(--primary, #333); } .ld-fav-panel-content { flex: 1; overflow-y: auto; padding: 16px; } .ld-fav-panel-content::-webkit-scrollbar { width: 6px; } .ld-fav-panel-content::-webkit-scrollbar-track { background: transparent; } .ld-fav-panel-content::-webkit-scrollbar-thumb { background: var(--primary-low, #ccc); border-radius: 3px; } .ld-fav-panel-content::-webkit-scrollbar-thumb:hover { background: var(--primary-medium, #999); } .ld-fav-panel-search { margin-bottom: 12px; } .ld-fav-panel-search input { width: 100%; padding: 8px 12px; border: 1px solid var(--primary-low, #ddd); border-radius: 6px; font-size: 14px; background: var(--secondary, #fff); color: var(--primary, #333); box-sizing: border-box; outline: none; transition: border-color 0.2s; } .ld-fav-panel-search input:focus { border-color: var(--ld-theme-color, #e5573c); } .ld-fav-panel-search input::placeholder { color: var(--primary-medium, #999); } .ld-fav-panel-empty { text-align: center; padding: 40px 20px; color: var(--primary-medium, #888); } .ld-fav-panel-item { padding: 12px; border-radius: 8px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; background: var(--primary-low, #f8f8f8); position: relative; gap: 12px; } .ld-fav-panel-item:hover { background: var(--highlight-low, #f0f0f0); } .ld-fav-panel-item-content { flex: 1; min-width: 0; } .ld-fav-panel-item-title { color: var(--primary, #333); text-decoration: none; font-size: 14px; line-height: 1.4; display: block; margin-bottom: 6px; } .ld-fav-panel-item-title:hover { color: var(--ld-theme-color, #e5573c); } .ld-fav-panel-item-meta { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } .ld-fav-panel-item-category { font-size: 12px; color: var(--primary, #333); background: var(--primary-low, #eee); padding: 2px 8px; border-radius: 4px; border: 1px solid var(--primary-low, #ddd); } .ld-fav-panel-item-tags { font-size: 12px; color: var(--primary-medium, #888); background: var(--primary-low, #eee); padding: 2px 8px; border-radius: 4px; } .ld-fav-panel-item-folder { font-size: 12px; color: var(--ld-theme-color, #e5573c); background: var(--ld-theme-bg-light, rgba(229, 87, 60, 0.1)); padding: 2px 8px; border-radius: 4px; } .ld-fav-panel-item-date { font-size: 12px; color: var(--primary-medium, #888); } .ld-fav-panel-item-remove { background: none; border: none; color: var(--danger, var(--ld-theme-color, #e5573c)); cursor: pointer; font-size: 18px; padding: 4px 8px; border-radius: 4px; opacity: 0; transition: opacity 0.2s; margin-left: 8px; } .ld-fav-panel-item:hover .ld-fav-panel-item-remove { opacity: 1; } .ld-fav-panel-item.dragging { opacity: 0.5; background: var(--highlight-low, #f0f0f0); } .ld-fav-panel-folder.drag-over { background: var(--ld-theme-bg-dark, rgba(229, 87, 60, 0.2)); border: 2px dashed var(--ld-theme-color, #e5573c); } `); function createModal() { const modal = document.createElement('div'); modal.id = 'ld-fav-modal'; modal.innerHTML = `

LinuxDo 收藏夹

`; document.body.appendChild(modal); const addDialog = document.createElement('div'); addDialog.id = 'ld-fav-add-dialog'; addDialog.className = 'ld-fav-dialog'; addDialog.innerHTML = `
+ 新建收藏夹
`; document.body.appendChild(addDialog); const newFolderDialog = document.createElement('div'); newFolderDialog.id = 'ld-fav-new-folder-dialog'; newFolderDialog.className = 'ld-fav-dialog'; newFolderDialog.innerHTML = `
`; document.body.appendChild(newFolderDialog); const manageDialog = document.createElement('div'); manageDialog.id = 'ld-fav-manage-dialog'; manageDialog.className = 'ld-fav-dialog'; manageDialog.innerHTML = `

管理收藏夹

`; document.body.appendChild(manageDialog); createFavoritesPanel(); return modal; } let currentTab = '全部'; let pendingFavorite = null; let isHoverDialog = false; function renderTabs() { const tabs = document.getElementById('ld-fav-tabs'); const folders = getFolders(); const allTabs = ['全部', '未分类', ...folders]; tabs.innerHTML = allTabs.map(tab => `
${tab}
`).join(''); tabs.querySelectorAll('.ld-fav-tab').forEach(el => { el.addEventListener('click', () => { currentTab = el.dataset.tab; renderTabs(); renderFavorites(); }); }); } function renderFavorites() { const body = document.getElementById('ld-fav-body'); const favorites = getFavorites(); const items = Object.values(favorites); let filtered = items; if (currentTab === '未分类') { filtered = items.filter(i => !i.folder || i.folder === '未分类'); } else if (currentTab !== '全部') { filtered = items.filter(i => i.folder === currentTab); } filtered.sort((a, b) => b.addedAt - a.addedAt); if (filtered.length === 0) { body.innerHTML = `
暂无收藏
`; return; } body.innerHTML = filtered.map(item => `
${item.title}
${item.folder ? `[${item.folder}] ` : ''} ${item.category || ''} ${item.tags && item.tags.length ? ' · ' + [...new Set(item.tags)].join(', ') : ''}
`).join(''); body.querySelectorAll('.ld-fav-remove').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.target.dataset.id; const favs = getFavorites(); delete favs[id]; saveFavorites(favs); renderFavorites(); updateButton(); }); }); } function showModal() { const modal = document.getElementById('ld-fav-modal'); modal.classList.add('show'); renderTabs(); renderFavorites(); } function hideModal() { document.getElementById('ld-fav-modal').classList.remove('show'); } function showAddDialog(topicInfo) { console.log('[LinuxDo Favorites] showAddDialog 被调用,topicInfo:', topicInfo); pendingFavorite = topicInfo; isHoverDialog = false; const dialog = document.getElementById('ld-fav-add-dialog'); const btn = document.getElementById('ld-fav-btn'); const folderList = document.getElementById('ld-fav-folder-list'); let folders = getFolders(); const mode = getMode(); let defaultFolder = '默认收藏'; if (mode === 'auto' && topicInfo.category) { const matchFolder = folders.find(f => f.includes(topicInfo.category) || topicInfo.category.includes(f) ); if (matchFolder) { defaultFolder = matchFolder; } else { folders.push(topicInfo.category); saveFolders(folders); defaultFolder = topicInfo.category; } } folderList.innerHTML = folders.map(f => `
${f}
`).join(''); folderList.querySelectorAll('.ld-fav-folder-item').forEach(item => { item.addEventListener('click', () => { const folder = item.dataset.folder; const favorites = getFavorites(); favorites[topicInfo.id] = { ...topicInfo, folder: folder }; saveFavorites(favorites); pendingFavorite = null; hideAddDialog(); updateButton(); }); }); document.getElementById('ld-fav-new-folder-btn').addEventListener('click', () => { hideAddDialog(true); showNewFolderDialog(); }); if (btn) { const rect = btn.getBoundingClientRect(); dialog.style.position = 'fixed'; dialog.style.top = (rect.bottom + 5) + 'px'; dialog.style.right = (window.innerWidth - rect.right) + 'px'; dialog.style.left = 'auto'; } dialog.classList.add('show'); console.log('[LinuxDo Favorites] 对话框已显示'); } function hideAddDialog(skipAutoSave) { const dialog = document.getElementById('ld-fav-add-dialog'); if (!skipAutoSave && !isHoverDialog && dialog.classList.contains('show') && pendingFavorite) { const mode = getMode(); let folder = '默认收藏'; if (mode === 'auto' && pendingFavorite.category) { const folders = getFolders(); const matchFolder = folders.find(f => f.includes(pendingFavorite.category) || pendingFavorite.category.includes(f) ); if (matchFolder) { folder = matchFolder; } else { folders.push(pendingFavorite.category); saveFolders(folders); folder = pendingFavorite.category; } } const favorites = getFavorites(); favorites[pendingFavorite.id] = { ...pendingFavorite, folder: folder }; saveFavorites(favorites); updateButton(); } dialog.classList.remove('show'); pendingFavorite = null; isHoverDialog = false; } function showChangeFolderDialog(favItem) { isHoverDialog = true; const dialog = document.getElementById('ld-fav-add-dialog'); const btn = document.getElementById('ld-fav-btn'); const folderList = document.getElementById('ld-fav-folder-list'); const folders = getFolders(); folderList.innerHTML = folders.map(f => `
${f}
`).join(''); folderList.querySelectorAll('.ld-fav-folder-item').forEach(item => { item.addEventListener('click', () => { const folder = item.dataset.folder; const favorites = getFavorites(); if (favorites[favItem.id]) { favorites[favItem.id].folder = folder; saveFavorites(favorites); hideAddDialog(true); updateButton(); } }); }); if (btn) { const rect = btn.getBoundingClientRect(); dialog.style.position = 'fixed'; dialog.style.top = (rect.bottom + 5) + 'px'; dialog.style.right = (window.innerWidth - rect.right) + 'px'; dialog.style.left = 'auto'; } dialog.classList.add('show'); } function showNewFolderDialog() { const dialog = document.getElementById('ld-fav-new-folder-dialog'); const btn = document.getElementById('ld-fav-btn'); const input = document.getElementById('ld-fav-new-folder-name'); input.value = ''; if (btn) { const rect = btn.getBoundingClientRect(); dialog.style.position = 'fixed'; dialog.style.top = (rect.bottom + 5) + 'px'; dialog.style.right = (window.innerWidth - rect.right) + 'px'; dialog.style.left = 'auto'; } dialog.classList.add('show'); input.focus(); } function hideNewFolderDialog() { document.getElementById('ld-fav-new-folder-dialog').classList.remove('show'); } function createNewFolder() { const input = document.getElementById('ld-fav-new-folder-name'); const name = input.value.trim(); if (!name) return; const folders = getFolders(); if (folders.includes(name)) { alert('收藏夹已存在'); return; } folders.push(name); saveFolders(folders); hideNewFolderDialog(); if (pendingFavorite) { const favorites = getFavorites(); favorites[pendingFavorite.id] = { ...pendingFavorite, folder: name }; saveFavorites(favorites); pendingFavorite = null; updateButton(); } renderTabs(); } function showManageDialog() { const dialog = document.getElementById('ld-fav-manage-dialog'); const list = document.getElementById('ld-fav-manage-list'); const folders = getFolders(); const favorites = getFavorites(); list.innerHTML = folders.map(f => { const count = Object.values(favorites).filter(i => i.folder === f).length; const isDefault = defaultFolders.includes(f); return `
${f} (${count}) ${isDefault ? '' : ``}
`; }).join(''); list.querySelectorAll('.ld-fav-remove').forEach(btn => { btn.addEventListener('click', (e) => { const folder = e.target.dataset.folder; if (confirm(`确定删除收藏夹"${folder}"?其中的帖子将移到未分类。`)) { const allFolders = getFolders().filter(f => f !== folder); saveFolders(allFolders); const favs = getFavorites(); Object.values(favs).forEach(fav => { if (fav.folder === folder) fav.folder = ''; }); saveFavorites(favs); showManageDialog(); renderTabs(); } }); }); dialog.classList.add('show'); } function hideManageDialog() { document.getElementById('ld-fav-manage-dialog').classList.remove('show'); } function updateButton() { let btn = document.getElementById('ld-fav-btn'); if (!btn) { btn = insertFavButton(); if (!btn) return; } const topicId = getTopicId(); if (!topicId) { btn.style.display = 'none'; return; } btn.style.display = 'inline-flex'; const favorites = getFavorites(); if (favorites[topicId]) { btn.classList.add('favorited'); btn.innerHTML = ''; } else { btn.classList.remove('favorited'); btn.innerHTML = ''; } } function showConfirmDialog(title, onConfirm) { const dialog = document.getElementById('ld-fav-confirm-dialog'); const titleEl = document.getElementById('ld-fav-confirm-title'); const cancelBtn = document.getElementById('ld-fav-confirm-cancel'); const okBtn = document.getElementById('ld-fav-confirm-ok'); titleEl.textContent = title; dialog.classList.add('show'); const hideDialog = () => { dialog.classList.remove('show'); cancelBtn.removeEventListener('click', hideDialog); okBtn.removeEventListener('click', handleConfirm); }; const handleConfirm = () => { hideDialog(); onConfirm(); }; cancelBtn.addEventListener('click', hideDialog); okBtn.addEventListener('click', handleConfirm); } function showInputDialog(title, defaultValue, onConfirm) { const dialog = document.getElementById('ld-fav-input-dialog'); const titleEl = document.getElementById('ld-fav-input-title'); const input = document.getElementById('ld-fav-input-field'); const cancelBtn = document.getElementById('ld-fav-input-cancel'); const okBtn = document.getElementById('ld-fav-input-ok'); titleEl.textContent = title; input.value = defaultValue || ''; dialog.classList.add('show'); input.focus(); input.select(); const hideDialog = () => { dialog.classList.remove('show'); cancelBtn.removeEventListener('click', hideDialog); okBtn.removeEventListener('click', handleConfirm); input.removeEventListener('keydown', handleKeydown); }; const handleConfirm = () => { const value = input.value.trim(); if (value) { hideDialog(); onConfirm(value); } }; const handleKeydown = (e) => { if (e.key === 'Enter') handleConfirm(); if (e.key === 'Escape') hideDialog(); }; cancelBtn.addEventListener('click', hideDialog); okBtn.addEventListener('click', handleConfirm); input.addEventListener('keydown', handleKeydown); } function createFavoritesPanel() { const panel = document.createElement('div'); panel.id = 'ld-fav-panel'; panel.innerHTML = `
`; document.body.appendChild(panel); document.getElementById('ld-fav-panel-close').addEventListener('click', hideFavoritesPanel); panel.addEventListener('click', (e) => { if (e.target === panel) hideFavoritesPanel(); }); return panel; } function renderPanelSidebar() { const sidebar = document.getElementById('ld-fav-panel-sidebar'); const folders = getFolders(); const favorites = getFavorites(); const allCount = Object.keys(favorites).length; let html = `
全部 ${allCount}
`; folders.forEach((folder) => { const count = Object.values(favorites).filter(f => f.folder === folder).length; const isDefault = defaultFolders.includes(folder); html += `
${folder} ${count}
${!isDefault ? `` : ''}
`; }); html += `
+ 新建收藏夹
⚙ 设置
收藏模式
主题颜色
`; sidebar.innerHTML = html; sidebar.querySelectorAll('.ld-fav-panel-folder').forEach(item => { item.addEventListener('click', (e) => { if (e.target.closest('.ld-fav-panel-folder-btn')) return; sidebar.querySelectorAll('.ld-fav-panel-folder').forEach(i => i.classList.remove('active')); item.classList.add('active'); const content = document.getElementById('ld-fav-panel-content'); content.innerHTML = ''; renderPanelContent(item.dataset.folder, ''); }); }); const folderItems = sidebar.querySelectorAll('.ld-fav-panel-folder[draggable="true"]'); let draggedItem = null; folderItems.forEach(item => { item.addEventListener('dragstart', (e) => { draggedItem = item; item.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; }); item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedItem = null; folderItems.forEach(i => i.classList.remove('drag-over')); const newOrder = Array.from(sidebar.querySelectorAll('.ld-fav-panel-folder[draggable="true"]')) .map(i => i.dataset.folder); saveFolders(newOrder); }); item.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; item.classList.add('drag-over'); }); item.addEventListener('dragleave', () => { item.classList.remove('drag-over'); }); item.addEventListener('drop', (e) => { e.preventDefault(); item.classList.remove('drag-over'); const favId = e.dataTransfer.getData('text/plain'); if (favId) { const favorites = getFavorites(); if (favorites[favId]) { favorites[favId].folder = item.dataset.folder; saveFavorites(favorites); renderPanelContent(document.querySelector('.ld-fav-panel-folder.active')?.dataset.folder || '全部'); renderPanelSidebar(); } return; } if (draggedItem && draggedItem !== item) { const allItems = Array.from(sidebar.querySelectorAll('.ld-fav-panel-folder[draggable="true"]')); const draggedIndex = allItems.indexOf(draggedItem); const targetIndex = allItems.indexOf(item); if (draggedIndex < targetIndex) { item.after(draggedItem); } else { item.before(draggedItem); } } }); }); sidebar.querySelectorAll('.ld-fav-panel-folder-btn.rename').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const folder = btn.dataset.folder; const folderItem = btn.closest('.ld-fav-panel-folder'); const nameSpan = folderItem.querySelector('.ld-fav-panel-folder-name'); const input = document.createElement('input'); input.className = 'ld-fav-panel-rename-input'; input.value = folder; nameSpan.replaceWith(input); input.focus(); input.select(); const saveRename = () => { const newName = input.value.trim(); if (newName && newName !== folder) { const folders = getFolders(); const index = folders.indexOf(folder); if (index !== -1) { folders[index] = newName; saveFolders(folders); const favs = getFavorites(); Object.values(favs).forEach(fav => { if (fav.folder === folder) fav.folder = newName; }); saveFavorites(favs); } } renderPanelSidebar(); const activeFolder = document.querySelector('.ld-fav-panel-folder.active'); renderPanelContent(activeFolder ? activeFolder.dataset.folder : '全部'); }; input.addEventListener('blur', saveRename); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') saveRename(); if (e.key === 'Escape') renderPanelSidebar(); }); }); }); sidebar.querySelectorAll('.ld-fav-panel-folder-btn.delete').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const folder = btn.dataset.folder; const folderItem = btn.closest('.ld-fav-panel-folder'); const rect = folderItem.getBoundingClientRect(); const dialog = document.getElementById('ld-fav-confirm-dialog'); dialog.style.top = (rect.bottom + 5) + 'px'; dialog.style.left = rect.left + 'px'; showConfirmDialog(`确定删除收藏夹"${folder}"?`, () => { const folders = getFolders().filter(f => f !== folder); saveFolders(folders); const favs = getFavorites(); Object.values(favs).forEach(fav => { if (fav.folder === folder) fav.folder = '默认收藏'; }); saveFavorites(favs); renderPanelSidebar(); const content = document.getElementById('ld-fav-panel-content'); content.innerHTML = ''; renderPanelContent('全部', ''); }); }); }); document.getElementById('ld-fav-panel-add-folder').addEventListener('click', (e) => { const rect = e.target.getBoundingClientRect(); const dialog = document.getElementById('ld-fav-input-dialog'); dialog.style.top = (rect.bottom + 5) + 'px'; dialog.style.left = rect.left + 'px'; showInputDialog('请输入收藏夹名称:', '', (name) => { const folders = getFolders(); if (folders.includes(name)) { alert('收藏夹已存在'); return; } folders.push(name); saveFolders(folders); renderPanelSidebar(); }); }); const settingsBtn = document.getElementById('ld-fav-panel-settings'); const settingsPanel = document.getElementById('ld-fav-settings-panel'); const themeOptions = document.getElementById('ld-fav-theme-options'); const currentTheme = getTheme(); themeOptions.innerHTML = Object.entries(themes).map(([key, theme]) => `
`).join(''); settingsBtn.addEventListener('click', () => { settingsPanel.classList.toggle('show'); }); themeOptions.querySelectorAll('.ld-fav-theme-option').forEach(option => { option.addEventListener('click', () => { const theme = option.dataset.theme; saveTheme(theme); applyTheme(theme); themeOptions.querySelectorAll('.ld-fav-theme-option').forEach(o => o.classList.remove('active')); option.classList.add('active'); }); }); const modeOptions = document.querySelectorAll('#ld-fav-mode-options input[type="radio"]'); modeOptions.forEach(option => { option.addEventListener('change', () => { saveMode(option.value); }); }); } function formatDate(timestamp) { const date = new Date(timestamp); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } function renderPanelContent(folder, searchQuery) { const content = document.getElementById('ld-fav-panel-content'); const searchInput = document.getElementById('ld-fav-search-input'); const currentQuery = searchQuery !== undefined ? searchQuery : (searchInput ? searchInput.value : ''); const favorites = getFavorites(); let items = Object.values(favorites); if (folder !== '全部') { items = items.filter(f => f.folder === folder); } if (currentQuery) { const query = currentQuery.toLowerCase(); items = items.filter(item => item.title.toLowerCase().includes(query) || (item.category && item.category.toLowerCase().includes(query)) || (item.tags && item.tags.some(tag => tag.toLowerCase().includes(query))) ); } items.sort((a, b) => b.addedAt - a.addedAt); if (!searchInput) { let html = `
`; content.innerHTML = html; bindSearchEvent(folder); document.getElementById('ld-fav-search-input').focus(); } const listContainer = document.getElementById('ld-fav-panel-list'); if (items.length === 0) { listContainer.innerHTML = `
${currentQuery ? '没有找到匹配的收藏' : '暂无收藏'}
`; return; } listContainer.innerHTML = items.map(item => `
${item.title}
${item.folder || '默认收藏'} ${item.category || '未分类'} ${item.tags && item.tags.length > 0 ? `${[...new Set(item.tags)].join(', ')}` : ''}
${formatDate(item.addedAt)}
`).join(''); const contentItems = content.querySelectorAll('.ld-fav-panel-item'); let draggedItem = null; contentItems.forEach(item => { item.addEventListener('dragstart', (e) => { draggedItem = item; item.classList.add('dragging'); e.dataTransfer.setData('text/plain', item.dataset.id); e.dataTransfer.effectAllowed = 'move'; }); item.addEventListener('dragend', () => { item.classList.remove('dragging'); draggedItem = null; }); }); content.querySelectorAll('.ld-fav-panel-item-remove').forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const id = btn.dataset.id; const favorites = getFavorites(); delete favorites[id]; saveFavorites(favorites); const activeFolder = document.querySelector('.ld-fav-panel-folder.active'); renderPanelContent(activeFolder ? activeFolder.dataset.folder : '全部'); renderPanelSidebar(); updateButton(); }); }); content.querySelectorAll('.ld-fav-panel-item-title').forEach(link => { link.addEventListener('click', (e) => { const postId = link.getAttribute('data-post-id'); if (postId) { e.preventDefault(); hideFavoritesPanel(); const postElement = document.querySelector(`article[data-post-id="${postId}"]`); if (postElement) { postElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); postElement.style.transition = 'background-color 0.3s'; postElement.style.backgroundColor = 'var(--ld-theme-bg-light, rgba(229, 87, 60, 0.1))'; setTimeout(() => { postElement.style.backgroundColor = ''; }, 2000); } else { window.location.href = link.href; } } }); }); } function bindSearchEvent(folder) { const searchInput = document.getElementById('ld-fav-search-input'); if (!searchInput) return; let searchTimer = null; searchInput.addEventListener('input', () => { if (searchTimer) clearTimeout(searchTimer); searchTimer = setTimeout(() => { renderPanelContent(folder); }, 300); }); } function showFavoritesPanel() { const panel = document.getElementById('ld-fav-panel'); if (!panel) return; const content = document.getElementById('ld-fav-panel-content'); content.innerHTML = ''; renderPanelSidebar(); renderPanelContent('全部', ''); panel.classList.add('show'); } function hideFavoritesPanel() { const panel = document.getElementById('ld-fav-panel'); if (panel) panel.classList.remove('show'); } function insertSidebarButton() { const existingBtn = document.getElementById('ld-fav-sidebar-btn'); if (existingBtn) return; const sidebarList = document.querySelector('#sidebar-section-content-community'); if (!sidebarList) return; const li = document.createElement('li'); li.className = 'sidebar-section-link-wrapper'; li.setAttribute('data-list-item-name', 'favorites'); li.innerHTML = ` 收藏 `; const moreBtn = sidebarList.querySelector('.sidebar-more-section-trigger'); if (moreBtn) { sidebarList.insertBefore(li, moreBtn.parentElement); } else { sidebarList.appendChild(li); } li.querySelector('a').addEventListener('click', (e) => { e.preventDefault(); showFavoritesPanel(); }); } function insertFavButton() { const existingBtn = document.getElementById('ld-fav-btn'); if (existingBtn) existingBtn.remove(); const btn = document.createElement('button'); btn.id = 'ld-fav-btn'; const topicControls = document.querySelector('.topic-body .post-controls') || document.querySelector('.post-controls') || document.querySelector('.topic-footer-main-buttons') || document.querySelector('.topic-footer-buttons') || document.querySelector('.post-menu-area'); if (topicControls) { topicControls.appendChild(btn); } else { return null; } btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const topicId = getTopicId(); if (!topicId) return; const favorites = getFavorites(); if (favorites[topicId]) { delete favorites[topicId]; saveFavorites(favorites); hideAddDialog(true); updateButton(); } else { const topicInfo = getTopicInfo(); if (topicInfo) { showAddDialog(topicInfo); } } }); let hoverTimer = null; btn.addEventListener('mouseenter', () => { const topicId = getTopicId(); if (!topicId) return; const favorites = getFavorites(); if (!favorites[topicId]) return; if (btn._hideTimer) { clearTimeout(btn._hideTimer); btn._hideTimer = null; } hoverTimer = setTimeout(() => { showChangeFolderDialog(favorites[topicId]); }, 500); }); btn.addEventListener('mouseleave', () => { if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } btn._hideTimer = setTimeout(() => { const dialog = document.getElementById('ld-fav-add-dialog'); if (!dialog.matches(':hover')) { hideAddDialog(isHoverDialog); } }, 200); }); insertPostFavButtons(); return btn; } function insertPostFavButtons() { return; } function init() { const modal = createModal(); insertFavButton(); insertSidebarButton(); applyTheme(getTheme()); document.getElementById('ld-fav-close').addEventListener('click', hideModal); document.getElementById('ld-fav-add-folder').addEventListener('click', showNewFolderDialog); document.getElementById('ld-fav-manage').addEventListener('click', showManageDialog); document.getElementById('ld-fav-new-folder-cancel').addEventListener('click', hideNewFolderDialog); document.getElementById('ld-fav-new-folder-confirm').addEventListener('click', createNewFolder); document.getElementById('ld-fav-manage-close').addEventListener('click', hideManageDialog); modal.addEventListener('click', (e) => { if (e.target === modal) hideModal(); }); document.addEventListener('click', (e) => { const addDialog = document.getElementById('ld-fav-add-dialog'); const newFolderDialog = document.getElementById('ld-fav-new-folder-dialog'); const btn = document.getElementById('ld-fav-btn'); if (!addDialog.contains(e.target) && !btn.contains(e.target) && !newFolderDialog.contains(e.target)) { hideAddDialog(); hideNewFolderDialog(); } }); window.addEventListener('scroll', () => { const addDialog = document.getElementById('ld-fav-add-dialog'); if (addDialog.classList.contains('show')) { hideAddDialog(); } }); const addDialog = document.getElementById('ld-fav-add-dialog'); addDialog.addEventListener('mouseenter', () => { const btn = document.getElementById('ld-fav-btn'); if (btn && btn._hideTimer) { clearTimeout(btn._hideTimer); btn._hideTimer = null; } }); addDialog.addEventListener('mouseleave', () => { const btn = document.getElementById('ld-fav-btn'); if (btn) { btn._hideTimer = setTimeout(() => { hideAddDialog(isHoverDialog); }, 200); } }); updateButton(); let lastUrl = ''; const observer = new MutationObserver(() => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; setTimeout(updateButton, 500); setTimeout(insertSidebarButton, 500); setTimeout(insertPostFavButtons, 500); } else { setTimeout(insertPostFavButtons, 100); } }); observer.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();