Adds a custom sticker box, quick-links nav bar, and white text for dark mode on XenForo 2.x.
// ==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);
})();