右上角固定顯示最近閱覽,支援刪除與明/暗主題
当前为
// ==UserScript==
// @name 巴哈姆特哈拉區顯示最近閱覽
// @namespace https://greasyfork.org/zh-TW/users/1537796-meredith2u
// @version 1.2
// @description 右上角固定顯示最近閱覽,支援刪除與明/暗主題
// @author meredith
// @match https://forum.gamer.com.tw/*
// @exclude https://forum.gamer.com.tw/A.php*
// @exclude https://forum.gamer.com.tw/post*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const THEME_COOKIE = 'custom_lastboard_theme';
const LIST_COOKIE = 'ckBH_lastBoard';
const PANEL_ID = 'custom-float-panel';
const DEBOUNCE_DELAY = 100;
// Cookie 工具
const getCookie = name => {
const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
return match ? decodeURIComponent(match[1]) : null;
};
const setCookie = (name, value, days = 365) => {
const date = new Date(Date.now() + days * 864e5);
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${date.toUTCString()}; path=/; domain=.gamer.com.tw`;
};
// 讀取並驗證資料
const loadList = () => {
const raw = getCookie(LIST_COOKIE);
if (!raw) return null;
let data;
try { data = JSON.parse(raw); } catch { return null; }
if (!Array.isArray(data)) return null;
// 過濾有效項目:必須是 [number, string]
return data.filter(item =>
Array.isArray(item) &&
item.length === 2 &&
typeof item[0] === 'string' &&
item[0].trim() &&
typeof item[1] === 'string' &&
item[1].trim()
);
};
let list = loadList();
if (!list || list.length === 0) return;
let currentTheme = getCookie(THEME_COOKIE) || 'dark';
// 主題定義
const themes = {
light: {
bg: 'rgba(255,255,255,0.95)', border: '#e0e0e0', color: '#2d2d2d',
title: '#1a1a1a', link: '#1a1a1a', hoverLink: '#0066cc',
divider: '#d0d0d0', itemDiv: '#e5e5e5', hover: 'rgba(0,0,0,0.04)',
delBg: 'rgba(255,102,102,0.15)', delBorder: 'rgba(255,102,102,0.3)',
delColor: '#e63946', delHover: 'rgba(255,102,102,0.3)'
},
dark: {
bg: 'rgba(15,15,15,0.92)', border: '#333', color: '#e0e0e0',
title: '#ffffff', link: '#ffffff', hoverLink: '#ffffff',
divider: '#444', itemDiv: '#333', hover: 'rgba(255,255,255,0.06)',
delBg: 'rgba(255,68,68,0.15)', delBorder: 'rgba(255,68,68,0.3)',
delColor: '#ff6b6b', delHover: 'rgba(255,68,68,0.3)'
}
};
// 建立面板
const createPanel = () => {
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.style.cssText = `
position:fixed;top:65px;right:20px;width:${location.pathname === '/' ? '280px' : '250px'};
max-height:80vh;overflow-y:auto;border-radius:12px;padding:14px;
font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
box-shadow:0 8px 24px rgba(0,0,0,0.6);z-index:30;transition:all .25s ease;
backdrop-filter:blur(8px);user-select:none;
`;
const header = document.createElement('div');
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;';
const title = document.createElement('h3');
title.textContent = '最近閱覽';
title.style.cssText = 'margin:0;font-size:16px;font-weight:600;';
header.appendChild(title);
const toggle = document.createElement('button');
toggle.id = 'theme-toggle';
toggle.style.cssText = `
background:none;border:none;font-size:18px;cursor:pointer;padding:4px;
border-radius:6px;width:32px;height:32px;display:flex;align-items:center;
justify-content:center;transition:all .2s ease;
`;
header.appendChild(toggle);
panel.appendChild(header);
const topDiv = document.createElement('div');
topDiv.id = 'top-divider';
topDiv.style.cssText = 'height:1px;margin:8px 0;';
panel.appendChild(topDiv);
const container = document.createElement('div');
container.addEventListener('click', handleContainerClick);
container.addEventListener('mouseenter', handleMouseEnter, true);
container.addEventListener('mouseleave', handleMouseLeave, true);
panel.appendChild(container);
toggle.addEventListener('click', () => {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
setCookie(THEME_COOKIE, currentTheme);
applyThemeToPanel();
renderList(); // 重新渲染以更新 icon
});
return { panel, container, title, toggle, topDiv };
};
const { panel, container, title, toggle, topDiv } = createPanel();
// 事件委派處理
function handleContainerClick(e) {
const delBtn = e.target.closest('.delete-btn');
if (!delBtn) return;
const item = delBtn.closest('.list-item');
if (!item) return;
const name = item.dataset.name;
if (!name || !confirm(`確定要從最近閱覽中移除「${name}」嗎?`)) return;
const id = item.dataset.id;
list = list.filter(([bid]) => bid !== id);
setCookie(LIST_COOKIE, list.length ? JSON.stringify(list) : '', 365);
if (list.length === 0) {
panel.remove();
return;
}
renderList();
}
function handleMouseEnter(e) {
const item = e.target.closest('.list-item');
if (!item) return;
const del = item.querySelector('.delete-btn');
if (del) {
item.style.backgroundColor = themes[currentTheme].hover;
del.style.opacity = '1';
del.style.transform = 'translateX(0)';
}
}
function handleMouseLeave(e) {
const item = e.target.closest('.list-item');
if (!item) return;
const del = item.querySelector('.delete-btn');
if (del) {
item.style.backgroundColor = '';
del.style.opacity = '0';
del.style.transform = 'translateX(8px)';
}
}
// 渲染清單(僅結構)
const renderList = () => {
const frag = document.createDocumentFragment();
const t = themes[currentTheme];
list.forEach(([id, name]) => {
const item = document.createElement('div');
item.className = 'list-item';
item.dataset.id = id;
item.dataset.name = name;
item.style.cssText = `
margin:0 0 10px;display:flex;justify-content:space-between;align-items:center;
padding:2px 3px;border-radius:3px;transition:background .2s;position:relative;
`;
const link = document.createElement('a');
link.href = `B.php?bsn=${id}`;
link.textContent = name;
link.style.cssText = `
flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
text-decoration:none;font-weight:500;font-size:15px;color:${t.link};
transition:color .2s;
`;
link.addEventListener('mouseover', () => link.style.color = t.hoverLink);
link.addEventListener('mouseout', () => link.style.color = t.link);
item.appendChild(link);
const del = document.createElement('button');
del.className = 'delete-btn';
del.textContent = '×';
del.title = '刪除此項目';
del.style.cssText = `
width:26px;height:26px;border-radius:50%;font-size:16px;font-weight:bold;
display:flex;align-items:center;justify-content:center;cursor:pointer;
opacity:0;transform:translateX(8px);transition:all .22s;pointer-events:auto;
background:${t.delBg};border:1px solid ${t.delBorder};color:${t.delColor};
`;
del.addEventListener('mouseenter', () => {
del.style.background = t.delHover;
del.style.transform = 'translateX(0) scale(1.1)';
});
del.addEventListener('mouseleave', () => {
del.style.background = t.delBg;
del.style.transform = 'translateX(0) scale(1)';
});
item.appendChild(del);
const div = document.createElement('div');
div.className = 'item-divider';
div.style.cssText = `height:1px;margin:8px 0;background:${t.itemDiv};`;
frag.appendChild(item);
frag.appendChild(div);
});
// 移除最後一個 divider
if (frag.lastChild?.classList.contains('item-divider')) {
frag.removeChild(frag.lastChild);
}
container.innerHTML = '';
container.appendChild(frag);
applyThemeToPanel();
};
// 僅更新面板樣式(不重繪內容)
const applyThemeToPanel = () => {
const t = themes[currentTheme];
const isLight = currentTheme === 'light';
Object.assign(panel.style, {
background: t.bg,
border: `1px solid ${t.border}`,
color: t.color,
boxShadow: `0 8px 24px rgba(0,0,0,${isLight ? '0.08' : '0.6'})`
});
title.style.color = t.title;
topDiv.style.background = `linear-gradient(to right, transparent, ${t.divider}, transparent)`;
toggle.textContent = isLight ? '🌙' : '☀️';
toggle.title = isLight ? '切換至黑暗模式' : '切換至明亮模式';
};
// 插入 DOM
document.body.appendChild(panel);
renderList();
// 防抖重建
let rebuildTimeout;
const scheduleRebuild = () => {
clearTimeout(rebuildTimeout);
rebuildTimeout = setTimeout(() => {
const newList = loadList();
if (newList && newList.length > 0 && !document.body.contains(panel)) {
list = newList;
document.body.appendChild(panel);
renderList();
}
}, DEBOUNCE_DELAY);
};
// 監聽 DOM 變化
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.removedNodes.length && Array.from(m.removedNodes).some(n => n.contains?.(panel))) {
scheduleRebuild();
break;
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
// 清理
window.addEventListener('unload', () => {
observer.disconnect();
clearTimeout(rebuildTimeout);
});
})();