// ==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 = `
`;
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();
}
})();