GameVN Scripts

Adds a custom sticker box, quick-links nav bar, and white text for dark mode on XenForo 2.x.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         GameVN Scripts
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds a custom sticker box, quick-links nav bar, and white text for dark mode on XenForo 2.x.
// @author       JR@gvn
// @match        *://gvn.co/*
// @match        *://*.gvn.co/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      imgur.com
// ==/UserScript==

(function () {
    'use strict';

    /* ── Configuration ─────────────────────────────────────────── */

    const CONFIG = {
        albums: [
            { id: 'pZxee8m', name: 'Mèo 25/26' },
            { id: 'v6H7yXQ', name: 'Mèo 23/24' },
            { id: 'C8UKRFY', name: 'Peepo' },
            { id: 'eRCCtp1', name: 'Fat Cat' },
        ],
        stickerSize: '80px',
        clientId: '546c25a59c58ad7',
        cacheKeyPrefix: 'xf_sticker_cache_v3_',
        cacheTime: 24 * 60 * 60 * 1000, // 24 hours
        quickLinks: [
            { name: 'Tin game', url: 'https://f.gvn.co/forums/tin-tuc-gioi-thieu-thao-luan-chung-ve-game.21/' },
            { name: 'Thư giãn', url: 'https://f.gvn.co/forums/thu-gian.50/' },
            { name: 'Phim ảnh', url: 'https://f.gvn.co/forums/phim-anh.41/' },
            { name: 'Thể thao', url: 'https://f.gvn.co/forums/the-thao.53/' },
        ],
    };

    const IDS = {
        darkStyle: 'xf-darkmode-post-text',
        container: 'xf-sticker-bar',
        tabs: 'xf-sticker-tabs',
        grid: 'xf-sticker-grid',
    };

    /* ── Helpers ────────────────────────────────────────────────── */

    const $ = (sel, root = document) => root.querySelector(sel);
    const $$ = (sel, root = document) => root.querySelectorAll(sel);

    const isDarkMode = () =>
        document.documentElement.getAttribute('data-variation') === 'alternate';

    function onReady(fn) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', fn, { once: true });
        } else {
            fn();
        }
    }

    function setStatusMessage(container, text) {
        container.innerHTML =
            `<div style="grid-column:1/-1;text-align:center;color:#888;padding:20px">${text}</div>`;
    }

    /* ── Dark-Mode Text Fix ────────────────────────────────────── */

    const DARK_MODE_CSS = `
        /* Post text */
        .message-body .bbWrapper, .message-content .bbWrapper,
        .message-userContent .bbWrapper, .bbWrapper,
        article.message-body, .message-body, .message-content,
        .message-userContent, .block-body .message,
        .p-body-content .message, .message-cell--main,
        .message-inner, .js-post .message-body, .message-attribution,
        .message-body p, .message-body span, .message-body div,
        .message-body li, .message-body td, .message-body th {
            color: #fff !important;
        }
        /* Links */
        .message-body a, .bbWrapper a { color: #8cb4ff !important; }
        /* Quotes */
        .bbCodeBlock--quote .bbCodeBlock-content { color: #e0e0e0 !important; }
        /* Code blocks */
        .bbCodeBlock--code .bbCodeBlock-content { color: #f0f0f0 !important; }
        /* Editor */
        .fr-element, .fr-element p, .fr-view, .fr-view p,
        .fr-box .fr-element, .fr-wrapper .fr-element,
        .editorContent, .js-editor .fr-element,
        .formSubmitRow-main textarea, textarea.input,
        .input--textarea, .bbCodeQuote-content {
            color: #fff !important;
        }
        .fr-placeholder { color: #aaa !important; }
    `;

    function applyDarkModeStyles() {
        const existing = document.getElementById(IDS.darkStyle);

        if (!isDarkMode()) {
            if (existing) existing.remove();
            return;
        }

        if (existing) return;

        const style = document.createElement('style');
        style.id = IDS.darkStyle;
        style.textContent = DARK_MODE_CSS;
        document.head.appendChild(style);
    }

    function initDarkMode() {
        onReady(applyDarkModeStyles);
        new MutationObserver(applyDarkModeStyles)
            .observe(document.documentElement, {
                attributes: true,
                attributeFilter: ['data-variation'],
            });
    }

    /* ── Quick Links Nav Bar ─────────────────────────────────── */

    function injectQuickLinks() {
        if (document.querySelector('.gvn-quicklink')) return true;

        const navList =
            document.querySelector('ul.p-nav-list') ||
            document.querySelector('.p-nav-list') ||
            document.querySelector('.p-nav-inner ul');
        if (!navList) return false;

        for (const link of CONFIG.quickLinks) {
            const li = document.createElement('li');
            li.className = 'gvn-quicklink';

            const navEl = document.createElement('div');
            navEl.className = 'p-navEl';

            const a = document.createElement('a');
            a.href = link.url;
            a.className = 'p-navEl-link';
            a.setAttribute('data-nav-id', link.name.toLowerCase().replace(/\s+/g, '-'));
            a.textContent = link.name;

            if (window.location.href.includes(link.url.replace(/\/$/, ''))) {
                navEl.classList.add('is-selected');
            }

            navEl.appendChild(a);
            li.appendChild(navEl);
            navList.appendChild(li);
        }

        return true;
    }

    function initQuickLinks() {
        if (injectQuickLinks()) return;

        // Retry: nav may not be in the DOM yet
        let tries = 0;
        const timer = setInterval(() => {
            if (injectQuickLinks() || ++tries > 30) clearInterval(timer);
        }, 300);
    }

    /* ── XF Editor Bridge ──────────────────────────────────────── */

    function insertIntoEditor(html) {
        if (typeof XF === 'undefined') return;

        const el = $('.js-editor');
        if (!el) return;

        const handler =
            XF.Element.getHandler(el, 'editor') ||
            XF.Element.getHandler(el, 'wysiwyg');
        if (!handler) return;

        if (handler.editor?.html?.insert) {
            handler.editor.html.insert(html);
        } else if (typeof handler.insertContent === 'function') {
            handler.insertContent(html);
        }
    }

    /* ── Imgur API ─────────────────────────────────────────────── */

    function fetchAlbum(albumId) {
        return new Promise((resolve) => {
            const cacheKey = CONFIG.cacheKeyPrefix + albumId;

            // Try cache first
            try {
                const cached = JSON.parse(GM_getValue(cacheKey, 'null'));
                if (cached && Date.now() - cached.timestamp < CONFIG.cacheTime) {
                    return resolve(cached.images);
                }
            } catch { /* cache miss */ }

            // Fetch from API
            GM_xmlhttpRequest({
                method: 'GET',
                url: `https://api.imgur.com/3/album/${albumId}/images`,
                headers: { Authorization: `Client-ID ${CONFIG.clientId}` },
                onload(response) {
                    try {
                        const { success, data } = JSON.parse(response.responseText);
                        if (!success || !data) return resolve([]);

                        const images = data.map((img) => img.link);
                        GM_setValue(cacheKey, JSON.stringify({ timestamp: Date.now(), images }));
                        resolve(images);
                    } catch {
                        resolve([]);
                    }
                },
                onerror() { resolve([]); },
            });
        });
    }

    /* ── Sticker Box UI ────────────────────────────────────────── */

    const TAB_STYLE_BASE = `
        border: 1px solid var(--border-color, #444);
        background: var(--content-alt-bg, transparent);
        color: var(--text-color, inherit);
        padding: 5px 12px;
        cursor: pointer;
        border-radius: 4px;
        font-family: inherit;
        font-size: 13px;
        transition: all .2s;
    `;

    let activeAlbumId = null;

    function setTabActive(btn, isActive) {
        if (isActive) {
            btn.style.background = 'var(--button-bg-primary, #005fad)';
            btn.style.color = '#fff';
            btn.style.borderColor = 'transparent';
        } else {
            btn.style.background = 'var(--content-alt-bg, transparent)';
            btn.style.color = 'var(--text-color, inherit)';
            btn.style.borderColor = 'var(--border-color, #444)';
        }
    }

    function switchTab(albumId) {
        activeAlbumId = albumId;

        const grid = document.getElementById(IDS.grid);
        if (!grid) return;

        // Highlight active tab
        $$(`#${IDS.tabs} button`).forEach((btn) => {
            const album = CONFIG.albums.find((a) => a.name === btn.textContent);
            setTabActive(btn, album?.id === albumId);
        });

        setStatusMessage(grid, 'Loading stickers…');

        fetchAlbum(albumId).then((images) => {
            if (activeAlbumId !== albumId) return; // tab changed while loading
            renderGrid(grid, images);
        });
    }

    function renderGrid(container, images) {
        container.innerHTML = '';

        if (!images.length) {
            setStatusMessage(container, 'No images found or API error.');
            return;
        }

        for (const src of images) {
            const cell = document.createElement('div');
            cell.title = 'Insert Sticker';
            cell.style.cssText = `
                display: flex; align-items: center; justify-content: center;
                height: 80px; cursor: pointer; border-radius: 4px;
                transition: background .15s, transform .1s;
                background: transparent; border: none;
            `;

            const img = document.createElement('img');
            img.src = src;
            img.loading = 'lazy';
            img.style.cssText =
                'max-width:95%; max-height:95%; object-fit:contain; pointer-events:none';

            cell.addEventListener('mouseenter', () => {
                cell.style.background = 'rgba(0,0,0,.05)';
                cell.style.transform = 'scale(1.05)';
            });
            cell.addEventListener('mouseleave', () => {
                cell.style.background = 'transparent';
                cell.style.transform = 'scale(1)';
            });
            cell.addEventListener('click', () => {
                insertIntoEditor(
                    `<img src="${src}" alt="Sticker" style="max-height:${CONFIG.stickerSize}"> `
                );
            });

            cell.appendChild(img);
            container.appendChild(cell);
        }
    }

    function renderTabs(tabsContainer) {
        tabsContainer.innerHTML = '';

        for (const album of CONFIG.albums) {
            const tab = document.createElement('button');
            tab.textContent = album.name;
            tab.type = 'button';
            tab.className = 'sticker-tab';
            tab.style.cssText = TAB_STYLE_BASE;
            tab.addEventListener('mouseenter', () => {
                if (activeAlbumId !== album.id) {
                    tab.style.background = 'var(--button-bg-primary, #005fad)';
                    tab.style.color = '#fff';
                    tab.style.borderColor = 'transparent';
                    tab.style.transform = 'translateY(-1px)';
                }
            });
            tab.addEventListener('mouseleave', () => {
                if (activeAlbumId !== album.id) {
                    tab.style.background = 'var(--content-alt-bg, transparent)';
                    tab.style.color = 'var(--text-color, inherit)';
                    tab.style.borderColor = 'var(--border-color, #444)';
                    tab.style.transform = 'translateY(0)';
                }
            });
            tab.addEventListener('click', (e) => {
                e.preventDefault();
                switchTab(album.id);
            });
            tabsContainer.appendChild(tab);
        }
    }

    function renderWidget() {
        if (document.getElementById(IDS.container)) return;

        const editor = $('.js-editor');
        if (!editor) return;

        const container = document.createElement('div');
        container.id = IDS.container;
        container.style.cssText = `
            background: transparent; border: none;
            margin: 10px 0 8px; display: flex; flex-direction: column;
        `;

        const tabsEl = document.createElement('div');
        tabsEl.id = IDS.tabs;
        tabsEl.style.cssText =
            'display:flex; gap:5px; padding:0 5px; margin-bottom:5px; flex-wrap:wrap';

        const gridEl = document.createElement('div');
        gridEl.id = IDS.grid;
        gridEl.style.cssText = `
            padding: 12px 5px; background: transparent; border: none;
            display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
            gap: 10px; max-height: 290px; overflow-y: auto;
            scrollbar-width: thin; min-height: 90px;
        `;
        setStatusMessage(gridEl, 'Select a sticker pack');

        container.append(tabsEl, gridEl);
        editor.after(container);

        renderTabs(tabsEl);

        if (CONFIG.albums.length) {
            switchTab(CONFIG.albums[0].id);
        }
    }

    /* ── Bootstrap ─────────────────────────────────────────────── */

    function initStickerBar() {
        if (typeof XF === 'undefined') return;

        renderWidget();

        // Re-render when editor appears dynamically (e.g. quick reply)
        new MutationObserver(() => {
            if ($('.js-editor') && !document.getElementById(IDS.container)) {
                setTimeout(renderWidget, 150);
            }
        }).observe(document.body, { childList: true, subtree: true });
    }

    initDarkMode();
    onReady(initQuickLinks);
    onReady(initStickerBar);
})();