// ==UserScript== // @name 更好的书签 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 一个功能强大的书签管理工具,支持双栏显示、搜索等功能 // @author xjy666a // @match *://*/* // @match file://*/* // @include * // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_registerMenuCommand // @run-at document-idle // @priority 1 // @license MIT // @icon https://youke1.picui.cn/s1/2025/11/08/690efd26c73bb.png // ==/UserScript== /* MIT License Copyright (c) 2025 xjy666a Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { 'use strict'; // 创建书签管理器 class BookmarkManager { constructor() { this.bookmarks = []; this.filteredBookmarks = []; this.currentView = 'grid'; // grid 或 list this.searchQuery = ''; this.isVisible = false; this.shadowRoot = null; this.selectedBookmarks = new Set(); // 选中的书签URL集合 this.batchMode = false; // 是否处于批量选择模式 this.storageListener = null; // 存储变化监听器 this.init(); } init() { // 创建主界面 this.createUI(); // 绑定事件 this.bindEvents(); // 从存储加载数据(使用 GM_getValue,可在所有网站间共享) this.loadFromStorage(); // 监听存储变化,实现跨标签页同步 this.setupStorageListener(); // 添加页面关闭前保存 this.setupAutoSave(); } createUI() { // 创建主容器 const container = document.createElement('div'); container.id = 'better-bookmarks-container'; // 创建 Shadow DOM 来隔离样式 this.shadowRoot = container.attachShadow({ mode: 'open' }); // 在 Shadow DOM 中创建内容 this.shadowRoot.innerHTML = `

更好的书签

还没有书签,请先导入书签文件、添加书签或将当前访问页面保存为书签

