您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Favourite pages on bustimes.org to the homepage. Drag to reorder. Dark/light mode compatible.
// ==UserScript== // @name BusTimes Favourites // @namespace https://bustimes.org/ // @version 1.9 // @description Favourite pages on bustimes.org to the homepage. Drag to reorder. Dark/light mode compatible. // @author dylan // @match https://bustimes.org/* // @grant none // ==/UserScript== (function () { 'use strict'; const FAVORITES_KEY = 'bustimes_favorites'; function getFavorites() { return JSON.parse(localStorage.getItem(FAVORITES_KEY) || '[]'); } function saveFavorites(favs) { localStorage.setItem(FAVORITES_KEY, JSON.stringify(favs)); } function isFavorited(url) { return getFavorites().some(fav => fav.url === url); } function toggleFavorite() { const url = window.location.href; const title = document.title.replace(' – bustimes.org', ''); let favs = getFavorites(); if (isFavorited(url)) { favs = favs.filter(fav => fav.url !== url); } else { favs.push({ url, title }); } saveFavorites(favs); updateStar(); } function getStarColor() { return document.body.classList.contains('dark-mode') ? '#ffcc00' : '#f5a623'; } function updateStar() { const star = document.getElementById('bustimes-fav-star'); if (!star) return; star.textContent = isFavorited(window.location.href) ? '★' : '☆'; star.style.color = getStarColor(); } function addStarButton() { const header = document.querySelector('header.site-header'); const searchForm = header?.querySelector('form.search'); const searchInput = searchForm?.querySelector('input[type="search"]'); if (!header || !searchForm || !searchInput) return; const starBtn = document.createElement('button'); starBtn.id = 'bustimes-fav-star'; starBtn.textContent = isFavorited(window.location.href) ? '★' : '☆'; starBtn.title = 'Click to favourite this page'; starBtn.style.fontSize = '20px'; starBtn.style.marginRight = '0.5rem'; starBtn.style.paddingLeft = '18px'; starBtn.style.cursor = 'pointer'; starBtn.style.border = 'none'; starBtn.style.background = 'transparent'; starBtn.style.color = getStarColor(); starBtn.onclick = toggleFavorite; const mapLink = header.querySelector('a[href="/map"]'); if (mapLink) { mapLink.parentElement.insertBefore(starBtn, mapLink); } const observer = new MutationObserver(updateStar); observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); } function displayFavoritesOnHomepage() { if (location.pathname !== '/') return; const favs = getFavorites(); if (favs.length === 0) return; const container = document.createElement('div'); container.style.margin = '1rem'; container.style.padding = '1rem'; container.style.border = '1px solid #ccc'; container.style.borderRadius = '8px'; container.style.background = document.body.classList.contains('dark-mode') ? '#333' : '#f9f9f9'; container.style.color = document.body.classList.contains('dark-mode') ? '#eee' : '#000'; const title = document.createElement('h2'); title.textContent = '⭐ Your Favourites'; title.style.marginBottom = '0.5rem'; container.appendChild(title); const list = document.createElement('ul'); list.id = 'bustimes-fav-list'; list.style.listStyle = 'none'; list.style.padding = '0'; list.style.margin = '0'; favs.forEach((fav, index) => { const li = document.createElement('li'); li.draggable = true; li.dataset.index = index; li.style.padding = '0.5rem'; li.style.marginBottom = '0.5rem'; li.style.cursor = 'move'; li.style.background = document.body.classList.contains('dark-mode') ? '#444' : '#fff'; li.style.border = '1px solid #ccc'; li.style.borderRadius = '4px'; li.style.transition = 'background 0.2s ease'; // Added flexbox styles to align link and delete button li.style.display = 'flex'; li.style.alignItems = 'center'; li.style.justifyContent = 'space-between'; const a = document.createElement('a'); a.href = fav.url; a.textContent = fav.title; a.style.color = 'inherit'; a.style.textDecoration = 'none'; a.draggable = false; const deleteBtn = document.createElement('button'); deleteBtn.textContent = '✖'; // cross symbol deleteBtn.title = 'Remove from favourites'; deleteBtn.style.background = 'transparent'; deleteBtn.style.border = 'none'; deleteBtn.style.color = document.body.classList.contains('dark-mode') ? '#f88' : '#d00'; deleteBtn.style.cursor = 'pointer'; deleteBtn.style.fontSize = '16px'; deleteBtn.style.marginLeft = '10px'; deleteBtn.draggable = false; deleteBtn.onclick = () => { let favs = getFavorites(); favs = favs.filter(f => f.url !== fav.url); saveFavorites(favs); // Refresh the list UI: const container = li.closest('div'); container.remove(); displayFavoritesOnHomepage(); }; li.appendChild(a); li.appendChild(deleteBtn); list.appendChild(li); }); container.appendChild(list); const main = document.querySelector('main'); if (main) { main.prepend(container); } else { document.body.prepend(container); } setupDragAndDrop(list); } function setupDragAndDrop(listElement) { let draggedEl = null; listElement.addEventListener('dragstart', (e) => { draggedEl = e.target; e.target.classList.add('dragging'); }); listElement.addEventListener('dragend', (e) => { e.target.classList.remove('dragging'); }); listElement.addEventListener('dragover', (e) => { e.preventDefault(); const afterElement = getDragAfterElement(listElement, e.clientY); if (afterElement == null) { listElement.appendChild(draggedEl); } else { listElement.insertBefore(draggedEl, afterElement); } }); listElement.addEventListener('drop', () => { const newOrder = Array.from(listElement.children).map(li => { const link = li.querySelector('a'); return { url: link.href, title: link.textContent }; }); saveFavorites(newOrder); }); const style = document.createElement('style'); style.textContent = ` #bustimes-fav-list li.dragging { opacity: 0.5; background: #999 !important; } #bustimes-fav-list { user-select: none; } `; document.head.appendChild(style); } function getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('li:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } // Init addStarButton(); displayFavoritesOnHomepage(); })();