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