`; // 添加样式到 Shadow DOM this.addStyles(); // 插入到页面 document.body.appendChild(container); } addStyles() { const style = document.createElement('style'); style.textContent = ` /* CSS Reset for Shadow DOM */ * { box-sizing: border-box; margin: 0; padding: 0; } :host { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 999999; display: none; } :host(.visible) { display: block; } .bb-wrapper { width: 100%; height: 100%; background: #f5f5f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; overflow-y: auto; } .bb-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px 30px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .bb-header h1 { margin: 0; font-size: 24px; font-weight: 600; } .bb-controls { display: flex; gap: 10px; } .bb-btn { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.3s; } .bb-btn-primary { background: white; color: #667eea; } .bb-btn-primary:hover { background: #f0f0f0; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .bb-btn:not(.bb-btn-primary) { background: rgba(255,255,255,0.2); color: white; } .bb-btn:not(.bb-btn-primary):hover { background: rgba(255,255,255,0.3); } #bb-batch-mode-btn { background: #f0f0f0; color: #333; border: 2px solid #e0e0e0; } #bb-batch-mode-btn:hover { background: #e0e0e0; border-color: #d0d0d0; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .bb-toolbar { background: white; padding: 15px 30px; display: flex; gap: 15px; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); flex-wrap: wrap; } .bb-toolbar .bb-btn:not(.bb-btn-danger) { background: #f0f0f0; color: #333; border: 2px solid #e0e0e0; } .bb-toolbar .bb-btn:not(.bb-btn-danger):hover { background: #e0e0e0; border-color: #d0d0d0; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .bb-toolbar .bb-btn-danger { background: #ff4444 !important; color: white !important; border: 2px solid #ff4444 !important; } .bb-toolbar .bb-btn-danger:hover { background: #ff6666 !important; border-color: #ff6666 !important; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 68, 68, 0.3); } .bb-search { flex: 1; min-width: 200px; position: relative; display: flex; align-items: center; } .bb-search-input { width: 100%; padding: 10px 40px 10px 15px; border: 2px solid #e0e0e0; border-radius: 6px; font-size: 14px; transition: border-color 0.3s; } .bb-search-input:focus { outline: none; border-color: #667eea; } .bb-btn-icon { width: 36px; height: 36px; border: 2px solid #e0e0e0; background: #e0e0e0; border-radius: 6px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; transition: all 0.3s; } .bb-btn-icon:hover { border-color: #667eea; color: #667eea; } .bb-btn-icon.active { background: #667eea; border-color: #667eea; color: white; } #bb-close-btn { background: rgba(255,255,255,0.2); border: none; color: white; font-size: 20px; width: 36px; height: 36px; } #bb-close-btn:hover { background: rgba(255,255,255,0.3); } .bb-view-toggle { display: flex; gap: 5px; } .bb-batch-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .bb-selected-count { margin-left: 10px; color: #667eea; font-weight: 600; font-size: 14px; } .bb-btn-danger { background: #ff4444; color: white; } .bb-btn-danger:hover { background: #ff6666; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 68, 68, 0.3); } .bb-context-menu { background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); padding: 8px 0; min-width: 150px; z-index: 1000001; } .bb-context-menu-item { padding: 10px 20px; cursor: pointer; font-size: 14px; color: #333; transition: background 0.2s; } .bb-context-menu-item:hover { background: #f5f5f5; } .bb-context-menu-item.bb-context-menu-danger { color: #ff4444; } .bb-context-menu-item.bb-context-menu-danger:hover { background: #ffe0e0; } .bb-content { padding: 30px; max-width: 1400px; margin: 0 auto; } .bb-empty-state { text-align: center; padding: 60px 20px; color: #999; } .bb-empty-state p { font-size: 16px; margin-bottom: 20px; } /* 网格视图 */ .bb-grid-view { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; } .bb-bookmark-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s; cursor: pointer; border: 2px solid transparent; position: relative; } .bb-bookmark-card:hover { transform: translateY(-4px); box-shadow: 0 4px 16px rgba(0,0,0,0.15); border-color: #667eea; } .bb-bookmark-card.pinned { border-color: #ffd700; background: linear-gradient(135deg, #fffef0 0%, #ffffff 100%); } .bb-bookmark-card.selected { border-color: #667eea; background: #f0f4ff; } .bb-bookmark-card .bb-bookmark-checkbox { position: absolute; top: 10px; left: 10px; width: 20px; height: 20px; cursor: pointer; z-index: 11; accent-color: #667eea; } .bb-pin-btn { position: absolute; top: 10px; right: 10px; background: rgba(255,255,255,0.9); border: 2px solid #e0e0e0; border-radius: 6px; width: 32px; height: 32px; cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; transition: all 0.3s; z-index: 10; padding: 0; } .bb-pin-btn:hover { background: #fff; border-color: #667eea; transform: scale(1.1); } .bb-bookmark-card.pinned .bb-pin-btn { border-color: #ffd700; background: #fffef0; } .bb-bookmark-icon { width: 48px; height: 48px; border-radius: 8px; margin-bottom: 12px; object-fit: cover; } .bb-bookmark-title { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 8px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .bb-bookmark-url { font-size: 12px; color: #999; margin-bottom: 8px; word-break: break-all; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; overflow: hidden; } /* 列表视图 */ .bb-list-view { display: flex; flex-direction: column; gap: 10px; } .bb-bookmark-item { background: white; border-radius: 8px; padding: 15px 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.1); transition: all 0.3s; cursor: pointer; display: flex; align-items: center; gap: 15px; border: 2px solid transparent; position: relative; } .bb-bookmark-item:hover { transform: translateX(4px); box-shadow: 0 2px 8px rgba(0,0,0,0.15); border-color: #667eea; } .bb-bookmark-item.pinned { border-color: #ffd700; background: linear-gradient(135deg, #fffef0 0%, #ffffff 100%); } .bb-bookmark-item.selected { border-color: #667eea; background: #f0f4ff; } .bb-bookmark-item .bb-bookmark-checkbox { width: 18px; height: 18px; cursor: pointer; accent-color: #667eea; flex-shrink: 0; } .bb-bookmark-item .bb-pin-btn { background: rgba(255,255,255,0.9); border: 2px solid #e0e0e0; border-radius: 6px; width: 28px; height: 28px; cursor: pointer; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: all 0.3s; padding: 0; flex-shrink: 0; } .bb-bookmark-item .bb-pin-btn:hover { background: #fff; border-color: #667eea; transform: scale(1.1); } .bb-bookmark-item.pinned .bb-pin-btn { border-color: #ffd700; background: #fffef0; } .bb-bookmark-item-icon { width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; object-fit: cover; } .bb-bookmark-item-info { flex: 1; min-width: 0; } .bb-bookmark-item-title { font-size: 15px; font-weight: 600; color: #333; margin-bottom: 4px; } .bb-bookmark-item-url { font-size: 12px; color: #999; word-break: break-all; } /* 高亮搜索关键词 */ .bb-highlight { background: #fff3cd; padding: 2px 4px; border-radius: 3px; } /* 添加书签对话框 */ .bb-dialog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000000; } .bb-dialog { background: white; border-radius: 8px; padding: 24px; min-width: 400px; max-width: 500px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); } .bb-dialog h3 { margin: 0 0 20px 0; font-size: 20px; color: #333; } .bb-dialog-form-group { margin-bottom: 16px; } .bb-dialog-form-group label { display: block; margin-bottom: 6px; font-size: 14px; color: #666; font-weight: 500; } .bb-dialog-form-group input, .bb-dialog-form-group select { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .bb-dialog-form-group input:focus, .bb-dialog-form-group select:focus { outline: none; border-color: #667eea; } .bb-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .bb-dialog-actions button { padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background 0.2s; } .bb-dialog-actions .bb-btn-cancel { background: #f0f0f0; color: #333; } .bb-dialog-actions .bb-btn-cancel:hover { background: #e0e0e0; } .bb-dialog-actions .bb-btn-confirm { background: #667eea; color: white; } .bb-dialog-actions .bb-btn-confirm:hover { background: #5568d3; } .bb-dialog-actions button:disabled { opacity: 0.6; cursor: not-allowed; } .bb-icon-preview { display: flex; align-items: center; gap: 12px; margin-top: 8px; padding: 12px; background: #f5f5f5; border-radius: 6px; } .bb-icon-preview img { width: 48px; height: 48px; border-radius: 6px; object-fit: cover; border: 2px solid #e0e0e0; } .bb-icon-preview-text { font-size: 12px; color: #666; } `; this.shadowRoot.appendChild(style); } bindEvents() { // 添加当前页面按钮 this.shadowRoot.getElementById('bb-add-current-btn').addEventListener('click', () => { this.showAddCurrentPageDialog(); }); // 手动添加书签按钮 this.shadowRoot.getElementById('bb-add-manual-btn').addEventListener('click', () => { this.showAddManualDialog(); }); // 导入按钮 this.shadowRoot.getElementById('bb-import-btn').addEventListener('click', () => { this.shadowRoot.getElementById('bb-file-input').click(); }); // 空状态的导入按钮(初始状态) const emptyImportBtn = this.shadowRoot.getElementById('bb-empty-import-btn-initial'); if (emptyImportBtn) { emptyImportBtn.addEventListener('click', () => { this.shadowRoot.getElementById('bb-file-input').click(); }); } // 文件选择 this.shadowRoot.getElementById('bb-file-input').addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { this.importBookmarks(file); // 清空文件输入,以便可以重复选择同一个文件 e.target.value = ''; } }); // 导出按钮 this.shadowRoot.getElementById('bb-export-btn').addEventListener('click', () => { this.exportBookmarks(); }); // 搜索 this.shadowRoot.getElementById('bb-search-input').addEventListener('input', (e) => { this.searchQuery = e.target.value.toLowerCase(); this.filterBookmarks(); }); // 清除搜索 this.shadowRoot.getElementById('bb-clear-search').addEventListener('click', () => { this.shadowRoot.getElementById('bb-search-input').value = ''; this.searchQuery = ''; this.filterBookmarks(); }); // 视图切换 this.shadowRoot.getElementById('bb-grid-view').addEventListener('click', () => { this.currentView = 'grid'; this.updateViewButtons(); this.renderBookmarks(); }); this.shadowRoot.getElementById('bb-list-view').addEventListener('click', () => { this.currentView = 'list'; this.updateViewButtons(); this.renderBookmarks(); }); // 批量选择模式按钮 this.shadowRoot.getElementById('bb-batch-mode-btn').addEventListener('click', () => { this.toggleBatchMode(); }); // 批量操作按钮 this.shadowRoot.getElementById('bb-batch-select-all').addEventListener('click', () => { this.selectAllBookmarks(); }); this.shadowRoot.getElementById('bb-batch-deselect-all').addEventListener('click', () => { this.deselectAllBookmarks(); }); this.shadowRoot.getElementById('bb-batch-pin').addEventListener('click', () => { if (this.selectedBookmarks.size > 0) { this.batchPin(); } }); this.shadowRoot.getElementById('bb-batch-unpin').addEventListener('click', () => { if (this.selectedBookmarks.size > 0) { this.batchUnpin(); } }); this.shadowRoot.getElementById('bb-batch-delete').addEventListener('click', () => { const count = this.selectedBookmarks.size; if (count > 0) { if (confirm(`确定要删除选中的 ${count} 个书签吗?`)) { const urls = Array.from(this.selectedBookmarks); this.deleteBookmarks(urls); this.toggleBatchMode(); } } }); this.shadowRoot.getElementById('bb-batch-cancel').addEventListener('click', () => { this.toggleBatchMode(); }); // 关闭按钮 this.shadowRoot.getElementById('bb-close-btn').addEventListener('click', () => { this.hide(); }); // 快捷键绑定 document.addEventListener('keydown', (e) => { // Ctrl+B 或 Cmd+B 打开/关闭 if ((e.ctrlKey || e.metaKey) && e.key === 'b' && !e.shiftKey) { e.preventDefault(); this.toggle(); } // Ctrl+Shift+A 或 Cmd+Shift+A 添加当前页面 if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'A') { e.preventDefault(); this.showAddCurrentPageDialog(); } // Esc 关闭 if (e.key === 'Escape' && this.isVisible) { this.hide(); } }); // 点击背景关闭 const container = document.getElementById('better-bookmarks-container'); container.addEventListener('click', (e) => { // 检查点击是否在 Shadow DOM 内部 // 如果事件路径中包含 Shadow Root,说明点击的是 Shadow DOM 内的元素,不关闭 // 如果事件路径中不包含 Shadow Root,说明点击的是容器本身(背景),关闭 const path = e.composedPath(); const clickedInShadow = path.includes(this.shadowRoot); // 只有当点击的是容器本身(不在 Shadow DOM 内)时才关闭 if (!clickedInShadow) { this.hide(); } }); } show() { this.isVisible = true; // 每次显示时重新加载数据,确保同步最新数据 this.loadFromStorage(); document.getElementById('better-bookmarks-container').classList.add('visible'); // 聚焦搜索框 setTimeout(() => { this.shadowRoot.getElementById('bb-search-input').focus(); }, 100); } hide() { this.isVisible = false; document.getElementById('better-bookmarks-container').classList.remove('visible'); } toggle() { if (this.isVisible) { this.hide(); } else { this.show(); } } updateViewButtons() { this.shadowRoot.getElementById('bb-grid-view').classList.toggle('active', this.currentView === 'grid'); this.shadowRoot.getElementById('bb-list-view').classList.toggle('active', this.currentView === 'list'); } importBookmarks(file) { const reader = new FileReader(); reader.onload = (e) => { const html = e.target.result; this.parseBookmarks(html); // 保存到存储(使用 GM_setValue,可在所有网站间共享) this.saveToStorage(); this.filterBookmarks(); }; reader.onerror = () => { alert('读取文件失败,请重试'); }; reader.readAsText(file); } parseBookmarks(html) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); this.bookmarks = []; // 递归解析书签(忽略文件夹结构) const parseNode = (node) => { const children = node.querySelectorAll(':scope > DT'); children.forEach(child => { const h3 = child.querySelector('H3'); const a = child.querySelector('A'); if (h3) { // 文件夹,递归解析其内容 const dl = child.querySelector('DL'); if (dl) { parseNode(dl); } } else if (a) { // 书签 const href = a.getAttribute('HREF'); const title = a.textContent.trim(); const icon = a.getAttribute('ICON'); const addDate = a.getAttribute('ADD_DATE'); if (href && title) { this.bookmarks.push({ title, url: href, icon: icon || this.getDefaultIcon(href), addDate: addDate ? parseInt(addDate) * 1000 : Date.now(), pinned: false }); } } }); }; const dl = doc.querySelector('DL'); if (dl) { parseNode(dl); } // 按置顶和标题排序 this.bookmarks.sort((a, b) => { // 置顶的书签优先 if (a.pinned !== b.pinned) { return b.pinned ? 1 : -1; } return a.title.localeCompare(b.title); }); } normalizeUrl(url) { // 规范化URL,自动添加协议 if (!url) return url; url = url.trim(); // 如果已经有协议,直接返回 if (/^https?:\/\//i.test(url)) { return url; } // 如果以www开头或其他格式,自动添加https:// return `https://${url}`; } getFavicon(url) { // 同步获取favicon URL(使用Google的favicon服务) try { const normalizedUrl = this.normalizeUrl(url); const urlObj = new URL(normalizedUrl); const domain = urlObj.hostname; // 使用Google的favicon服务(最可靠且快速) return `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; } catch { return 'data:image/svg+xml,'; } } getDefaultIcon(url) { // 保持向后兼容,使用同步方法 return this.getFavicon(url); } filterBookmarks() { this.filteredBookmarks = this.bookmarks.filter(bookmark => { // 搜索筛选 if (this.searchQuery) { const query = this.searchQuery.toLowerCase(); return bookmark.title.toLowerCase().includes(query) || bookmark.url.toLowerCase().includes(query); } return true; }); // 对过滤后的书签进行排序:置顶优先 this.filteredBookmarks.sort((a, b) => { if (a.pinned !== b.pinned) { return b.pinned ? 1 : -1; } return a.title.localeCompare(b.title); }); this.renderBookmarks(); } highlightText(text, query) { if (!query) return this.escapeHtml(text); // 转义HTML const escaped = this.escapeHtml(text); // 转义查询字符串中的特殊字符 const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`(${escapedQuery})`, 'gi'); return escaped.replace(regex, '$1'); } renderBookmarks() { const content = this.shadowRoot.getElementById('bb-content'); if (this.filteredBookmarks.length === 0) { content.innerHTML = `

${this.searchQuery ? '没有找到匹配的书签' : '还没有书签,请先导入书签文件、添加书签或将当前访问页面保存为书签'}

${!this.searchQuery ? `` : ''}
`; // 绑定空状态的导入按钮事件 if (!this.searchQuery) { const emptyImportBtn = content.querySelector('#bb-empty-import-btn'); if (emptyImportBtn) { emptyImportBtn.addEventListener('click', () => { this.shadowRoot.getElementById('bb-file-input').click(); }); } } return; } let html = ''; if (this.currentView === 'grid') { html += '
'; this.filteredBookmarks.forEach(bookmark => { html += this.renderBookmarkCard(bookmark); }); html += '
'; } else { html += '
'; this.filteredBookmarks.forEach(bookmark => { html += this.renderBookmarkItem(bookmark); }); html += '
'; } content.innerHTML = html; // 更新批量工具栏状态 this.updateBatchToolbar(); // 绑定点击事件 content.querySelectorAll('.bb-bookmark-card, .bb-bookmark-item').forEach((el) => { el.addEventListener('click', (e) => { // 如果点击的是复选框、置顶按钮或右键菜单,不打开链接 if (e.target.classList.contains('bb-pin-btn') || e.target.classList.contains('bb-bookmark-checkbox') || e.target.closest('.bb-context-menu')) { e.stopPropagation(); return; } // 批量模式下点击卡片选择/取消选择 if (this.batchMode) { const url = el.getAttribute('data-url'); if (url) { this.toggleBookmarkSelection(url); this.renderBookmarks(); } return; } const url = el.getAttribute('data-url'); if (url) { window.open(url, '_blank'); } }); // 绑定右键菜单 el.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); const url = el.getAttribute('data-url'); if (url) { this.showContextMenu(e, url); } }); }); // 绑定复选框事件 content.querySelectorAll('.bb-bookmark-checkbox').forEach((checkbox) => { checkbox.addEventListener('change', (e) => { e.stopPropagation(); const url = checkbox.getAttribute('data-url'); if (url) { this.toggleBookmarkSelection(url); this.renderBookmarks(); } }); }); // 绑定置顶按钮事件 content.querySelectorAll('.bb-pin-btn').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); const url = btn.getAttribute('data-url'); this.togglePin(url); }); }); } renderBookmarkCard(bookmark) { const highlightedTitle = this.highlightText(bookmark.title, this.searchQuery); const highlightedUrl = this.highlightText(bookmark.url, this.searchQuery); const pinIcon = bookmark.pinned ? '📌' : '📍'; const pinClass = bookmark.pinned ? 'pinned' : ''; const isSelected = this.selectedBookmarks.has(bookmark.url); const checkboxHtml = this.batchMode ? ` ` : ''; return `
${checkboxHtml}
${highlightedTitle}
${highlightedUrl}
`; } renderBookmarkItem(bookmark) { const highlightedTitle = this.highlightText(bookmark.title, this.searchQuery); const highlightedUrl = this.highlightText(bookmark.url, this.searchQuery); const pinIcon = bookmark.pinned ? '📌' : '📍'; const pinClass = bookmark.pinned ? 'pinned' : ''; const isSelected = this.selectedBookmarks.has(bookmark.url); const checkboxHtml = this.batchMode ? ` ` : ''; return `
${checkboxHtml}
${highlightedTitle}
${highlightedUrl}
`; } togglePin(url) { const bookmark = this.bookmarks.find(b => b.url === url); if (bookmark) { bookmark.pinned = !bookmark.pinned; this.saveToStorage(); this.filterBookmarks(); } } deleteBookmark(url) { const index = this.bookmarks.findIndex(b => b.url === url); if (index !== -1) { // 从内存中删除 this.bookmarks.splice(index, 1); this.selectedBookmarks.delete(url); // 使用油猴 API 保存到存储 try { if (this.bookmarks && this.bookmarks.length > 0) { GM_setValue('better-bookmarks', JSON.stringify(this.bookmarks)); } else { GM_setValue('better-bookmarks', JSON.stringify([])); } } catch (e) { console.error('删除书签失败:', e); alert('删除书签失败: ' + e.message); return; } this.filterBookmarks(); } } deleteBookmarks(urls) { // 从内存中批量删除 urls.forEach(url => { const index = this.bookmarks.findIndex(b => b.url === url); if (index !== -1) { this.bookmarks.splice(index, 1); } this.selectedBookmarks.delete(url); }); // 使用油猴 API 保存到存储 try { if (this.bookmarks && this.bookmarks.length > 0) { GM_setValue('better-bookmarks', JSON.stringify(this.bookmarks)); } else { GM_setValue('better-bookmarks', JSON.stringify([])); } } catch (e) { console.error('批量删除书签失败:', e); alert('批量删除书签失败: ' + e.message); return; } this.filterBookmarks(); } toggleBookmarkSelection(url) { if (this.selectedBookmarks.has(url)) { this.selectedBookmarks.delete(url); } else { this.selectedBookmarks.add(url); } this.updateBatchToolbar(); } selectAllBookmarks() { this.filteredBookmarks.forEach(bookmark => { this.selectedBookmarks.add(bookmark.url); }); this.updateBatchToolbar(); this.renderBookmarks(); } deselectAllBookmarks() { this.selectedBookmarks.clear(); this.updateBatchToolbar(); this.renderBookmarks(); } toggleBatchMode() { this.batchMode = !this.batchMode; if (!this.batchMode) { this.selectedBookmarks.clear(); } this.updateBatchToolbar(); this.renderBookmarks(); } batchPin() { const urls = Array.from(this.selectedBookmarks); urls.forEach(url => { const bookmark = this.bookmarks.find(b => b.url === url); if (bookmark && !bookmark.pinned) { bookmark.pinned = true; } }); this.saveToStorage(); this.filterBookmarks(); this.selectedBookmarks.clear(); this.updateBatchToolbar(); } batchUnpin() { const urls = Array.from(this.selectedBookmarks); urls.forEach(url => { const bookmark = this.bookmarks.find(b => b.url === url); if (bookmark && bookmark.pinned) { bookmark.pinned = false; } }); this.saveToStorage(); this.filterBookmarks(); this.selectedBookmarks.clear(); this.updateBatchToolbar(); } showContextMenu(e, url) { // 移除已存在的右键菜单 const existingMenu = this.shadowRoot.querySelector('.bb-context-menu'); if (existingMenu) { existingMenu.remove(); } const bookmark = this.bookmarks.find(b => b.url === url); if (!bookmark) return; // 获取容器位置 const container = this.shadowRoot.querySelector('.bb-wrapper'); const rect = container.getBoundingClientRect(); const menu = document.createElement('div'); menu.className = 'bb-context-menu'; menu.style.position = 'absolute'; menu.style.left = `${e.clientX - rect.left}px`; menu.style.top = `${e.clientY - rect.top}px`; menu.style.zIndex = '1000001'; menu.innerHTML = `
${bookmark.pinned ? '取消置顶' : '置顶'}
编辑
删除
`; container.appendChild(menu); // 绑定菜单项点击事件 menu.querySelectorAll('.bb-context-menu-item').forEach(item => { item.addEventListener('click', (e) => { e.stopPropagation(); const action = item.getAttribute('data-action'); this.handleContextMenuAction(action, url); menu.remove(); }); }); // 点击其他地方关闭菜单 const closeMenu = (e) => { const path = e.composedPath ? e.composedPath() : []; if (!path.includes(menu)) { menu.remove(); document.removeEventListener('click', closeMenu); } }; setTimeout(() => { document.addEventListener('click', closeMenu); }, 100); } handleContextMenuAction(action, url) { switch (action) { case 'pin': this.togglePin(url); break; case 'edit': const bookmark = this.bookmarks.find(b => b.url === url); if (bookmark) { this.showEditBookmarkDialog(bookmark); } break; case 'delete': if (confirm('确定要删除这个书签吗?')) { this.deleteBookmark(url); } break; } } updateBatchToolbar() { const batchControls = this.shadowRoot.getElementById('bb-batch-controls'); const batchModeBtn = this.shadowRoot.getElementById('bb-batch-mode-btn'); const selectedCount = this.shadowRoot.getElementById('bb-selected-count'); if (this.batchMode) { batchControls.style.display = 'flex'; batchModeBtn.textContent = '取消批量'; const count = this.selectedBookmarks.size; selectedCount.textContent = `已选择 ${count} 项`; } else { batchControls.style.display = 'none'; batchModeBtn.textContent = '批量选择'; } } showAddCurrentPageDialog() { // 获取当前页面信息 const currentUrl = window.location.href; const currentTitle = document.title || currentUrl; // 检查是否已存在 const existingBookmark = this.bookmarks.find(b => b.url === currentUrl); if (existingBookmark) { if (confirm(`该页面已存在于书签中:\n${existingBookmark.title}\n\n是否要更新它?`)) { this.showEditBookmarkDialog(existingBookmark); } return; } // 创建对话框(在 Shadow DOM 中) const overlay = document.createElement('div'); overlay.className = 'bb-dialog-overlay'; const overlayStyle = document.createElement('style'); overlayStyle.textContent = ` .bb-dialog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000000; } .bb-dialog { background: white; border-radius: 8px; padding: 24px; min-width: 400px; max-width: 500px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .bb-dialog h3 { margin: 0 0 20px 0; font-size: 20px; color: #333; } .bb-dialog-form-group { margin-bottom: 16px; } .bb-dialog-form-group label { display: block; margin-bottom: 6px; font-size: 14px; color: #666; font-weight: 500; } .bb-dialog-form-group input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .bb-dialog-form-group input:focus { outline: none; border-color: #667eea; } .bb-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .bb-dialog-actions button { padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background 0.2s; } .bb-dialog-actions .bb-btn-cancel { background: #f0f0f0; color: #333; } .bb-dialog-actions .bb-btn-cancel:hover { background: #e0e0e0; } .bb-dialog-actions .bb-btn-confirm { background: #667eea; color: white; } .bb-dialog-actions .bb-btn-confirm:hover { background: #5568d3; } `; overlay.appendChild(overlayStyle); const dialog = document.createElement('div'); dialog.className = 'bb-dialog'; dialog.innerHTML = `

添加当前页面

`; overlay.appendChild(dialog); document.body.appendChild(overlay); // 绑定事件 const cancelBtn = overlay.querySelector('#bb-dialog-cancel'); const confirmBtn = overlay.querySelector('#bb-dialog-confirm'); const titleInput = overlay.querySelector('#bb-dialog-title'); const closeDialog = () => { document.body.removeChild(overlay); }; cancelBtn.addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeDialog(); } }); confirmBtn.addEventListener('click', () => { const title = titleInput.value.trim(); let url = overlay.querySelector('#bb-dialog-url').value.trim(); if (!title) { alert('请输入标题'); return; } if (!url) { alert('URL不能为空'); return; } // 规范化URL(自动添加https://) url = this.normalizeUrl(url); // 验证URL格式 try { new URL(url); } catch { alert('请输入有效的URL(例如:example.com 或 www.example.com)'); return; } this.addBookmark(title, url); closeDialog(); }); // 按Enter确认 titleInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { confirmBtn.click(); } }); // 聚焦到标题输入框 setTimeout(() => titleInput.focus(), 100); } showAddManualDialog() { // 创建对话框 const overlay = document.createElement('div'); overlay.className = 'bb-dialog-overlay'; const overlayStyle = document.createElement('style'); overlayStyle.textContent = ` .bb-dialog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000000; } .bb-dialog { background: white; border-radius: 8px; padding: 24px; min-width: 400px; max-width: 500px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .bb-dialog h3 { margin: 0 0 20px 0; font-size: 20px; color: #333; } .bb-dialog-form-group { margin-bottom: 16px; } .bb-dialog-form-group label { display: block; margin-bottom: 6px; font-size: 14px; color: #666; font-weight: 500; } .bb-dialog-form-group input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .bb-dialog-form-group input:focus { outline: none; border-color: #667eea; } .bb-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .bb-dialog-actions button { padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background 0.2s; } .bb-dialog-actions .bb-btn-cancel { background: #f0f0f0; color: #333; } .bb-dialog-actions .bb-btn-cancel:hover { background: #e0e0e0; } .bb-dialog-actions .bb-btn-confirm { background: #667eea; color: white; } .bb-dialog-actions .bb-btn-confirm:hover { background: #5568d3; } .bb-dialog-actions button:disabled { opacity: 0.6; cursor: not-allowed; } .bb-icon-preview { display: flex; align-items: center; gap: 12px; margin-top: 8px; padding: 12px; background: #f5f5f5; border-radius: 6px; } .bb-icon-preview img { width: 48px; height: 48px; border-radius: 6px; object-fit: cover; border: 2px solid #e0e0e0; } .bb-icon-preview-text { font-size: 12px; color: #666; } `; overlay.appendChild(overlayStyle); const dialog = document.createElement('div'); dialog.className = 'bb-dialog'; dialog.innerHTML = `

手动添加书签

`; overlay.appendChild(dialog); document.body.appendChild(overlay); // 绑定事件 const cancelBtn = overlay.querySelector('#bb-manual-dialog-cancel'); const confirmBtn = overlay.querySelector('#bb-manual-dialog-confirm'); const titleInput = overlay.querySelector('#bb-manual-dialog-title'); const urlInput = overlay.querySelector('#bb-manual-dialog-url'); const iconPreview = overlay.querySelector('#bb-icon-preview'); const iconPreviewImg = overlay.querySelector('#bb-icon-preview-img'); // URL输入时自动获取并预览icon let iconUpdateTimeout; urlInput.addEventListener('input', () => { clearTimeout(iconUpdateTimeout); let url = urlInput.value.trim(); if (!url) { iconPreview.style.display = 'none'; return; } // 规范化URL(自动添加https://) url = this.normalizeUrl(url); // 验证URL格式 try { new URL(url); } catch { iconPreview.style.display = 'none'; return; } // 延迟更新,避免频繁请求 iconUpdateTimeout = setTimeout(() => { const icon = this.getFavicon(url); iconPreviewImg.src = icon; iconPreviewImg.onerror = () => { iconPreviewImg.src = 'data:image/svg+xml,'; }; iconPreview.style.display = 'flex'; }, 500); }); const closeDialog = () => { document.body.removeChild(overlay); }; cancelBtn.addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeDialog(); } }); confirmBtn.addEventListener('click', async () => { const title = titleInput.value.trim(); let url = urlInput.value.trim(); if (!title) { alert('请输入标题'); return; } if (!url) { alert('URL不能为空'); return; } // 规范化URL(自动添加https://) url = this.normalizeUrl(url); // 验证URL格式 try { new URL(url); } catch { alert('请输入有效的URL(例如:example.com 或 www.example.com)'); return; } // 检查是否已存在 const existingBookmark = this.bookmarks.find(b => b.url === url); if (existingBookmark) { if (!confirm(`该URL已存在于书签中:\n${existingBookmark.title}\n\n是否要更新它?`)) { return; } } this.addBookmark(title, url); closeDialog(); }); // 按Enter确认 urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { confirmBtn.click(); } }); titleInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { urlInput.focus(); } }); // 聚焦到标题输入框 setTimeout(() => titleInput.focus(), 100); } showEditBookmarkDialog(bookmark) { // 创建对话框(在 Shadow DOM 中) const overlay = document.createElement('div'); overlay.className = 'bb-dialog-overlay'; const overlayStyle = document.createElement('style'); overlayStyle.textContent = ` .bb-dialog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 1000000; } .bb-dialog { background: white; border-radius: 8px; padding: 24px; min-width: 400px; max-width: 500px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } .bb-dialog h3 { margin: 0 0 20px 0; font-size: 20px; color: #333; } .bb-dialog-form-group { margin-bottom: 16px; } .bb-dialog-form-group label { display: block; margin-bottom: 6px; font-size: 14px; color: #666; font-weight: 500; } .bb-dialog-form-group input { width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; box-sizing: border-box; } .bb-dialog-form-group input:focus { outline: none; border-color: #667eea; } .bb-dialog-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; } .bb-dialog-actions button { padding: 8px 16px; border: none; border-radius: 4px; font-size: 14px; cursor: pointer; transition: background 0.2s; } .bb-dialog-actions .bb-btn-cancel { background: #f0f0f0; color: #333; } .bb-dialog-actions .bb-btn-cancel:hover { background: #e0e0e0; } .bb-dialog-actions .bb-btn-confirm { background: #667eea; color: white; } .bb-dialog-actions .bb-btn-confirm:hover { background: #5568d3; } `; overlay.appendChild(overlayStyle); const dialog = document.createElement('div'); dialog.className = 'bb-dialog'; dialog.innerHTML = `

编辑书签

`; overlay.appendChild(dialog); document.body.appendChild(overlay); // 绑定事件 const cancelBtn = overlay.querySelector('#bb-dialog-cancel'); const confirmBtn = overlay.querySelector('#bb-dialog-confirm'); const titleInput = overlay.querySelector('#bb-dialog-title'); const urlInput = overlay.querySelector('#bb-dialog-url'); const closeDialog = () => { document.body.removeChild(overlay); }; cancelBtn.addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) { closeDialog(); } }); confirmBtn.addEventListener('click', () => { const title = titleInput.value.trim(); let url = urlInput.value.trim(); if (!title) { alert('请输入标题'); return; } if (!url) { alert('URL不能为空'); return; } // 规范化URL(自动添加https://) url = this.normalizeUrl(url); // 验证URL格式 try { new URL(url); } catch { alert('请输入有效的URL(例如:example.com 或 www.example.com)'); return; } bookmark.title = title; bookmark.url = url; bookmark.icon = this.getFavicon(url); this.saveToStorage(); this.filterBookmarks(); closeDialog(); }); // 聚焦到标题输入框 setTimeout(() => titleInput.focus(), 100); } addBookmark(title, url) { // 检查是否已存在相同的URL const existingIndex = this.bookmarks.findIndex(b => b.url === url); const icon = this.getFavicon(url); if (existingIndex !== -1) { // 如果已存在,更新它 this.bookmarks[existingIndex].title = title; this.bookmarks[existingIndex].icon = icon; } else { // 添加新书签 this.bookmarks.push({ title: title.trim(), url: url.trim(), icon: icon, addDate: Date.now(), pinned: false }); } // 保存并更新显示 this.saveToStorage(); this.filterBookmarks(); } addCurrentPage(title, url) { // 保持向后兼容 this.addBookmark(title, url); } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } saveToStorage() { try { if (this.bookmarks && this.bookmarks.length > 0) { GM_setValue('better-bookmarks', JSON.stringify(this.bookmarks)); } else { // 如果书签为空,也保存空数组,以便下次加载时知道已经初始化过 GM_setValue('better-bookmarks', JSON.stringify([])); } } catch (e) { console.error('保存书签失败:', e); alert('保存书签失败: ' + e.message); } } setupAutoSave() { // 页面关闭前保存 window.addEventListener('beforeunload', () => { this.saveToStorage(); }); // 页面隐藏时保存(切换标签页等) document.addEventListener('visibilitychange', () => { if (document.hidden) { this.saveToStorage(); } }); } setupStorageListener() { // 监听存储变化,实现跨标签页实时同步 try { if (typeof GM_addValueChangeListener !== 'undefined') { this.storageListener = GM_addValueChangeListener('better-bookmarks', (name, oldValue, newValue, remote) => { // remote 为 true 表示变化来自其他标签页 if (remote) { // 重新加载数据 this.loadFromStorage(); // 如果界面是打开的,刷新显示 if (this.isVisible) { this.filterBookmarks(); } } }); } } catch (e) { console.warn('无法设置存储监听器:', e); // 如果不支持监听器,至少每次显示时重新加载数据(已在 show() 方法中实现) } } loadFromStorage() { try { // 先尝试从 GM_getValue 加载 let stored = GM_getValue('better-bookmarks', null); // 如果 GM_getValue 没有数据,尝试从 localStorage 迁移(兼容旧数据) if (!stored) { const oldStored = localStorage.getItem('better-bookmarks'); if (oldStored) { stored = oldStored; // 迁移到 GM_setValue GM_setValue('better-bookmarks', oldStored); // 清理旧的 localStorage 数据 try { localStorage.removeItem('better-bookmarks'); } catch (e) { // 忽略清理错误 } } } if (stored) { this.bookmarks = JSON.parse(stored); // 确保所有书签都有 pinned 属性,并移除旧的 folder 属性 this.bookmarks.forEach(bookmark => { if (bookmark.pinned === undefined) { bookmark.pinned = false; } // 移除 folder 属性(如果存在) if (bookmark.folder !== undefined) { delete bookmark.folder; } }); // 保存清理后的数据 this.saveToStorage(); this.filterBookmarks(); } } catch (e) { console.error('加载书签失败:', e); } } exportBookmarks() { // 生成Netscape格式的书签HTML let html = ` Bookmarks

Bookmarks

`; // 生成HTML(所有书签放在一个文件夹中) html += `

收藏夹

\n

\n`; this.bookmarks.forEach(bookmark => { const date = Math.floor(bookmark.addDate / 1000); const url = this.escapeHtml(bookmark.url); const icon = this.escapeHtml(bookmark.icon || ''); const title = this.escapeHtml(bookmark.title); html += `

${title}\n`; }); html += `

\n`; html += `

`; // 下载文件 const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `bookmarks_${new Date().toISOString().split('T')[0]}.html`; a.click(); URL.revokeObjectURL(url); } } // 注册油猴菜单命令 - 显示快捷键提示 GM_registerMenuCommand('快捷键说明', () => { const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; const ctrlKey = isMac ? 'Cmd' : 'Ctrl'; const shortcuts = [ `${ctrlKey} + B:打开/关闭书签界面`, `${ctrlKey} + Shift + A:添加当前页面到书签`, `Esc:关闭书签界面` ]; alert('快捷键说明:\n\n' + shortcuts.join('\n')); }); // 等待DOM加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new BookmarkManager(); }); } else { new BookmarkManager(); } })();