bustimes.org - Favourites

Favourite pages on bustimes.org to the homepage. Drag to reorder. Dark/light mode compatible.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         bustimes.org - 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();
})();