您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 Komica 每篇貼文添加收藏功能,提供懸浮視窗管理收藏,支持自定義位置、大小及配色切換,並新增收藏圖片縮圖功能。
// ==UserScript== // @name Komica 收藏貼文功能 // @namespace https://komica.org/ // @version 4.1 // @description 在 Komica 每篇貼文添加收藏功能,提供懸浮視窗管理收藏,支持自定義位置、大小及配色切換,並新增收藏圖片縮圖功能。 // @author Yun // @license GNU GPLv3 // @icon https://i.ibb.co/bscXhHh/icon.png // @match https://gita.komica1.org/* // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // 常量定義 const CONSTANTS = { STORAGE_KEYS: { FAVORITES: 'komicaFavorites', POSITION: 'favoritesWindowPosition', SIZE: 'favoritesWindowSize', THEME: 'favoritesWindowTheme', CATEGORIES: 'komicaCategories' }, DEFAULT_VALUES: { POSITION: { top: '10px', left: '10px' }, SIZE: { width: '300px', height: '400px' }, THEME: 'original' }, THEMES: { original: { background: '#F0E0D6', header: '#EA8', text: '#800000', button: '#EA8', border: '#B89080' }, blackWhite: { background: '#FFFFFF', header: '#BBBBBB', text: '#000000', button: '#BBBBBB', border: '#999999' }, blue: { background: '#DDEEFF', header: '#6699CC', text: '#003366', button: '#6699CC', border: '#4477AA' } }, HOTKEYS: { TOGGLE_WINDOW: 'Alt+F', QUICK_SAVE: 'Alt+S' } }; // 工具函數 const utils = { throttle: (func, limit) => { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }, showToast: (message, type = 'info') => { const toast = document.createElement('div'); toast.textContent = message; toast.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); padding: 10px 20px; border-radius: 5px; color: white; z-index: 10002; opacity: 0; transition: opacity 0.3s; `; switch(type) { case 'success': toast.style.backgroundColor = '#4CAF50'; break; case 'error': toast.style.backgroundColor = '#f44336'; break; default: toast.style.backgroundColor = '#2196F3'; } document.body.appendChild(toast); setTimeout(() => toast.style.opacity = '1', 10); setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, 2000); }, exportData: () => { const data = { favorites: favorites, categories: categories }; const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `komica_favorites_${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); utils.showToast('匯出成功!', 'success'); }, importData: (file) => { const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); if (data.favorites && Array.isArray(data.favorites)) { favorites = data.favorites; localStorage.setItem(CONSTANTS.STORAGE_KEYS.FAVORITES, JSON.stringify(favorites)); if (data.categories) { categories = data.categories; localStorage.setItem(CONSTANTS.STORAGE_KEYS.CATEGORIES, JSON.stringify(categories)); } updateFavoritesWindow(); utils.showToast('匯入成功!', 'success'); } else { utils.showToast('無效的檔案格式!', 'error'); } } catch (error) { utils.showToast('匯入失敗!', 'error'); console.error('Import error:', error); } }; reader.readAsText(file); }, // 更新分類列表 updateCategories: () => { // 獲取所有已使用的分類 const usedCategories = new Set(favorites.map(fav => fav.category)); // 更新分類列表,只保留已使用的分類和"未分類" categories = Array.from(usedCategories); if (!categories.includes('未分類')) { categories.push('未分類'); } // 儲存更新後的分類列表 localStorage.setItem(CONSTANTS.STORAGE_KEYS.CATEGORIES, JSON.stringify(categories)); // 更新分類選單 const categorySelect = document.querySelector('#favoritesWindow select'); if (categorySelect) { const currentValue = categorySelect.value; categorySelect.innerHTML = ` <option value="全部">全部</option> ${categories.map(cat => `<option value="${cat}">${cat}</option>`).join('')} `; categorySelect.value = currentValue; } } }; // 狀態管理 let favorites = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.FAVORITES)) || []; let categories = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.CATEGORIES)) || ['未分類']; let position = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.POSITION)) || CONSTANTS.DEFAULT_VALUES.POSITION; let size = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.SIZE)) || CONSTANTS.DEFAULT_VALUES.SIZE; let theme = JSON.parse(localStorage.getItem(CONSTANTS.STORAGE_KEYS.THEME)) || CONSTANTS.DEFAULT_VALUES.THEME; let currentCategory = '全部'; let currentSort = 'time-desc'; let searchTerm = ''; // 更新收藏按鈕顏色 function updateCollectButtonsColor() { document.querySelectorAll('.collect-btn').forEach(btn => { btn.style.color = CONSTANTS.THEMES[theme].text; }); } // 應用主題 function applyTheme(favoritesWindow, toggleBtn) { const currentTheme = CONSTANTS.THEMES[theme]; favoritesWindow.style.backgroundColor = currentTheme.background; favoritesWindow.querySelector('.window-header').style.backgroundColor = currentTheme.header; favoritesWindow.querySelector('.window-header').style.color = currentTheme.text; if (toggleBtn) { toggleBtn.style.backgroundColor = currentTheme.button; toggleBtn.style.color = currentTheme.text; } // 更新所有文字顏色 document.querySelectorAll('#favoritesWindow a, #favoritesWindow p, #favoritesWindow select') .forEach(el => el.style.color = currentTheme.text); // 新增:更新編輯和刪除按鈕顏色 document.querySelectorAll('#favoritesWindow .favorites-content span[title="編輯分類"], #favoritesWindow .favorites-content span[title="移除收藏"]') .forEach(btn => btn.style.color = currentTheme.text); // 新增:更新分類標籤顏色 document.querySelectorAll('#favoritesWindow .favorites-content span:not([title])') .forEach(tag => { tag.style.backgroundColor = `${currentTheme.header}40`; tag.style.color = currentTheme.text; }); document.querySelectorAll('#favoritesWindow input, #favoritesWindow select') .forEach(el => { el.style.border = `1px solid ${currentTheme.border}`; el.style.backgroundColor = currentTheme.background; el.style.color = currentTheme.text; }); // 更新分隔線顏色 document.querySelectorAll('#favoritesWindow .favorites-content > div') .forEach(item => { item.style.borderBottom = `1px solid ${currentTheme.border}`; }); document.querySelector('#favoritesWindow .favorites-toolbar')?.style .setProperty('border-bottom-color', currentTheme.border); updateCollectButtonsColor(); } // 建立工具列 function createToolbar(favoritesWindow) { const toolbar = document.createElement('div'); toolbar.className = 'favorites-toolbar'; toolbar.style.cssText = ` padding: 5px; display: flex; gap: 10px; align-items: center; border-bottom: 1px solid ${CONSTANTS.THEMES[theme].border}; `; // 搜尋框 const searchInput = document.createElement('input'); searchInput.type = 'text'; searchInput.placeholder = '搜尋...'; searchInput.style.cssText = ` padding: 3px; border: 1px solid ${CONSTANTS.THEMES[theme].border}; border-radius: 3px; flex: 1; background-color: ${CONSTANTS.THEMES[theme].background}; color: ${CONSTANTS.THEMES[theme].text}; `; searchInput.style.setProperty('::placeholder', CONSTANTS.THEMES[theme].text); searchInput.addEventListener('input', (e) => { searchTerm = e.target.value.toLowerCase(); updateFavoritesWindow(); }); // 分類選擇 const categorySelect = document.createElement('select'); categorySelect.style.cssText = ` padding: 3px; border: 1px solid ${CONSTANTS.THEMES[theme].border}; border-radius: 3px; background-color: ${CONSTANTS.THEMES[theme].background}; color: ${CONSTANTS.THEMES[theme].text}; `; categorySelect.innerHTML = ` <option value="全部">全部</option> ${categories.map(cat => `<option value="${cat}">${cat}</option>`).join('')} `; categorySelect.value = currentCategory; categorySelect.addEventListener('change', (e) => { currentCategory = e.target.value; updateFavoritesWindow(); }); // 排序選擇 const sortSelect = document.createElement('select'); sortSelect.style.cssText = ` padding: 3px; border: 1px solid ${CONSTANTS.THEMES[theme].border}; border-radius: 3px; background-color: ${CONSTANTS.THEMES[theme].background}; color: ${CONSTANTS.THEMES[theme].text}; `; sortSelect.innerHTML = ` <option value="time-desc">時間 ↓</option> <option value="time-asc">時間 ↑</option> <option value="id-desc">編號 ↓</option> <option value="id-asc">編號 ↑</option> `; sortSelect.value = currentSort; sortSelect.addEventListener('change', (e) => { currentSort = e.target.value; updateFavoritesWindow(); }); toolbar.appendChild(searchInput); toolbar.appendChild(categorySelect); toolbar.appendChild(sortSelect); return toolbar; } // 添加收藏按鈕 function addCollectButtons() { const posts = document.querySelectorAll('.post:not(.has-collect-btn)'); posts.forEach(post => { post.classList.add('has-collect-btn'); const postId = post.dataset.no; const threadId = post.closest('.thread')?.dataset.no || postId; const thumbnail = post.querySelector('img')?.src || null; if (!postId) return; const collectBtn = document.createElement('span'); collectBtn.textContent = favorites.some(fav => fav.id === postId) ? '★' : '☆'; collectBtn.className = 'collect-btn text-button'; collectBtn.style.cssText = ` margin-left: 10px; cursor: pointer; color: ${CONSTANTS.THEMES[theme].text}; `; collectBtn.addEventListener('click', () => { const existingFavorite = favorites.find(fav => fav.id === postId); if (existingFavorite) { favorites = favorites.filter(fav => fav.id !== postId); collectBtn.textContent = '☆'; utils.showToast('已移除收藏', 'info'); } else { const postContent = post.querySelector('.quote')?.textContent || '無內文'; favorites.push({ id: postId, url: `https://gita.komica1.org/00b/pixmicat.php?res=${threadId}#r${postId}`, content: postContent, thumbnail, category: '未分類', timestamp: Date.now() }); collectBtn.textContent = '★'; utils.showToast('已加入收藏', 'success'); } localStorage.setItem(CONSTANTS.STORAGE_KEYS.FAVORITES, JSON.stringify(favorites)); utils.updateCategories(); // 更新分類列表 updateFavoritesWindow(); }); const postHead = post.querySelector('.post-head'); if (postHead) postHead.appendChild(collectBtn); }); } // 建立收藏視窗 function createFavoritesWindow() { const favoritesWindow = document.createElement('div'); favoritesWindow.id = 'favoritesWindow'; favoritesWindow.style.cssText = ` position: fixed; top: ${position.top}; left: ${position.left}; width: ${size.width}; height: ${size.height}; border: 1px solid #666; box-shadow: 0 3px 10px rgba(0,0,0,0.75); overflow: hidden; display: none; resize: both; z-index: 10001; flex-direction: column; `; // 視窗標題列 const header = document.createElement('div'); header.className = 'window-header'; header.style.cssText = ` padding: 5px; cursor: move; display: flex; justify-content: space-between; align-items: center; `; const title = document.createElement('span'); title.textContent = '已收藏的貼文'; title.style.fontWeight = 'bold'; const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; // 匯出按鈕 const exportBtn = document.createElement('span'); exportBtn.textContent = '↓'; exportBtn.title = '匯出收藏'; exportBtn.style.cursor = 'pointer'; exportBtn.addEventListener('click', utils.exportData); // 匯入按鈕 const importBtn = document.createElement('span'); importBtn.textContent = '↑'; importBtn.title = '匯入收藏'; importBtn.style.cursor = 'pointer'; const importInput = document.createElement('input'); importInput.type = 'file'; importInput.accept = '.json'; importInput.style.display = 'none'; importInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { utils.importData(e.target.files[0]); e.target.value = ''; // 重置 input,允許重複匯入相同檔案 } }); importBtn.addEventListener('click', () => importInput.click()); document.body.appendChild(importInput); // 清空按鈕 const clearAllBtn = document.createElement('span'); clearAllBtn.textContent = '⌫'; clearAllBtn.style.cursor = 'pointer'; clearAllBtn.title = '清空收藏'; clearAllBtn.addEventListener('click', () => { if (confirm('確定要清空所有收藏嗎?')) { favorites = []; localStorage.setItem(CONSTANTS.STORAGE_KEYS.FAVORITES, JSON.stringify(favorites)); utils.updateCategories(); // 更新分類列表 updateFavoritesWindow(); utils.showToast('已清空所有收藏', 'info'); } }); // 主題切換按鈕 const changeThemeBtn = document.createElement('span'); changeThemeBtn.textContent = '↹'; changeThemeBtn.style.cursor = 'pointer'; changeThemeBtn.title = '切換配色'; changeThemeBtn.addEventListener('click', () => { const themeKeys = Object.keys(CONSTANTS.THEMES); const currentIndex = themeKeys.indexOf(theme); theme = themeKeys[(currentIndex + 1) % themeKeys.length]; localStorage.setItem(CONSTANTS.STORAGE_KEYS.THEME, JSON.stringify(theme)); applyTheme(favoritesWindow, toggleBtn); updateFavoritesWindow(); // 重新渲染收藏列表以更新所有元素顏色 utils.showToast(`已切換至${theme}主題`, 'info'); }); // 關閉按鈕 const closeBtn = document.createElement('span'); closeBtn.textContent = '✕'; closeBtn.style.cursor = 'pointer'; closeBtn.title = '關閉視窗'; closeBtn.addEventListener('click', () => { favoritesWindow.style.display = 'none'; }); // 添加所有按鈕到容器 [exportBtn, importBtn, clearAllBtn, changeThemeBtn, closeBtn].forEach(btn => { buttonContainer.appendChild(btn); }); header.appendChild(title); header.appendChild(buttonContainer); favoritesWindow.appendChild(header); // 添加工具列 const toolbar = createToolbar(favoritesWindow); favoritesWindow.appendChild(toolbar); // 內容區域 const content = document.createElement('div'); content.className = 'favorites-content'; content.style.cssText = ` flex: 1; overflow-y: auto; padding: 10px; `; favoritesWindow.appendChild(content); // 拖曳功能 let isDragging = false; let offsetX, offsetY; const startDragging = (e) => { if (e.target !== header) return; isDragging = true; offsetX = e.clientX - favoritesWindow.offsetLeft; offsetY = e.clientY - favoritesWindow.offsetTop; header.style.cursor = 'grabbing'; }; const onDrag = utils.throttle((e) => { if (!isDragging) return; favoritesWindow.style.left = `${e.clientX - offsetX}px`; favoritesWindow.style.top = `${e.clientY - offsetY}px`; }, 16); const stopDragging = () => { if (!isDragging) return; isDragging = false; header.style.cursor = 'move'; position = { top: favoritesWindow.style.top, left: favoritesWindow.style.left }; localStorage.setItem(CONSTANTS.STORAGE_KEYS.POSITION, JSON.stringify(position)); }; header.addEventListener('mousedown', startDragging); document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDragging); // 視窗大小調整 const onResize = utils.throttle(() => { size = { width: favoritesWindow.style.width, height: favoritesWindow.style.height }; localStorage.setItem(CONSTANTS.STORAGE_KEYS.SIZE, JSON.stringify(size)); }, 100); favoritesWindow.addEventListener('mouseup', onResize); // 切換按鈕 const toggleBtn = document.createElement('div'); toggleBtn.textContent = '★ 收藏'; toggleBtn.style.cssText = ` position: fixed; bottom: 10px; right: 10px; padding: 5px 10px; cursor: pointer; z-index: 10000; border-radius: 3px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); `; toggleBtn.addEventListener('click', () => { favoritesWindow.style.display = favoritesWindow.style.display === 'none' ? 'flex' : 'none'; }); // 快捷鍵支援 document.addEventListener('keydown', (e) => { // Alt + F:切換收藏視窗 if (e.altKey && e.key.toLowerCase() === 'f') { e.preventDefault(); toggleBtn.click(); } // Alt + S:快速收藏當前貼文 if (e.altKey && e.key.toLowerCase() === 's') { e.preventDefault(); const activePost = document.activeElement.closest('.post'); if (activePost) { const collectBtn = activePost.querySelector('.collect-btn'); if (collectBtn) collectBtn.click(); } } }); document.body.appendChild(favoritesWindow); document.body.appendChild(toggleBtn); applyTheme(favoritesWindow, toggleBtn); updateFavoritesWindow(); return favoritesWindow; } function updateFavoritesWindow() { const content = document.querySelector('#favoritesWindow .favorites-content'); if (!content) return; content.innerHTML = ''; let filteredFavorites = [...favorites]; // 套用分類篩選 if (currentCategory !== '全部') { filteredFavorites = filteredFavorites.filter(fav => fav.category === currentCategory); } // 套用搜尋篩選 if (searchTerm) { filteredFavorites = filteredFavorites.filter(fav => fav.content.toLowerCase().includes(searchTerm) || fav.id.includes(searchTerm) ); } // 套用排序 filteredFavorites.sort((a, b) => { switch(currentSort) { case 'time-desc': return (b.timestamp || 0) - (a.timestamp || 0); case 'time-asc': return (a.timestamp || 0) - (b.timestamp || 0); case 'id-desc': return b.id.localeCompare(a.id); case 'id-asc': return a.id.localeCompare(b.id); default: return 0; } }); if (filteredFavorites.length === 0) { const noFavorites = document.createElement('p'); noFavorites.textContent = searchTerm ? '沒有符合搜尋條件的收藏' : '尚無收藏'; noFavorites.style.textAlign = 'center'; noFavorites.style.color = CONSTANTS.THEMES[theme].text; content.appendChild(noFavorites); return; } // 使用 DocumentFragment 優化 DOM 操作 const fragment = document.createDocumentFragment(); filteredFavorites.forEach(({ id, url, content: favContent, thumbnail, category }) => { const item = document.createElement('div'); item.style.cssText = ` display: flex; align-items: center; padding: 5px; border-bottom: 1px solid ${CONSTANTS.THEMES[theme].border}; gap: 10px; `; if (thumbnail) { const img = document.createElement('img'); img.src = thumbnail; img.alt = '縮圖'; img.style.cssText = ` width: 50px; height: 50px; object-fit: cover; border-radius: 3px; `; item.appendChild(img); } const contentWrapper = document.createElement('div'); contentWrapper.style.flex = '1'; const link = document.createElement('a'); link.href = url; link.textContent = `${id}: ${favContent.substring(0, 30)}...`; link.style.cssText = ` display: block; text-decoration: none; color: ${CONSTANTS.THEMES[theme].text}; margin-bottom: 5px; `; const categoryTag = document.createElement('span'); categoryTag.textContent = category; categoryTag.style.cssText = ` font-size: 0.8em; padding: 2px 5px; background-color: ${CONSTANTS.THEMES[theme].header}40; border-radius: 3px; color: ${CONSTANTS.THEMES[theme].text}; `; contentWrapper.appendChild(link); contentWrapper.appendChild(categoryTag); item.appendChild(contentWrapper); // 操作按鈕容器 const actions = document.createElement('div'); actions.style.display = 'flex'; actions.style.gap = '5px'; // 編輯分類按鈕 // 編輯分類按鈕 const editCategoryBtn = document.createElement('span'); editCategoryBtn.textContent = '✎'; editCategoryBtn.title = '編輯分類'; editCategoryBtn.style.cssText = ` cursor: pointer; color: ${CONSTANTS.THEMES[theme].text}; `; editCategoryBtn.addEventListener('click', () => { const newCategory = prompt('請輸入分類名稱:', category); if (newCategory !== null && newCategory.trim() !== '') { const fav = favorites.find(f => f.id === id); if (fav) { fav.category = newCategory.trim(); localStorage.setItem(CONSTANTS.STORAGE_KEYS.FAVORITES, JSON.stringify(favorites)); utils.updateCategories(); // 更新分類列表 updateFavoritesWindow(); utils.showToast('已更新分類', 'success'); } } }); // 刪除按鈕 const removeBtn = document.createElement('span'); removeBtn.textContent = '✕'; removeBtn.title = '移除收藏'; removeBtn.style.cssText = ` cursor: pointer; color: ${CONSTANTS.THEMES[theme].text}; `; removeBtn.addEventListener('click', () => { favorites = favorites.filter(fav => fav.id !== id); localStorage.setItem(CONSTANTS.STORAGE_KEYS.FAVORITES, JSON.stringify(favorites)); utils.updateCategories(); // 更新分類列表 updateFavoritesWindow(); utils.showToast('已移除收藏', 'info'); }); actions.appendChild(editCategoryBtn); actions.appendChild(removeBtn); item.appendChild(actions); fragment.appendChild(item); }); content.appendChild(fragment); } function init() { addCollectButtons(); createFavoritesWindow(); } // 監聽 DOM 變化以添加收藏按鈕 const observer = new MutationObserver(() => { addCollectButtons(); }); observer.observe(document.body, { childList: true, subtree: true }); init(); })();