VNDB Friends List

Add friends list functionality to VNDB user pages

// ==UserScript==
// @name         VNDB Friends List
// @namespace    http://tampermonkey.net/
// @version      1.54
// @description  Add friends list functionality to VNDB user pages
// @author       ALVIBO
// @match        https://vndb.org/u*
// @match        https://vndb.org/t/u*
// @match        https://vndb.org/w?u=u*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js
// @license     http://creativecommons.org/licenses/by-nc-sa/4.0/
// @thanks     For the cover preview on mouseover, I drew some inspiration and used a few lines from the original VNDB Cover Preview script by Kuro_scripts
// ==/UserScript==

(function() {
    'use strict';

    // Check if we're on a valid user page and if edit link exists
    const userPageMatch = location.pathname.match(/^\/u\d+/) ||
                         location.pathname.match(/^\/t\/u\d+/) ||
                         location.search.match(/[?&]u=u\d+/);
    if (!userPageMatch) return;

    const editLink = document.querySelector('header nav menu li a[href$="/edit"]');
    if (!editLink) return;

    // Initialize state
    let friends = GM_getValue('vndb_friends', []);
    let friendsCache = GM_getValue('vndb_friends_cache', {});
    let currentPage = 1;
    const friendsPerPage = 10;
    let settings = GM_getValue('vndb_friends_settings', {
        textColor: null,
        buttonTextColor: null,
        backgroundColor: null,
        buttonBackgroundColor: null,
        titleColor: null,
        borderColor: null,
        separatorColor: null,
        fontSize: 17,
        buttonFontSize: 16,
        tabFontSize: 18,
        opacity: null,
        cacheDuration: 3,
        gamesPerFriend: 5,
        maxActivities: 51
    });

    // Get the base user URL (without additional paths)
    const baseUserUrl = location.pathname.split('/')[1] + (location.pathname.split('/')[2] || '');

    // Function to get computed background color
    function getBackgroundColor() {
        const bodyBg = window.getComputedStyle(document.body).backgroundColor;
        const rgb = bodyBg.match(/\d+/g);
        return rgb ? rgb.map(Number) : [255, 255, 255];
    }

    // Function to create theme-matching color
    function createThemeColors() {
        const bgColor = getBackgroundColor();
        const mainTextColor = window.getComputedStyle(document.body).color;
        const articleH1 = document.querySelector('article h1');
        const titleColor = articleH1 ? window.getComputedStyle(articleH1).color : mainTextColor;
        const opacity = settings.opacity || 0.70;

        return {
            containerBg: settings.backgroundColor ?
                `rgba(${parseInt(settings.backgroundColor.slice(1,3),16)},
                      ${parseInt(settings.backgroundColor.slice(3,5),16)},
                      ${parseInt(settings.backgroundColor.slice(5,7),16)},
                      ${opacity})` :
                `rgba(${bgColor[0]}, ${bgColor[1]}, ${bgColor[2]}, ${opacity})`,
            borderColor: mainTextColor,
            textColor: settings.textColor || mainTextColor,
            linkColor: settings.titleColor || titleColor
        };
    }

    // Create friends list container
    const friendsContainer = document.createElement('div');
    const themeColors = createThemeColors();

    friendsContainer.innerHTML = `
        <style>
            .friends-container {
                display: none;
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                padding: 20px;
                border: 1px solid ${themeColors.borderColor};
                z-index: 1000;
                min-width: 300px;
                font-size: ${settings.fontSize || '17px'};
                max-height: 80vh;
                max-width: 90vw;
                overflow-y: auto;
            }

            .friends-container::before {
                content: '';
                position: absolute;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                backdrop-filter: blur(5px);
                z-index: -1;
            }

            .friends-settings {
                margin-top: 10px;
                border-top: 1px solid ${themeColors.borderColor};
                padding-top: 10px;
                display: none;
            }
            .settings-group {
                margin: 5px 0;
                display: flex;
                align-items: center;
                gap: 5px;
            }
            .settings-group label {
                min-width: 120px;
            }
            .color-inputs {
                display: flex;
                gap: 5px;
                align-items: center;
            }
            .color-inputs input[type="text"],
            .color-inputs input[type="number"] {
                width: 70px;
                padding: 2px 4px;
                border: 1px solid;
                border-radius: 3px;
                background: inherit;
            }
            .settings-toggle {
                margin-top: 10px;
                text-align: center;
            }
            .friends-container h2,
            .friends-container h3 {
                color: ${themeColors.linkColor};
            }
            .friends-container .friend-link {
                color: ${themeColors.textColor} !important;
            }
            .tab-buttons {
                display: flex;
                margin-bottom: 15px;
                border-bottom: 1px solid ${themeColors.borderColor};
            }
            .tab-button {
                padding: 8px 16px;
                border: none;
                background: none;
                color: ${themeColors.textColor};
                cursor: pointer;
            }
            .tab-button.active {
                border-bottom: 2px solid ${themeColors.linkColor};
            }
            .tab-content {
                display: none;
            }
            .tab-content.active {
                display: block;
            }
            .activity-item {
                margin: 8px 0;
                padding: 8px;
                border-bottom: 1px solid ${settings.separatorColor || themeColors.borderColor};
                word-break: break-word;
                overflow-wrap: break-word;
            }
            .activity-date {
                color: ${themeColors.textColor};
                opacity: 0.8;
                font-size: 0.9em;
            }
            .friends-container::-webkit-scrollbar {
                width: 8px;
            }
            .friends-container::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.1);
            }
            .friends-container::-webkit-scrollbar-thumb {
                background: rgba(128, 128, 128, 0.5);
                border-radius: 4px;
            }
            #activityFeed {
                max-height: calc(80vh - 300px);
                overflow-y: auto;
                margin-bottom: 15px;
            }
            #activityFeed::-webkit-scrollbar {
                width: 8px;
            }
            #activityFeed::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.1);
            }
            #activityFeed::-webkit-scrollbar-thumb {
                background: rgba(128, 128, 128, 0.5);
                border-radius: 4px;
            }
            .activity-controls {
                margin-top: 10px;
                text-align: center;
            }
            .friends-container button:not(.tab-button) {
                font-size: ${settings.buttonFontSize ? `${settings.buttonFontSize}px` : '16px'} !important;
            }

            .tab-button {
                font-size: ${settings.tabFontSize ? `${settings.tabFontSize}px` : '18px'} !important;
            }
        </style>
        <div class="friends-container">
            <h2>Friends List</h2>
            <div class="tab-buttons">
                <button class="tab-button active" data-tab="friendsList">Friends List</button>
                <button class="tab-button" data-tab="activityFeed">Recent Activity</button>
            </div>
            <div id="friendsList" class="tab-content active"></div>
            <div id="activityFeed" class="tab-content"></div>
            <div class="activity-controls tab-content" data-tab="activityFeed">
                <button id="reloadActivity">Reload Activity</button>
            </div>
            <div id="pagination" style="margin-top: 10px; text-align: center;"></div>
            <div style="margin-top: 10px;">
                <input type="text" id="newFriend" placeholder="Username" style="margin-right: 5px;">
                <button id="addFriend">Add Friend</button>
            </div>
            <div class="settings-toggle">
                <button id="toggleSettings">Show Settings</button>
            </div>
            <div class="friends-settings">
                <h3>Settings</h3>
                <!-- Text Colors -->
                <div class="settings-group">
                    <label>Title Color:</label>
                    <div class="color-inputs">
                        <input type="color" id="titleColor">
                        <input type="text" id="titleColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="titleColor">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Text Color:</label>
                    <div class="color-inputs">
                        <input type="color" id="textColor">
                        <input type="text" id="textColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="textColor">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Button Text:</label>
                    <div class="color-inputs">
                        <input type="color" id="buttonTextColor">
                        <input type="text" id="buttonTextColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="buttonTextColor">Reset</button>
                </div>

                <!-- Background Colors -->
                <div class="settings-group">
                    <label>Background:</label>
                    <div class="color-inputs">
                        <input type="color" id="backgroundColor">
                        <input type="text" id="backgroundColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="backgroundColor">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Button Color:</label>
                    <div class="color-inputs">
                        <input type="color" id="buttonBackgroundColor">
                        <input type="text" id="buttonBackgroundColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="buttonBackgroundColor">Reset</button>
                </div>

                <!-- Border Colors -->
                <div class="settings-group">
                    <label>Border Color:</label>
                    <div class="color-inputs">
                        <input type="color" id="borderColor">
                        <input type="text" id="borderColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="borderColor">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Separator Color:</label>
                    <div class="color-inputs">
                        <input type="color" id="separatorColor">
                        <input type="text" id="separatorColorHex" placeholder="#hex">
                    </div>
                    <button class="resetButton" data-setting="separatorColor">Reset</button>
                </div>

                <!-- Other Settings -->
                <div class="settings-group">
                    <label>Font Size:</label>
                    <div class="color-inputs">
                        <input type="number" id="fontSize" min="8" max="24" step="1">
                        <span>px</span>
                    </div>
                    <button class="resetButton" data-setting="fontSize">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Button Text Size:</label>
                    <div class="color-inputs">
                        <input type="number" id="buttonFontSize" min="8" max="24" step="1">
                        <span>px</span>
                    </div>
                    <button class="resetButton" data-setting="buttonFontSize">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Tab Text Size:</label>
                    <div class="color-inputs">
                        <input type="number" id="tabFontSize" min="8" max="24" step="1">
                        <span>px</span>
                    </div>
                    <button class="resetButton" data-setting="tabFontSize">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Opacity:</label>
                    <input type="range" id="opacity" min="0" max="100" step="5">
                    <span id="opacityValue"></span>%
                    <button class="resetButton" data-setting="opacity">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Cache Duration:</label>
                    <div class="color-inputs">
                        <input type="number" id="cacheDuration" min="1" max="60" step="1">
                        <span>minutes</span>
                    </div>
                    <button class="resetButton" data-setting="cacheDuration">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Games per Friend:</label>
                    <div class="color-inputs">
                        <input type="number" id="gamesPerFriend" min="1" max="50" step="1">
                        <span>games</span>
                    </div>
                    <button class="resetButton" data-setting="gamesPerFriend">Reset</button>
                </div>
                <div class="settings-group">
                    <label>Max Activities:</label>
                    <div class="color-inputs">
                        <input type="number" id="maxActivities" min="5" max="100" step="1">
                        <span>total</span>
                    </div>
                    <button class="resetButton" data-setting="maxActivities">Reset</button>
                </div>
            </div>
            <button id="closeFriends" style="margin-top: 10px;">Close</button>
        </div>
    `;
    document.body.appendChild(friendsContainer);

    const container = friendsContainer.querySelector('.friends-container');
    const settingsPanel = container.querySelector('.friends-settings');
    updateContainerStyle();

    // Function to update container style
    function updateContainerStyle() {
        const themeColors = createThemeColors();

        container.style.border = `1px solid ${settings.borderColor || themeColors.borderColor}`;
        container.style.background = themeColors.containerBg;
        container.style.color = settings.textColor || themeColors.textColor;
        container.style.fontSize = settings.fontSize ? `${settings.fontSize}px` : '17px';

        // Update titles specifically
        const titles = container.querySelectorAll('h2, h3');
        titles.forEach(title => {
            title.style.setProperty('color', settings.titleColor || themeColors.linkColor, 'important');
        });

        // Update other elements
        const friendLinks = container.querySelectorAll('.friend-link');
        friendLinks.forEach(link => {
            link.style.setProperty('color', settings.textColor || themeColors.textColor, 'important');
        });
    }

    // Create a style element for dynamic CSS rules
    const dynamicStyles = document.createElement('style');
    document.head.appendChild(dynamicStyles);

    // Function to update dynamic CSS rules
    function updateDynamicStyles() {
        const themeColors = createThemeColors();
        const opacity = settings.opacity || 0.70;

        // Convert hex background color to rgba if it exists
        let backgroundStyle = themeColors.containerBg;
        if (settings.backgroundColor) {
            const hex = settings.backgroundColor.replace('#', '');
            const r = parseInt(hex.substring(0, 2), 16);
            const g = parseInt(hex.substring(2, 4), 16);
            const b = parseInt(hex.substring(4, 6), 16);
            backgroundStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`;
        }

        dynamicStyles.textContent = `
            .friends-container {
                border: 1px solid ${settings.borderColor || themeColors.borderColor} !important;
                background: ${backgroundStyle} !important;
            }
            .friends-container .friend-link {
                color: ${settings.textColor || themeColors.textColor} !important;
            }
            .friends-container h2,
            .friends-container h3 {
                color: ${settings.titleColor || themeColors.linkColor} !important;
            }
            .friends-container button {
                background-color: ${settings.buttonBackgroundColor || 'inherit'} !important;
                color: ${settings.buttonTextColor || themeColors.textColor} !important;
                font-size: ${settings.buttonFontSize ? `${settings.buttonFontSize}px` : '16px'} !important;
            }
            .friends-settings {
                border-top: 1px solid ${settings.separatorColor || themeColors.borderColor} !important;
            }
            .activity-item {
                border-bottom: 1px solid ${settings.separatorColor || themeColors.borderColor} !important;
            }
            .activity-date {
                color: ${settings.textColor || themeColors.textColor} !important;
                opacity: 0.8;
            }
            .tab-button {
                font-size: ${settings.tabFontSize ? `${settings.tabFontSize}px` : '18px'} !important;
            }
        `;
    }

    // Update both styles when theme changes
    function forceStyleUpdate() {
        updateContainerStyle();
        updateDynamicStyles();

        // Force update of activity items if they exist
        const activityItems = document.querySelectorAll('.activity-item');
        if (activityItems.length > 0) {
            const themeColors = createThemeColors();
            activityItems.forEach(item => {
                item.style.borderBottom = `1px solid ${settings.separatorColor || themeColors.borderColor}`;
            });

            const activityDates = document.querySelectorAll('.activity-date');
            activityDates.forEach(date => {
                date.style.color = settings.textColor || themeColors.textColor;
            });
        }

        // Force update button and tab font sizes with !important
        const buttons = container.querySelectorAll('button:not(.tab-button)');
        buttons.forEach(button => {
            if (settings.buttonFontSize) {
                button.style.setProperty('font-size', `${settings.buttonFontSize}px`, 'important');
            } else {
                button.style.removeProperty('font-size');
            }
        });

        const tabButtons = container.querySelectorAll('.tab-button');
        tabButtons.forEach(tab => {
            if (settings.tabFontSize) {
                tab.style.setProperty('font-size', `${settings.tabFontSize}px`, 'important');
            } else {
                tab.style.removeProperty('font-size');
            }
        });
    }

    // Modify the mutation observer to be more aggressive
    const themeObserver = new MutationObserver((mutations) => {
        if (container.style.display === 'block') {
            forceStyleUpdate();
            // Multiple delayed updates to ensure changes are caught
            setTimeout(forceStyleUpdate, 100);
            setTimeout(forceStyleUpdate, 300);
            setTimeout(forceStyleUpdate, 500);
        }
    });

    // Observe both document head and body
    themeObserver.observe(document.head, {
        attributes: true,
        childList: true,
        subtree: true
    });

    themeObserver.observe(document.body, {
        attributes: true,
        childList: true,
        subtree: true
    });

    // More frequent checks
    setInterval(() => {
        if (container.style.display === 'block') {
            forceStyleUpdate();
        }
    }, 250);

    // Settings management
    function resetSetting(setting) {
        switch(setting) {
            case 'cacheDuration':
                settings[setting] = 3;
                const cacheDurationInput = document.getElementById('cacheDuration');
                if (cacheDurationInput) {
                    cacheDurationInput.value = 3;
                    // Clear existing cache when duration is changed
                    activityCache.timestamp = 0;
                    sessionStorage.removeItem('vndb_activity_cache');
                    // Only update if we're on the activity tab
                    if (document.querySelector('.tab-button[data-tab="activityFeed"]').classList.contains('active')) {
                        updateActivityFeed();
                    }
                }
                break;
            case 'gamesPerFriend':
                settings[setting] = 5;
                const gamesPerFriendInput = document.getElementById('gamesPerFriend');
                if (gamesPerFriendInput) gamesPerFriendInput.value = 5;
                break;
            case 'maxActivities':
                settings[setting] = 51;
                const maxActivitiesInput = document.getElementById('maxActivities');
                if (maxActivitiesInput) maxActivitiesInput.value = 51;
                break;
            case 'fontSize':
                settings[setting] = 17;
                const fontSizeInput = document.getElementById('fontSize');
                if (fontSizeInput) fontSizeInput.value = 17;
                break;
            case 'buttonFontSize':
                settings[setting] = 16;
                const buttonFontSizeInput = document.getElementById('buttonFontSize');
                if (buttonFontSizeInput) buttonFontSizeInput.value = 16;
                break;
            case 'tabFontSize':
                settings[setting] = 18;
                const tabFontSizeInput = document.getElementById('tabFontSize');
                if (tabFontSizeInput) tabFontSizeInput.value = 18;
                break;
            case 'opacity':
                settings[setting] = 0.70;
                const opacityInput = document.getElementById('opacity');
                const opacityValue = document.getElementById('opacityValue');
                if (opacityInput) {
                    opacityInput.value = 70;
                    opacityValue.textContent = '70';
                }
                break;
            default:
                settings[setting] = null;
                const colorInput = document.getElementById(setting);
                const hexInput = document.getElementById(setting + 'Hex');
                if (colorInput && hexInput) {
                    colorInput.value = '#000000';
                    hexInput.value = '';
                }
        }

        GM_setValue('vndb_friends_settings', settings);
        forceStyleUpdate();
    }

    // Initialize settings inputs
    const settingsInputs = {
        textColor: document.getElementById('textColor'),
        backgroundColor: document.getElementById('backgroundColor'),
        fontSize: document.getElementById('fontSize'),
        opacity: document.getElementById('opacity')
    };

    Object.entries(settingsInputs).forEach(([setting, input]) => {
        if (settings[setting]) {
            if (setting === 'opacity') {
                input.value = settings[setting] * 100;
                document.getElementById('opacityValue').textContent = input.value;
            } else if (setting === 'fontSize') {
                input.value = settings[setting];
            } else {
                input.value = settings[setting];
            }
        } else if (setting === 'opacity') {
            input.value = 70;
            document.getElementById('opacityValue').textContent = '70';
        }

        input.addEventListener('change', function() {
            let value = this.value;
            if (setting === 'opacity') {
                value = this.value / 100;
                document.getElementById('opacityValue').textContent = this.value;
            } else if (setting === 'fontSize') {
                value = parseInt(this.value);
            }
            settings[setting] = value;
            GM_setValue('vndb_friends_settings', settings);
            updateContainerStyle();
        });
    });

    // Add Friends link to menu only if it doesn't already exist
    const menu = document.querySelector('header nav menu');
    if (!menu.querySelector('li a[href="#"]')) {
        const friendsLink = document.createElement('li');
        friendsLink.innerHTML = `<a href="#">friends</a>`;
        menu.appendChild(friendsLink);
    }

    // Modify this section to ensure pagination buttons are hidden on Recent Activity tab
    function updatePagination() {
        const totalPages = Math.ceil(friends.length / friendsPerPage);
        const pagination = document.getElementById('pagination');

        const activeTab = sessionStorage.getItem('vndb_friends_active_tab') || 'friendsList';

        if (activeTab === 'activityFeed') {
            pagination.style.display = 'none';
            return;
        }

        if (totalPages <= 1) {
            pagination.style.display = 'none';
            return;
        }

        pagination.style.display = 'block';
        pagination.innerHTML = `
            ${currentPage > 1 ? `<button class="pageButton" data-page="${currentPage - 1}">←</button>` : ''}
            Page ${currentPage} of ${totalPages}
            ${currentPage < totalPages ? `<button class="pageButton" data-page="${currentPage + 1}">→</button>` : ''}
        `;

        // Add event listeners to the newly created pagination buttons
        const pageButtons = pagination.querySelectorAll('.pageButton');
        pageButtons.forEach(button => {
            button.addEventListener('click', function() {
                changePage(parseInt(this.dataset.page));
            });
        });
    }

    // Ensure pagination visibility updates correctly on tab switches
    function handleTabSwitch(tabId) {
        const pagination = document.getElementById('pagination');

        if (tabId === 'activityFeed') {
            pagination.style.display = 'none';
        } else if (tabId === 'friendsList') {
            const totalPages = Math.ceil(friends.length / friendsPerPage);
            pagination.style.display = totalPages > 1 ? 'block' : 'none';
        }

        // Always call updatePagination to refresh buttons when switching tabs
        updatePagination();
    }

    // Listen for tab switches and update pagination accordingly
    document.querySelectorAll('.tab-button').forEach(button => {
        button.addEventListener('click', () => {
            const tabId = button.dataset.tab;
            sessionStorage.setItem('vndb_friends_active_tab', tabId);
            handleTabSwitch(tabId);
        });
    });

    // Ensure pagination is not displayed even after a browser back action
    window.addEventListener('load', () => {
        const activeTab = sessionStorage.getItem('vndb_friends_active_tab') || 'friendsList';
        handleTabSwitch(activeTab);
    });

    // Function to change page
    function changePage(newPage) {
        currentPage = newPage;
        displayFriendsList();
    }

    // Function to display friends list from cache
    function displayFriendsList() {
        const friendsList = document.getElementById('friendsList');
        friendsList.innerHTML = '';

        const startIndex = (currentPage - 1) * friendsPerPage;
        const endIndex = startIndex + friendsPerPage;
        const currentFriends = friends.slice(startIndex, endIndex);

        for (const friend of currentFriends) {
            const userData = friendsCache[friend];
            if (userData) {
                const friendDiv = document.createElement('div');
                friendDiv.style.margin = '5px 0';
                friendDiv.innerHTML = `
                    <a href="/u${userData.id.slice(1)}" class="friend-link">${userData.username}</a>
                    <button class="removeFriend" data-username="${friend}" style="margin-left: 10px;">Remove</button>
                `;
                friendsList.appendChild(friendDiv);
            }
        }

        // Force style updates after adding new elements
        forceStyleUpdate();
        setTimeout(forceStyleUpdate, 100);
        setTimeout(forceStyleUpdate, 300);

        // Add event listeners to remove buttons
        const removeButtons = friendsList.querySelectorAll('.removeFriend');
        removeButtons.forEach(button => {
            button.addEventListener('click', function() {
                removeFriend(this.dataset.username);
            });
        });

        updatePagination();
    }

    // Function to fetch and cache friend data
    async function fetchFriendData(username) {
        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://api.vndb.org/kana/user?q=${username}`,
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    onload: function(response) {
                        resolve(JSON.parse(response.responseText));
                    },
                    onerror: reject
                });
            });

            if (response[username]) {
                friendsCache[username] = response[username];
                GM_setValue('vndb_friends_cache', friendsCache);
                return response[username];
            }
            return null;
        } catch (error) {
            console.error(`Error fetching data for friend ${username}:`, error);
            return null;
        }
    }

    // Function to add new friend
    async function addFriend() {
        const input = document.getElementById('newFriend');
        const username = input.value.trim();

        if (!username) return;

        const userData = await fetchFriendData(username);
        if (userData) {
            if (!friends.includes(username)) {
                friends.push(username);
                GM_setValue('vndb_friends', friends);
                currentPage = Math.ceil(friends.length / friendsPerPage);
                displayFriendsList();
                input.value = '';
            }
        } else {
            alert('User not found!');
        }
    }

    // Function to remove friend
    function removeFriend(username) {
        friends = friends.filter(f => f !== username);
        delete friendsCache[username];
        GM_setValue('vndb_friends', friends);
        GM_setValue('vndb_friends_cache', friendsCache);

        const totalPages = Math.ceil(friends.length / friendsPerPage);
        if (currentPage > totalPages) {
            currentPage = Math.max(1, totalPages);
        }

        displayFriendsList();
    }

    // Event listeners
    const friendsLink = document.querySelector('header nav menu li a[href="#"]');
    friendsLink.addEventListener('click', async (e) => {
        e.preventDefault();
        const container = document.querySelector('.friends-container');
        if (container.style.display === 'block') {
            container.style.display = 'none';
            sessionStorage.setItem('vndb_friends_container_open', 'false');
        } else {
            showContainer();
        }
    });

    document.getElementById('closeFriends').addEventListener('click', () => {
        container.style.display = 'none';
        sessionStorage.setItem('vndb_friends_container_open', 'false');
    });

    document.getElementById('addFriend').addEventListener('click', addFriend);

    document.getElementById('newFriend').addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            addFriend();
        }
    });

    // Settings toggle
    const toggleButton = document.getElementById('toggleSettings');
    toggleButton.addEventListener('click', () => {
        const isVisible = settingsPanel.style.display === 'block';
        settingsPanel.style.display = isVisible ? 'none' : 'block';
        toggleButton.textContent = isVisible ? 'Show Settings' : 'Hide Settings';
    });

    // Reset buttons
    const resetButtons = document.querySelectorAll('.resetButton');
    resetButtons.forEach(button => {
        button.addEventListener('click', function() {
            resetSetting(this.dataset.setting);
        });
    });

    // Preload friend data
    (async function preloadFriendData() {
        for (const friend of friends) {
            if (!friendsCache[friend]) {
                await fetchFriendData(friend);
            }
        }
    })();

    // Add event listeners for all possible theme change triggers
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden && container.style.display === 'block') {
            forceStyleUpdate();
        }
    });

    window.addEventListener('resize', () => {
        if (container.style.display === 'block') {
            forceStyleUpdate();
        }
    });

    // Monitor for CSS animations and transitions
    container.addEventListener('animationend', forceStyleUpdate);
    container.addEventListener('transitionend', forceStyleUpdate);

    // Monitor the container itself for any changes
    const containerObserver = new MutationObserver(() => {
        if (container.style.display === 'block') {
            forceStyleUpdate();
        }
    });

    containerObserver.observe(container, {
        attributes: true,
        childList: true,
        subtree: true,
        characterData: true
    });

    // Add color input sync function
    function syncColorInputs(colorId, hexId) {
        const colorInput = document.getElementById(colorId);
        const hexInput = document.getElementById(hexId);

        colorInput.addEventListener('input', (e) => {
            hexInput.value = e.target.value;
            settings[colorId] = e.target.value;
            GM_setValue('vndb_friends_settings', settings);
            forceStyleUpdate();
        });

        hexInput.addEventListener('input', (e) => {
            const hex = e.target.value;
            if (/^#[0-9A-Fa-f]{6}$/.test(hex)) {
                colorInput.value = hex;
                settings[colorId] = hex;
                GM_setValue('vndb_friends_settings', settings);
                forceStyleUpdate();
            }
        });
    }

    // Initialize color inputs
    function initializeColorInputs() {
        const colorPairs = [
            ['titleColor', 'titleColorHex'],
            ['textColor', 'textColorHex'],
            ['buttonTextColor', 'buttonTextColorHex'],
            ['backgroundColor', 'backgroundColorHex'],
            ['buttonBackgroundColor', 'buttonBackgroundColorHex'],
            ['borderColor', 'borderColorHex'],
            ['separatorColor', 'separatorColorHex']
        ];

        colorPairs.forEach(([colorId, hexId]) => {
            const colorInput = document.getElementById(colorId);
            const hexInput = document.getElementById(hexId);

            if (settings[colorId]) {
                colorInput.value = settings[colorId];
                hexInput.value = settings[colorId];
            }

            syncColorInputs(colorId, hexId);
        });

        // Initialize numeric inputs
        const numericInputs = [
            'fontSize',
            'buttonFontSize',
            'tabFontSize',
            'cacheDuration',
            'gamesPerFriend',
            'maxActivities'
        ];

        numericInputs.forEach(settingId => {
            const input = document.getElementById(settingId);
            if (input && settings[settingId] !== null) {
                input.value = settings[settingId];
            }

            // Add change listener
            input.addEventListener('change', function() {
                settings[settingId] = parseInt(this.value) || null;
                GM_setValue('vndb_friends_settings', settings);
                forceStyleUpdate();

                // Clear activity cache if relevant settings change
                if (settingId === 'cacheDuration' ||
                    settingId === 'gamesPerFriend' ||
                    settingId === 'maxActivities') {
                    activityCache.timestamp = 0;
                    sessionStorage.removeItem('vndb_activity_cache');
                    // Only update activity feed if we're already on that tab
                    if (document.querySelector('.tab-button[data-tab="activityFeed"]').classList.contains('active')) {
                        updateActivityFeed();
                    }
                }
            });
        });

        // Initialize opacity
        const opacityInput = document.getElementById('opacity');
        const opacityValue = document.getElementById('opacityValue');
        if (settings.opacity !== null) {
            opacityInput.value = settings.opacity * 100;
            opacityValue.textContent = Math.round(settings.opacity * 100);
        } else {
            opacityInput.value = 70;
            opacityValue.textContent = '70';
            settings.opacity = 0.70;
            GM_setValue('vndb_friends_settings', settings);
        }

        opacityInput.addEventListener('input', function() {
            opacityValue.textContent = this.value;
            settings.opacity = this.value / 100;
            GM_setValue('vndb_friends_settings', settings);
            forceStyleUpdate();
        });
    }

    // Add to the settings HTML, after the opacity setting
    const importExportHTML = `
        <div class="settings-group" style="margin-top: 20px;">
            <label>Backup:</label>
            <div style="display: flex; gap: 5px; flex-wrap: wrap;">
                <button id="exportData">Export All</button>
                <button id="importData">Import</button>
            </div>
        </div>
        <div id="importOptions" style="display: none; margin-top: 10px;">
            <div style="margin-bottom: 10px;">
                <input type="file" id="importFile" accept=".json" style="display: none;">
                <label>Import options:</label>
                <div style="margin-top: 5px;">
                    <label style="font-weight: normal;">
                        <input type="checkbox" id="importFriends" checked> Friends List
                    </label>
                    <label style="font-weight: normal; margin-left: 10px;">
                        <input type="checkbox" id="importSettings" checked> Settings
                    </label>
                </div>
                <div style="margin-top: 10px;">
                    <button id="confirmImport">Confirm Import</button>
                    <button id="cancelImport">Cancel</button>
                </div>
            </div>
        </div>
    `;

    // Add event listeners for import/export functionality
    function setupImportExport() {
        const exportButton = document.getElementById('exportData');
        const importButton = document.getElementById('importData');
        const importOptions = document.getElementById('importOptions');
        const importFile = document.getElementById('importFile');
        const confirmImport = document.getElementById('confirmImport');
        const cancelImport = document.getElementById('cancelImport');
        const importFriendsCheck = document.getElementById('importFriends');
        const importSettingsCheck = document.getElementById('importSettings');

        // Export functionality
        exportButton.addEventListener('click', () => {
            const exportData = {
                friends: friends,
                friendsCache: friendsCache,
                settings: settings
            };

            const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `vndb_friends_backup_${new Date().toISOString().split('T')[0]}.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        });

        // Import functionality
        importButton.addEventListener('click', () => {
            importOptions.style.display = 'block';
            importButton.style.display = 'none';
        });

        cancelImport.addEventListener('click', () => {
            importOptions.style.display = 'none';
            importButton.style.display = 'block';
            importFile.value = '';
        });

        confirmImport.addEventListener('click', () => {
            importFile.click();
        });

        importFile.addEventListener('change', (e) => {
            const file = e.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (event) => {
                try {
                    const importData = JSON.parse(event.target.result);

                    if (importFriendsCheck.checked) {
                        friends = importData.friends || [];
                        friendsCache = importData.friendsCache || {};
                        GM_setValue('vndb_friends', friends);
                        GM_setValue('vndb_friends_cache', friendsCache);
                    }

                    if (importSettingsCheck.checked && importData.settings) {
                        // Ensure all settings are properly copied with new defaults
                        const newSettings = {
                            textColor: null,
                            buttonTextColor: null,
                            backgroundColor: null,
                            buttonBackgroundColor: null,
                            titleColor: null,
                            borderColor: null,
                            separatorColor: null,
                            fontSize: 17,
                            buttonFontSize: 16,
                            tabFontSize: 18,
                            opacity: null,
                            cacheDuration: 3,
                            gamesPerFriend: 5,
                            maxActivities: 51,
                            ...importData.settings
                        };

                        settings = newSettings;
                        GM_setValue('vndb_friends_settings', settings);
                        initializeColorInputs();
                    }

                    // Update display
                    displayFriendsList();
                    forceStyleUpdate();

                    // Reset import UI
                    importOptions.style.display = 'none';
                    importButton.style.display = 'block';
                    importFile.value = '';

                    // Show success message
                    alert('Import completed successfully!');
                } catch (error) {
                    alert('Error importing data. Please check the file format.');
                    console.error('Import error:', error);
                }
            };
            reader.readAsText(file);
        });

        // Validate that at least one option is selected
        function updateImportButton() {
            confirmImport.disabled = !importFriendsCheck.checked && !importSettingsCheck.checked;
        }

        importFriendsCheck.addEventListener('change', updateImportButton);
        importSettingsCheck.addEventListener('change', updateImportButton);
    }

    // Add a check to prevent duplicate initialization
    function initializeImportExport() {
        const settingsPanel = container.querySelector('.friends-settings');
        // Remove any existing import/export section first
        const existingSection = settingsPanel.querySelector('#importExportSection');
        if (existingSection) {
            existingSection.remove();
        }

        const importExportDiv = document.createElement('div');
        importExportDiv.id = 'importExportSection';
        importExportDiv.innerHTML = importExportHTML;
        settingsPanel.appendChild(importExportDiv);
        setupImportExport();
    }

    // Add near the top of the script, after initializing settings
    let isContainerOpen = sessionStorage.getItem('vndb_friends_container_open') === 'true';

    // Check if container should be open on page load
    if (isContainerOpen) {
        container.style.display = 'block';
        initializeColorInputs();
        initializeImportExport();
        forceStyleUpdate();
        displayFriendsList();
    }

    // Add new functions for activity handling
    let activityCache = JSON.parse(sessionStorage.getItem('vndb_activity_cache')) || {
        timestamp: 0,
        data: []
    };

    async function fetchFriendActivity(username) {
        try {
            // Get the numeric ID from the friendsCache
            const userData = friendsCache[username];
            if (!userData || !userData.id) {
                console.error(`No cached data found for user ${username}`);
                return [];
            }

            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://api.vndb.org/kana/ulist',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    data: JSON.stringify({
                        "user": userData.id,
                        "fields": "id, vote, voted, vn.title",
                        "filters": ["label", "=", 7],
                        "sort": "voted",
                        "reverse": true,
                        "results": settings.gamesPerFriend || 5
                    }),
                    onload: function(response) {
                        if (response.status === 200) {
                            try {
                                const data = JSON.parse(response.responseText);
                                if (data && Array.isArray(data.results)) {
                                    resolve(data);
                                } else {
                                    console.error('Invalid API response structure:', data);
                                    resolve({ results: [] });
                                }
                            } catch (e) {
                                console.error('JSON parse error:', e);
                                resolve({ results: [] });
                            }
                        } else {
                            console.error('API error:', response.responseText);
                            resolve({ results: [] });
                        }
                    },
                    onerror: function(error) {
                        console.error('Request error:', error);
                        resolve({ results: [] });
                    }
                });
            });

            if (!response.results) {
                return [];
            }

            // Map the results to include the username
            return response.results.map(item => ({
                username,
                vnId: item.id,
                vnTitle: item.vn.title,
                vote: item.vote / 10,
                voted: item.voted
            }));
        } catch (error) {
            console.error(`Error fetching activity for ${username}:`, error);
            return [];
        }
    }

    async function updateActivityFeed() {
        const now = Date.now();
        const cacheDurationMs = (settings.cacheDuration || 3) * 60 * 1000; // Convert minutes to milliseconds
        const activityFeed = document.getElementById('activityFeed');

        // Check for valid cache in sessionStorage
        if (activityCache.data &&
            activityCache.data.length > 0 &&
            now - activityCache.timestamp < cacheDurationMs) {
            displayActivityFeed(activityCache.data);
            // Show quick cache message
            const cacheMsg = document.createElement('div');
            cacheMsg.style.textAlign = 'center';
            cacheMsg.style.fontSize = '0.8em';
            cacheMsg.style.opacity = '0.7';
            const timeLeft = Math.round((cacheDurationMs - (now - activityCache.timestamp)) / 1000);
            cacheMsg.textContent = `Loaded from cache (expires in ${timeLeft}s)`;
            activityFeed.insertAdjacentElement('afterbegin', cacheMsg);
            setTimeout(() => cacheMsg.remove(), 1500);
            return;
        }

        // Show loading message only when fetching from API
        activityFeed.innerHTML = '<div class="loading">Fetching new activity data...</div>';

        try {
            const activities = [];
            for (const friend of friends) {
                try {
                    const friendActivity = await fetchFriendActivity(friend);
                    activities.push(...friendActivity);
                } catch (error) {
                    console.error(`Error fetching activity for ${friend}:`, error);
                    continue;
                }
            }

            // Sort by vote date, most recent first
            activities.sort((a, b) => b.voted - a.voted);

            // Limit to 51 items
            const maxActivities = settings.maxActivities || 51;
            const limitedActivities = activities.slice(0, maxActivities);

            // Update cache and store in sessionStorage
            activityCache = {
                timestamp: now,
                data: limitedActivities
            };
            sessionStorage.setItem('vndb_activity_cache', JSON.stringify(activityCache));

            displayActivityFeed(limitedActivities);
        } catch (error) {
            console.error('Error updating activity feed:', error);
            activityFeed.innerHTML = '<div class="error">Error loading activity feed</div>';
        }
    }

    function displayActivityFeed(activities) {
        const activityFeed = document.getElementById('activityFeed');
        activityFeed.innerHTML = '';

        if (!activities || activities.length === 0) {
            activityFeed.innerHTML = '<div class="no-activity">No recent activity</div>';
            return;
        }

        const maxActivities = settings.maxActivities || 51;
        const limitedActivities = activities.slice(0, maxActivities);

        limitedActivities.forEach(activity => {
            if (!activity.voted || !activity.vnTitle) return;

            const date = new Date(activity.voted * 1000);
            const formattedDate = `${date.getDate().toString().padStart(2, '0')}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear()}`;

            const activityItem = document.createElement('div');
            activityItem.className = 'activity-item';

            const userData = friendsCache[activity.username];
            const userId = userData ? userData.id.slice(1) : '';

            activityItem.innerHTML = `
                <div>
                    <strong><a href="/u${userId}" class="friend-link">${activity.username}</a></strong> rated
                    <a href="/v${activity.vnId.toString().replace('v', '')}" class="friend-link vn-link">${activity.vnTitle}</a>
                    <strong>${activity.vote}</strong>
                </div>
                <div class="activity-date">${formattedDate}</div>
            `;
            activityFeed.appendChild(activityItem);
        });

        // Add event listeners using delegation
        const vnLinks = activityFeed.querySelectorAll('a.vn-link');
        vnLinks.forEach(link => {
            link.addEventListener('mouseenter', function() {
                handleFriendsMouseOver.call(this);
            });

            link.addEventListener('mouseleave', function() {
                handleFriendsMouseLeave.call(this);
            });
        });

        adjustContainerPosition();

        // Update scroll handler
        window.addEventListener('scroll', () => {
            if ($('#friendsPopover').css('display') === 'block') {
                $('#friendsPopover').friendsCenter();
            }
        });
    }

    // Add near the top where other state is initialized
    let activeTab = sessionStorage.getItem('vndb_friends_active_tab') || 'friendsList';

    // Update the tab switching functionality
    document.querySelectorAll('.tab-button').forEach(button => {
        button.addEventListener('click', () => {
            // Update active states
            document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
            document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));

            button.classList.add('active');
            const tabId = button.dataset.tab;

            // Show all tab content elements associated with this tab
            document.querySelectorAll(`.tab-content[data-tab="${tabId}"], #${tabId}`).forEach(content => {
                content.classList.add('active');
            });

            // Store active tab in session storage
            sessionStorage.setItem('vndb_friends_active_tab', tabId);
            activeTab = tabId;

            // Load activity feed if selected
            if (tabId === 'activityFeed') {
                updateActivityFeed();
            }
        });
    });

    // Update the section where container visibility is restored
    if (isContainerOpen) {
        container.style.display = 'block';
        initializeColorInputs();
        initializeImportExport();
        forceStyleUpdate();

        // Restore active tab
        document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
        document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));

        const activeTabButton = document.querySelector(`.tab-button[data-tab="${activeTab}"]`);
        const activeTabContent = document.getElementById(activeTab);

        if (activeTabButton && activeTabContent) {
            activeTabButton.classList.add('active');
            activeTabContent.classList.add('active');

            // Also show associated tab content elements
            document.querySelectorAll(`.tab-content[data-tab="${activeTab}"]`).forEach(content => {
                content.classList.add('active');
            });

            // Load activity feed if it was the active tab
            if (activeTab === 'activityFeed') {
                updateActivityFeed();
            } else {
                displayFriendsList();
            }
        }
    }

    // Function to adjust container position
    function adjustContainerPosition() {
        const container = document.querySelector('.friends-container');
        if (!container || container.style.display === 'none') return;

        // Get dimensions
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        const containerHeight = container.offsetHeight;
        const containerWidth = container.offsetWidth;

        // Reset position to center
        container.style.top = '50%';
        container.style.left = '50%';
        container.style.transform = 'translate(-50%, -50%)';

        // Get the container's position after centering
        const rect = container.getBoundingClientRect();

        // Adjust if too tall for viewport
        if (containerHeight > viewportHeight - 40) {
            container.style.top = '20px';
            container.style.transform = 'translateX(-50%)';
            container.style.maxHeight = `${viewportHeight - 40}px`;
        } else if (rect.top < 20) {
            container.style.top = '20px';
            container.style.transform = 'translateX(-50%)';
        } else if (rect.bottom > viewportHeight - 20) {
            container.style.top = `${viewportHeight - containerHeight - 20}px`;
            container.style.transform = 'translateX(-50%)';
        }

        // Adjust if too wide for viewport
        if (containerWidth > viewportWidth - 40) {
            container.style.left = '20px';
            container.style.transform = 'none';
            container.style.maxWidth = `${viewportWidth - 40}px`;
        } else if (rect.left < 20) {
            container.style.left = '20px';
            container.style.transform = 'none';
        } else if (rect.right > viewportWidth - 20) {
            container.style.left = `${viewportWidth - containerWidth - 20}px`;
            container.style.transform = 'none';
        }
    }

    // Function to handle container visibility
    function showContainer() {
        const container = document.querySelector('.friends-container');
        container.style.display = 'block';

        // Force recalculation of position
        requestAnimationFrame(() => {
            adjustContainerPosition();
            // Double-check position after a short delay
            setTimeout(adjustContainerPosition, 100);
        });

        sessionStorage.setItem('vndb_friends_container_open', 'true');
        initializeColorInputs();
        initializeImportExport();
        forceStyleUpdate();
        displayFriendsList();
    }

    // Update the event listeners section to include scroll events
    window.addEventListener('resize', adjustContainerPosition);
    window.addEventListener('scroll', adjustContainerPosition);

    // Add near the top of the script, after other initial declarations
    let timeoutId;

    // Add after the existing container creation
    $('body').append('<div id="friendsPopover"></div>');
    $('#friendsPopover').css({
        position: 'absolute',
        zIndex: '1001',
        boxShadow: '0px 0px 5px black',
        display: 'none'
    });

    // Add the centering function
    jQuery.fn.friendsCenter = function () {
        const windowHeight = $(window).height();
        const boxHeight = $(this).outerHeight();
        const scrollOffset = $(window).scrollTop();
        const hoveredLink = $('.activity-item a:hover').get(0);

        if (!hoveredLink) return this;

        const rect = hoveredLink.getBoundingClientRect();
        const leftoffset = rect.left;
        const topoffset = rect.top;
        let newTopOffset;

        if (topoffset - boxHeight / 2 < 10) {
            newTopOffset = 10;
        } else if (topoffset + boxHeight / 2 > windowHeight - 10) {
            newTopOffset = windowHeight - boxHeight - 10;
        } else {
            newTopOffset = topoffset - boxHeight / 2;
        }

        this.css("top", newTopOffset + scrollOffset);
        this.css("left", Math.max(0, leftoffset - $(this).outerWidth() - 25));

        return this;
    };

    // Update the hover handlers
    function handleFriendsMouseOver() {
        // Only show covers if we're on the activity tab
        const activeTab = sessionStorage.getItem('vndb_friends_active_tab');
        if (activeTab !== 'activityFeed') {
            return;
        }

        const vnId = this.getAttribute('href');
        if (!vnId) return;

        const pagelink = 'https://vndb.org' + vnId;

        timeoutId = setTimeout(() => {
            if (GM_getValue(pagelink)) {
                const retrievedLink = GM_getValue(pagelink);
                $('#friendsPopover').empty().append('<img src="' + retrievedLink + '"></img>');
                $('#friendsPopover img').on('load', function() {
                    if (this.height === 0) {
                        GM_deleteValue(pagelink);
                    } else {
                        $('#friendsPopover').friendsCenter().css('display', 'block');
                    }
                });
            } else {
                $.ajax({
                    url: pagelink,
                    dataType: 'text',
                    success: function (data) {
                        const parser = new DOMParser();
                        const dataDOC = parser.parseFromString(data, 'text/html');
                        const imagelink = dataDOC.querySelector(".vnimg img").src;
                        if (!imagelink) return;

                        const img = new Image();
                        img.onload = function() {
                            // Check tab again before showing the image
                            // (in case user switched tabs during load)
                            const currentTab = sessionStorage.getItem('vndb_friends_active_tab');
                            if (currentTab !== 'activityFeed') return;

                            if (this.height === 0) return;
                            $('#friendsPopover').empty().append(this).friendsCenter().css('display', 'block');
                            GM_setValue(pagelink, imagelink);
                        };
                        img.src = imagelink;
                    }
                });
            }
        }, 250);
    }

    function handleFriendsMouseLeave() {
        const activeTab = sessionStorage.getItem('vndb_friends_active_tab');
        if (activeTab !== 'activityFeed') {
            return;
        }

        clearTimeout(timeoutId);
        $('#friendsPopover').css('display', 'none');
    }

    // Add mutation observer to handle dynamic page changes
    const pageObserver = new MutationObserver(() => {
        if (document.querySelector('.friends-container').style.display === 'block') {
            adjustContainerPosition();
        }
    });

    pageObserver.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true
    });
})();