Extracts data from a model's Babepedia profile and generates a ready-to-post BBCode forum entry. Multi-language UI (PL/EN/DE/FR/ES/RU). Personal & Body always included; Rating, Biography and other sections user-selectable via a dropdown menu and saved in localStorage. Emojis stripped from biography.
// ==UserScript==
// @name Babepedia -> BBCode Bio Extractor
// @namespace babepedia-bbcode-extractor
// @version 1.8
// @description Extracts data from a model's Babepedia profile and generates a ready-to-post BBCode forum entry. Multi-language UI (PL/EN/DE/FR/ES/RU). Personal & Body always included; Rating, Biography and other sections user-selectable via a dropdown menu and saved in localStorage. Emojis stripped from biography.
// @author Hehek aka PK2
// @match https://www.babepedia.com/babe/*
// @grant GM_setClipboard
// @grant GM_download
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ---------- i18n ----------
const I18N = {
pl: {
menu: 'Menu',
menuTitle: 'Babepedia Tools',
menuSub: 'Wybierz akcję',
genBBCode: 'Generuj BBCode',
genBBCodeDesc: 'Utwórz gotowy post z danymi modelki',
download: 'Pobierz 6 zdjęć',
downloadDesc: 'Zapisz zdjęcia profilowe na dysk',
settings: 'Ustawienia sekcji',
settingsDesc: 'Zarządzaj sekcjami i językiem',
backToMenu: '‹ Wróć do menu',
saveChoice: '💾 Zapisz wybór',
savedChoice: 'Zapisano wybór sekcji',
language: 'Język interfejsu',
selectSectionsTitle: 'Wybierz sekcje do uwzględnienia',
selectSectionsSub: 'Sekcje Personal i Body są zawsze dołączane. Pozostałe możesz włączyć lub wyłączyć — wybór zostanie zapamiętany w tej przeglądarce.',
always: 'zawsze',
cancel: 'Anuluj',
saveContinue: 'Zapisz i kontynuuj',
galleryTitle: 'Wklej BBCode galerii zdjęć',
gallerySub: 'Wgraj 6 pobranych zdjęć ręcznie (np. na ImageTwist) i wklej poniżej BBCode, który otrzymasz. Zostaw puste, aby wygenerować post bez galerii.',
galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
skipNoGallery: 'Pomiń (bez galerii)',
generateBBCode: 'Generuj BBCode',
resultTitle: 'Generated BBCode post',
resultSub: 'Skopiuj poniższy kod i wklej go na forum.',
close: 'Zamknij',
copy: '📋 Kopiuj',
copied: 'Skopiowano do schowka',
downloading: 'Pobieranie {n} zdjęć...',
noPhotos: 'Nie znaleziono zdjęć do pobrania na tej stronie.',
downloadFail: 'Nie udało się pobrać {name}.',
},
en: {
menu: 'Menu',
menuTitle: 'Babepedia Tools',
menuSub: 'Choose an action',
genBBCode: 'Generate BBCode',
genBBCodeDesc: 'Create a ready-to-post entry from model data',
download: 'Download 6 photos',
downloadDesc: 'Save profile photos to disk',
settings: 'Section settings',
settingsDesc: 'Manage sections and language',
backToMenu: '‹ Back to menu',
saveChoice: '💾 Save selection',
savedChoice: 'Section selection saved',
language: 'Interface language',
selectSectionsTitle: 'Choose sections to include',
selectSectionsSub: 'Personal and Body are always included. You can toggle the rest — the choice is remembered in this browser.',
always: 'always',
cancel: 'Cancel',
saveContinue: 'Save and continue',
galleryTitle: 'Paste your photo gallery BBCode',
gallerySub: 'Upload the 6 downloaded photos yourself (e.g. to ImageTwist) and paste the BBCode it gives you below. Leave empty to generate the post without a gallery.',
galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
skipNoGallery: 'Skip (no gallery)',
generateBBCode: 'Generate BBCode',
resultTitle: 'Generated BBCode post',
resultSub: 'Copy the code below and paste it on the forum.',
close: 'Close',
copy: '📋 Copy',
copied: 'Copied to clipboard',
downloading: 'Downloading {n} photos...',
noPhotos: 'No downloadable photos found on this page.',
downloadFail: 'Could not download {name}.',
},
de: {
menu: 'Menü',
menuTitle: 'Babepedia Tools',
menuSub: 'Aktion wählen',
genBBCode: 'BBCode erzeugen',
genBBCodeDesc: 'Fertigen Beitrag aus Modelldaten erstellen',
download: '6 Fotos herunterladen',
downloadDesc: 'Profilfotos auf die Festplatte speichern',
settings: 'Bereichseinstellungen',
settingsDesc: 'Bereiche und Sprache verwalten',
backToMenu: '‹ Zurück zum Menü',
saveChoice: '💾 Auswahl speichern',
savedChoice: 'Bereichsauswahl gespeichert',
language: 'Sprache der Oberfläche',
selectSectionsTitle: 'Bereiche zum Einfügen wählen',
selectSectionsSub: 'Personal und Body sind immer enthalten. Die übrigen kannst du ein- oder ausschalten — die Auswahl wird in diesem Browser gespeichert.',
always: 'immer',
cancel: 'Abbrechen',
saveContinue: 'Speichern und fortfahren',
galleryTitle: 'BBCode deiner Fotogalerie einfügen',
gallerySub: 'Lade die 6 heruntergeladenen Fotos selbst hoch (z. B. zu ImageTwist) und füge den erhaltenen BBCode unten ein. Leer lassen, um den Beitrag ohne Galerie zu erzeugen.',
galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
skipNoGallery: 'Überspringen (ohne Galerie)',
generateBBCode: 'BBCode erzeugen',
resultTitle: 'Erzeugter BBCode-Beitrag',
resultSub: 'Kopiere den Code unten und füge ihn im Forum ein.',
close: 'Schließen',
copy: '📋 Kopieren',
copied: 'In die Zwischenablage kopiert',
downloading: 'Lade {n} Fotos herunter...',
noPhotos: 'Keine herunterladbaren Fotos auf dieser Seite gefunden.',
downloadFail: '{name} konnte nicht heruntergeladen werden.',
},
fr: {
menu: 'Menu',
menuTitle: 'Babepedia Tools',
menuSub: 'Choisissez une action',
genBBCode: 'Générer le BBCode',
genBBCodeDesc: 'Créer un post prêt à publier à partir des données du modèle',
download: 'Télécharger 6 photos',
downloadDesc: 'Enregistrer les photos de profil sur le disque',
settings: 'Paramètres des sections',
settingsDesc: 'Gérer les sections et la langue',
backToMenu: '‹ Retour au menu',
saveChoice: '💾 Enregistrer la sélection',
savedChoice: 'Sélection des sections enregistrée',
language: 'Langue de l\'interface',
selectSectionsTitle: 'Choisir les sections à inclure',
selectSectionsSub: 'Personal et Body sont toujours inclus. Vous pouvez activer ou désactiver les autres — le choix est mémorisé dans ce navigateur.',
always: 'toujours',
cancel: 'Annuler',
saveContinue: 'Enregistrer et continuer',
galleryTitle: 'Collez le BBCode de votre galerie photo',
gallerySub: 'Téléchargez vous-même les 6 photos (par ex. sur ImageTwist) et collez ci-dessous le BBCode fourni. Laissez vide pour générer le post sans galerie.',
galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
skipNoGallery: 'Ignorer (sans galerie)',
generateBBCode: 'Générer le BBCode',
resultTitle: 'BBCode généré',
resultSub: 'Copiez le code ci-dessous et collez-le sur le forum.',
close: 'Fermer',
copy: '📋 Copier',
copied: 'Copié dans le presse-papiers',
downloading: 'Téléchargement de {n} photos...',
noPhotos: 'Aucune photo téléchargeable trouvée sur cette page.',
downloadFail: 'Impossible de télécharger {name}.',
},
es: {
menu: 'Menú',
menuTitle: 'Babepedia Tools',
menuSub: 'Elige una acción',
genBBCode: 'Generar BBCode',
genBBCodeDesc: 'Crear una publicación lista para el foro con los datos de la modelo',
download: 'Descargar 6 fotos',
downloadDesc: 'Guardar las fotos de perfil en el disco',
settings: 'Ajustes de secciones',
settingsDesc: 'Gestionar secciones e idioma',
backToMenu: '‹ Volver al menú',
saveChoice: '💾 Guardar selección',
savedChoice: 'Selección de secciones guardada',
language: 'Idioma de la interfaz',
selectSectionsTitle: 'Elige las secciones a incluir',
selectSectionsSub: 'Personal y Body siempre se incluyen. Puedes activar o desactivar las demás — la elección se recuerda en este navegador.',
always: 'siempre',
cancel: 'Cancelar',
saveContinue: 'Guardar y continuar',
galleryTitle: 'Pega el BBCode de tu galería de fotos',
gallerySub: 'Sube tú mismo las 6 fotos descargadas (p. ej. a ImageTwist) y pega abajo el BBCode que te dé. Déjalo vacío para generar la publicación sin galería.',
galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
skipNoGallery: 'Omitir (sin galería)',
generateBBCode: 'Generar BBCode',
resultTitle: 'BBCode generado',
resultSub: 'Copia el código de abajo y pégalo en el foro.',
close: 'Cerrar',
copy: '📋 Copiar',
copied: 'Copiado al portapapeles',
downloading: 'Descargando {n} fotos...',
noPhotos: 'No se encontraron fotos descargables en esta página.',
downloadFail: 'No se pudo descargar {name}.',
},
ru: {
menu: 'Меню',
menuTitle: 'Babepedia Tools',
menuSub: 'Выберите действие',
genBBCode: 'Сгенерировать BBCode',
genBBCodeDesc: 'Создать готовый пост с данными модели',
download: 'Скачать 6 фото',
downloadDesc: 'Сохранить фотографии профиля на диск',
settings: 'Настройки разделов',
settingsDesc: 'Управление разделами и языком',
backToMenu: '‹ Назад в меню',
saveChoice: '💾 Сохранить выбор',
savedChoice: 'Выбор разделов сохранён',
language: 'Язык интерфейса',
selectSectionsTitle: 'Выберите разделы для включения',
selectSectionsSub: 'Разделы Personal и Body включаются всегда. Остальные можно включать и выключать — выбор запоминается в этом браузере.',
always: 'всегда',
cancel: 'Отмена',
saveContinue: 'Сохранить и продолжить',
galleryTitle: 'Вставьте BBCode галереи фото',
gallerySub: 'Загрузите 6 скачанных фото самостоятельно (например, на ImageTwist) и вставьте ниже полученный BBCode. Оставьте пустым, чтобы создать пост без галереи.',
galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
skipNoGallery: 'Пропустить (без галереи)',
generateBBCode: 'Сгенерировать BBCode',
resultTitle: 'Сгенерированный BBCode',
resultSub: 'Скопируйте код ниже и вставьте его на форуме.',
close: 'Закрыть',
copy: '📋 Копировать',
copied: 'Скопировано в буфер обмена',
downloading: 'Скачивание {n} фото...',
noPhotos: 'На этой странице не найдено фото для скачивания.',
downloadFail: 'Не удалось скачать {name}.',
},
};
const LANG_NAMES = {
pl: 'Polski',
en: 'English',
de: 'Deutsch',
fr: 'Français',
es: 'Español',
ru: 'Русский',
};
const STORAGE_KEY = 'babepedia_bbcode_sections_v1';
const MANDATORY_SECTIONS = ['Personal', 'Body'];
const BIOGRAPHY_KEY = 'Biography';
const RATING_KEY = 'Rating';
const DEFAULT_ENABLED = [RATING_KEY, BIOGRAPHY_KEY];
function detectBrowserLang() {
const nav = (navigator.language || 'en').slice(0, 2).toLowerCase();
return I18N[nav] ? nav : 'en';
}
function t(key, vars) {
const lang = currentLang();
const dict = I18N[lang] || I18N.en;
let s = dict[key] || I18N.en[key] || key;
if (vars) {
Object.keys(vars).forEach(k => {
s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), vars[k]);
});
}
return s;
}
function currentLang() {
const prefs = loadPrefs();
if (prefs && prefs.lang && I18N[prefs.lang]) return prefs.lang;
return detectBrowserLang();
}
// ---------- Helpers ----------
function text(el) {
return el ? el.textContent.replace(/\s+/g, ' ').trim() : '';
}
function slugify(str) {
return str.replace(/[^\w\-]+/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
}
function stripEmojis(str) {
if (!str) return '';
return str
.replace(/[\u{1F000}-\u{1FAFF}]/gu, '')
.replace(/[\u{2600}-\u{27BF}]/gu, '')
.replace(/[\u{2B00}-\u{2BFF}]/gu, '')
.replace(/[\u{FE00}-\u{FE0F}]/gu, '')
.replace(/[\u{1F1E6}-\u{1F1FF}]/gu, '')
.replace(/\u200D/g, '')
.replace(/[\u{2190}-\u{21FF}]/gu, '')
.replace(/[\u{2300}-\u{23FF}]/gu, '')
.replace(/[\u{25A0}-\u{25FF}]/gu, '')
.replace(/[\u{2700}-\u{27BF}]/gu, '')
.replace(/\s+/g, ' ')
.trim();
}
function getName() {
return text(document.querySelector('#babename'));
}
function getAka() {
const akaEl = document.querySelector('#name-block #aka');
if (!akaEl) return '';
const clone = akaEl.cloneNode(true);
clone.querySelectorAll('img, #aliasinfo, small').forEach(n => n.remove());
return stripEmojis(clone.textContent.replace(/\s+/g, ' ').trim());
}
function getBioText() {
const visible = document.querySelector('#visible-text');
const hidden = document.querySelector('#hidden-text');
let raw;
if (visible) {
raw = (text(visible) + ' ' + text(hidden)).trim();
} else {
raw = text(document.querySelector('#biotext'));
}
return stripEmojis(raw);
}
function getInfoGrid() {
const grid = document.querySelector('#personal-info-block .info-grid');
const result = {};
if (!grid) return result;
let currentSection = 'Info';
Array.from(grid.children).forEach(item => {
if (item.classList.contains('info-title')) {
const h3 = item.querySelector('h3');
currentSection = h3 ? text(h3) : currentSection;
result[currentSection] = result[currentSection] || {};
return;
}
const label = item.querySelector('.label');
const value = item.querySelector('.value');
if (label && value) {
const valClone = value.cloneNode(true);
valClone.querySelectorAll('small, #cupconversions').forEach(n => n.remove());
const key = text(label).replace(/:$/, '');
const val = stripEmojis(valClone.textContent.replace(/\s+/g, ' ').trim());
result[currentSection] = result[currentSection] || {};
result[currentSection][key] = val;
}
});
return result;
}
function getRating() {
let score = text(document.querySelector('.rating-global strong'));
score = stripEmojis(score);
const votes = text(document.querySelector('.rating-global small'));
return score ? `${score} (${votes})` : '';
}
// ---------- Section preferences ----------
function loadPrefs() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.enabled)) {
return parsed;
}
} catch (e) {
console.warn('[Babepedia] failed to load prefs', e);
}
return null;
}
function savePrefs(prefs) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
} catch (e) {
console.warn('[Babepedia] failed to save prefs', e);
}
}
function defaultPrefs() {
return { enabled: [...DEFAULT_ENABLED], lang: detectBrowserLang() };
}
function isSectionEnabled(section, prefs) {
if (MANDATORY_SECTIONS.includes(section)) return true;
if (section === BIOGRAPHY_KEY || section === RATING_KEY) {
if (!prefs) return true;
return prefs.enabled.includes(section);
}
if (!prefs) return false;
return prefs.enabled.includes(section);
}
// ---------- Photos ----------
const IMAGE_EXTENSION_RE = /\.(jpe?g|png|gif|webp)(?:[?#].*)?$/i;
function getTopImageUrls(limit) {
limit = limit || 6;
const urls = [];
const seen = new Set();
function addFrom(selector) {
document.querySelectorAll(selector).forEach(a => {
if (urls.length >= limit) return;
const href = a.href;
if (href && IMAGE_EXTENSION_RE.test(href) && !seen.has(href)) {
seen.add(href);
urls.push(href);
}
});
}
addFrom('#profbox2 a.img');
if (urls.length < limit) addFrom('.useruploads2 a.img');
return urls.slice(0, limit);
}
const DOWNLOAD_DELAY_MS = 400;
async function downloadOneImageViaFetch(url, filename) {
const response = await fetch(url, { credentials: 'include' });
if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${url}`);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(blobUrl), 10000);
}
function downloadOneImageViaGM(url, filename) {
return new Promise((resolve, reject) => {
if (typeof GM_download !== 'function') {
reject(new Error('GM_download is not available'));
return;
}
GM_download({
url,
name: filename,
saveAs: false,
onload: resolve,
onerror: (err) => reject(new Error(`GM_download: ${err && err.error ? err.error : 'unknown error'}`)),
});
});
}
async function downloadOneImage(url, filename) {
console.log('[Babepedia downloader] downloading', filename, 'from', url);
try {
await downloadOneImageViaFetch(url, filename);
} catch (fetchErr) {
console.warn('[Babepedia downloader] fetch failed', filename, fetchErr, '- trying GM_download');
try {
await downloadOneImageViaGM(url, filename);
} catch (gmErr) {
console.error('[Babepedia downloader] all methods failed', filename, fetchErr, gmErr);
alert(t('downloadFail', { name: filename }) + `\nFetch: ${fetchErr.message}\nGM_download: ${gmErr.message}\nCheck console (F12).`);
}
}
}
function downloadImages(urls) {
const name = slugify(getName() || 'babepedia');
console.log(`[Babepedia downloader] starting download of ${urls.length} photo(s) for "${name}"`);
urls.forEach((url, i) => {
const ext = (url.split('.').pop().split(/[?#]/)[0] || 'jpg');
const filename = `${name}_${i + 1}.${ext}`;
setTimeout(() => downloadOneImage(url, filename), i * DOWNLOAD_DELAY_MS);
});
}
// ---------- BBCode builder ----------
function buildBBCode(galleryBBCode, prefs) {
const name = getName();
const aka = getAka();
const bio = getBioText();
const info = getInfoGrid();
const rating = getRating();
const sourceUrl = window.location.href.split('?')[0];
let out = '';
out += `[b][size=150]${name}[/size][/b]\n`;
if (aka) out += `[i]${aka}[/i]\n`;
out += `\n`;
if (galleryBBCode && galleryBBCode.trim()) {
out += `${galleryBBCode.trim()}\n\n`;
}
if (rating && isSectionEnabled(RATING_KEY, prefs)) {
out += `[b]Rating:[/b] ${rating}\n\n`;
}
Object.keys(info).forEach(section => {
if (!isSectionEnabled(section, prefs)) return;
const fields = info[section];
const keys = Object.keys(fields);
if (!keys.length) return;
out += `[b]${section}[/b]\n`;
keys.forEach(k => {
out += `${k}: ${fields[k]}\n`;
});
out += `\n`;
});
if (bio && isSectionEnabled(BIOGRAPHY_KEY, prefs)) {
out += `[b]Biography[/b]\n${bio}\n\n`;
}
out += `[i]Source: [URL]${sourceUrl}[/URL][/i]`;
return out;
}
// ---------- Styles ----------
function injectStyles() {
if (document.getElementById('bbe-styles')) return;
const style = document.createElement('style');
style.id = 'bbe-styles';
style.textContent = `
:root {
--bbe-bg: #1e2130;
--bbe-bg-darker: #14161f;
--bbe-bg-footer: #1a1d29;
--bbe-border: #2c3044;
--bbe-border-strong: #383d56;
--bbe-text: #e6e8ef;
--bbe-text-dim: #9096ad;
--bbe-accent: #d2452d;
--bbe-accent-hover: #e0553c;
--bbe-radius: 10px;
--bbe-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
/* ===== Floating menu root ===== */
.bbe-root {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 99999;
font-family: var(--bbe-font);
}
.bbe-trigger {
display: flex; align-items: center; gap: 8px;
padding: 12px 18px;
background: var(--bbe-accent);
color: #fff;
border: none;
border-radius: 12px;
font-size: 14px; font-weight: 600;
font-family: inherit;
cursor: pointer;
box-shadow: 0 6px 24px rgba(210, 69, 45, 0.35), 0 2px 8px rgba(0,0,0,0.4);
transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
}
.bbe-trigger:hover {
background: var(--bbe-accent-hover);
transform: translateY(-1px);
box-shadow: 0 8px 28px rgba(210, 69, 45, 0.45), 0 3px 10px rgba(0,0,0,0.45);
}
.bbe-trigger:active { transform: translateY(0); }
.bbe-trigger .bbe-icon {
display: inline-flex;
transition: transform 0.2s ease;
font-size: 16px;
}
.bbe-root.open .bbe-trigger .bbe-icon { transform: rotate(90deg); }
/* ===== Dropdown panel ===== */
.bbe-panel {
position: absolute;
bottom: 62px;
right: 0;
width: 300px;
background: var(--bbe-bg);
color: var(--bbe-text);
border: 1px solid var(--bbe-border);
border-radius: 14px;
box-shadow: 0 20px 60px rgba(0,0,0,0.55), 0 6px 20px rgba(0,0,0,0.4);
overflow: hidden;
opacity: 0;
transform: translateY(8px) scale(0.97);
transform-origin: bottom right;
pointer-events: none;
transition: opacity 0.16s ease, transform 0.16s ease;
}
.bbe-root.open .bbe-panel {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
.bbe-panel-header {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--bbe-border);
background: var(--bbe-bg-footer);
}
.bbe-panel-title {
margin: 0;
font-size: 13px; font-weight: 600;
color: #fff;
letter-spacing: 0.2px;
}
.bbe-panel-sub {
margin: 2px 0 0;
font-size: 11.5px;
color: var(--bbe-text-dim);
}
.bbe-panel-body {
padding: 6px;
max-height: 400px;
overflow-y: auto;
}
.bbe-panel-item {
display: flex; align-items: center; gap: 12px;
width: 100%;
padding: 10px 12px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--bbe-text);
font-family: inherit;
font-size: 13.5px;
text-align: left;
cursor: pointer;
transition: background 0.12s;
}
.bbe-panel-item:hover { background: #262a3d; }
.bbe-panel-item:active { background: #2d3150; }
.bbe-panel-item .bbe-item-icon {
flex-shrink: 0;
width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
border-radius: 8px;
background: #262a3d;
font-size: 15px;
}
.bbe-panel-item:hover .bbe-item-icon { background: #333850; }
.bbe-panel-item .bbe-item-text { flex: 1; min-width: 0; }
.bbe-panel-item .bbe-item-title {
font-size: 13.5px;
font-weight: 500;
color: #fff;
}
.bbe-panel-item .bbe-item-desc {
font-size: 11.5px;
color: var(--bbe-text-dim);
margin-top: 1px;
line-height: 1.35;
}
.bbe-panel-item .bbe-item-chevron {
color: var(--bbe-text-dim);
font-size: 12px;
transition: transform 0.15s;
}
.bbe-panel-item:hover .bbe-item-chevron { transform: translateX(2px); color: var(--bbe-text); }
.bbe-panel-sep {
height: 1px;
background: var(--bbe-border);
margin: 4px 10px;
}
/* ===== Settings view inside panel ===== */
.bbe-settings-view { display: none; }
.bbe-root.show-settings .bbe-menu-view { display: none; }
.bbe-root.show-settings .bbe-settings-view { display: block; }
.bbe-back-btn {
display: flex; align-items: center; gap: 6px;
padding: 8px 12px 6px;
background: transparent;
border: none;
color: var(--bbe-text-dim);
font-family: inherit;
font-size: 12px;
cursor: pointer;
transition: color 0.12s;
}
.bbe-back-btn:hover { color: var(--bbe-text); }
.bbe-section-list {
display: flex; flex-direction: column; gap: 4px;
padding: 4px 6px 6px;
}
.bbe-section-item {
display: flex; align-items: center; gap: 10px;
padding: 9px 10px;
background: var(--bbe-bg-darker);
border: 1px solid var(--bbe-border);
border-radius: 8px;
cursor: pointer;
user-select: none;
transition: background 0.12s, border-color 0.12s;
}
.bbe-section-item:hover { background: #181b27; border-color: var(--bbe-border-strong); }
.bbe-section-item.locked { cursor: not-allowed; opacity: 0.75; }
.bbe-section-item input[type="checkbox"] {
width: 16px; height: 16px;
accent-color: var(--bbe-accent);
cursor: pointer;
flex-shrink: 0;
}
.bbe-section-item.locked input[type="checkbox"] { accent-color: #6b7089; }
.bbe-section-name {
flex: 1;
font-size: 13px;
color: var(--bbe-text);
}
.bbe-lock-badge {
font-size: 10.5px;
padding: 2px 7px;
border-radius: 10px;
background: #2a2e40;
color: var(--bbe-text-dim);
font-weight: 500;
letter-spacing: 0.3px;
}
.bbe-lang-box {
padding: 8px 10px 4px;
border-top: 1px solid var(--bbe-border);
margin-top: 4px;
}
.bbe-lang-label {
display: block;
font-size: 11px;
color: var(--bbe-text-dim);
margin-bottom: 5px;
letter-spacing: 0.3px;
text-transform: uppercase;
}
.bbe-lang-select {
width: 100%;
background: var(--bbe-bg-darker);
color: var(--bbe-text);
border: 1px solid var(--bbe-border);
border-radius: 8px;
padding: 8px 10px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
outline: none;
transition: border-color 0.12s;
}
.bbe-lang-select:hover { border-color: var(--bbe-border-strong); }
.bbe-lang-select:focus { border-color: var(--bbe-accent); }
.bbe-save-btn {
width: calc(100% - 12px);
margin: 6px 6px 8px;
padding: 10px 14px;
background: var(--bbe-accent);
color: #fff;
border: none;
border-radius: 8px;
font-family: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.12s;
}
.bbe-save-btn:hover { background: var(--bbe-accent-hover); }
/* ===== Modals ===== */
.bbe-overlay {
position: fixed; inset: 0;
background: rgba(10, 12, 18, 0.78);
backdrop-filter: blur(3px);
z-index: 999999;
display: flex; align-items: center; justify-content: center;
padding: 20px;
animation: bbe-fade 0.15s ease-out;
font-family: var(--bbe-font);
}
@keyframes bbe-fade { from { opacity: 0; } to { opacity: 1; } }
.bbe-box {
background: var(--bbe-bg);
color: var(--bbe-text);
width: 100%; max-width: 720px; max-height: 85vh;
border-radius: 14px;
box-shadow: 0 24px 70px rgba(0,0,0,0.6);
display: flex; flex-direction: column;
overflow: hidden;
border: 1px solid var(--bbe-border);
}
.bbe-modal-header {
padding: 18px 22px 12px;
border-bottom: 1px solid var(--bbe-border);
}
.bbe-modal-title {
margin: 0 0 4px;
font-size: 17px; font-weight: 600;
color: #fff;
}
.bbe-modal-sub {
margin: 0; font-size: 13px; color: var(--bbe-text-dim); line-height: 1.45;
}
.bbe-modal-body {
padding: 16px 22px;
overflow-y: auto;
flex: 1 1 auto;
}
.bbe-modal-body textarea {
width: 100%; box-sizing: border-box;
background: var(--bbe-bg-darker);
border: 1px solid var(--bbe-border);
color: #d6d9e3;
border-radius: 8px;
padding: 12px;
font-family: "SF Mono", Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
.bbe-modal-body textarea:focus { border-color: var(--bbe-accent); }
.bbe-modal-footer {
padding: 14px 22px 18px;
border-top: 1px solid var(--bbe-border);
display: flex; gap: 8px; justify-content: flex-end;
background: var(--bbe-bg-footer);
}
.bbe-btn {
border: 1px solid transparent;
padding: 9px 16px;
border-radius: 8px;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
font-family: inherit;
color: var(--bbe-text);
background: #2a2e40;
}
.bbe-btn:hover { background: #333850; }
.bbe-btn.primary { background: var(--bbe-accent); color: #fff; }
.bbe-btn.primary:hover { background: var(--bbe-accent-hover); }
.bbe-btn.ghost { background: transparent; border-color: var(--bbe-border); }
.bbe-btn.ghost:hover { background: #232736; }
/* ===== Toast ===== */
.bbe-toast {
position: fixed;
bottom: 24px; left: 50%;
transform: translateX(-50%) translateY(20px);
background: #2a2e40;
color: #fff;
padding: 11px 20px;
border-radius: 8px;
font-size: 13.5px;
font-family: var(--bbe-font);
box-shadow: 0 8px 30px rgba(0,0,0,0.4);
opacity: 0;
transition: opacity 0.18s, transform 0.18s;
z-index: 1000000;
pointer-events: none;
border: 1px solid var(--bbe-border-strong);
}
.bbe-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
`;
document.head.appendChild(style);
}
// ---------- Toast ----------
function toast(msg) {
let el = document.querySelector('.bbe-toast');
if (!el) {
el = document.createElement('div');
el.className = 'bbe-toast';
document.body.appendChild(el);
}
el.textContent = msg;
requestAnimationFrame(() => el.classList.add('show'));
clearTimeout(el._timer);
el._timer = setTimeout(() => el.classList.remove('show'), 1800);
}
// ---------- Modal helpers ----------
function makeOverlay() {
const overlay = document.createElement('div');
overlay.className = 'bbe-overlay';
document.body.appendChild(overlay);
overlay.addEventListener('click', e => {
if (e.target === overlay) overlay.remove();
});
return overlay;
}
function makeModalBox(title, subtitle) {
const box = document.createElement('div');
box.className = 'bbe-box';
const header = document.createElement('div');
header.className = 'bbe-modal-header';
const h = document.createElement('h3');
h.className = 'bbe-modal-title';
h.textContent = title;
header.appendChild(h);
if (subtitle) {
const p = document.createElement('p');
p.className = 'bbe-modal-sub';
p.textContent = subtitle;
header.appendChild(p);
}
const body = document.createElement('div');
body.className = 'bbe-modal-body';
const footer = document.createElement('div');
footer.className = 'bbe-modal-footer';
box.appendChild(header);
box.appendChild(body);
box.appendChild(footer);
return { box, body, footer };
}
// ---------- Result modal ----------
function showResult(bbcode) {
injectStyles();
const overlay = makeOverlay();
const { box, body, footer } = makeModalBox(
t('resultTitle'),
t('resultSub')
);
const textarea = document.createElement('textarea');
textarea.value = bbcode;
textarea.style.height = '380px';
body.appendChild(textarea);
const closeBtn = document.createElement('button');
closeBtn.className = 'bbe-btn ghost';
closeBtn.textContent = t('close');
closeBtn.onclick = () => overlay.remove();
const copyBtn = document.createElement('button');
copyBtn.className = 'bbe-btn primary';
copyBtn.textContent = t('copy');
copyBtn.onclick = () => {
textarea.select();
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(bbcode);
} else {
document.execCommand('copy');
}
toast(t('copied'));
};
footer.appendChild(closeBtn);
footer.appendChild(copyBtn);
overlay.appendChild(box);
textarea.focus();
textarea.select();
}
// ---------- Gallery input modal ----------
function showGalleryInputModal(prefs) {
injectStyles();
const overlay = makeOverlay();
const { box, body, footer } = makeModalBox(
t('galleryTitle'),
t('gallerySub')
);
const textarea = document.createElement('textarea');
textarea.placeholder = t('galleryPlaceholder');
textarea.style.height = '200px';
body.appendChild(textarea);
const cancelBtn = document.createElement('button');
cancelBtn.className = 'bbe-btn ghost';
cancelBtn.textContent = t('cancel');
cancelBtn.onclick = () => overlay.remove();
const skipBtn = document.createElement('button');
skipBtn.className = 'bbe-btn';
skipBtn.textContent = t('skipNoGallery');
skipBtn.onclick = () => {
overlay.remove();
showResult(buildBBCode('', prefs));
};
const genBtn = document.createElement('button');
genBtn.className = 'bbe-btn primary';
genBtn.textContent = t('generateBBCode');
genBtn.onclick = () => {
overlay.remove();
showResult(buildBBCode(textarea.value, prefs));
};
footer.appendChild(cancelBtn);
footer.appendChild(skipBtn);
footer.appendChild(genBtn);
overlay.appendChild(box);
textarea.focus();
}
// ---------- Sections settings modal (fallback / first-run) ----------
function showSettingsModal(allSections, prefs, onSave) {
injectStyles();
const overlay = makeOverlay();
const { box, body, footer } = makeModalBox(
t('selectSectionsTitle'),
t('selectSectionsSub')
);
const checkboxMap = {};
const list = document.createElement('div');
list.className = 'bbe-section-list';
allSections.forEach(section => {
const mandatory = MANDATORY_SECTIONS.includes(section);
const item = document.createElement('label');
item.className = 'bbe-section-item' + (mandatory ? ' locked' : '');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = mandatory || (prefs && prefs.enabled.includes(section));
cb.disabled = mandatory;
checkboxMap[section] = cb;
const nameSpan = document.createElement('span');
nameSpan.className = 'bbe-section-name';
nameSpan.textContent = section;
item.appendChild(cb);
item.appendChild(nameSpan);
if (mandatory) {
const badge = document.createElement('span');
badge.className = 'bbe-lock-badge';
badge.textContent = t('always');
item.appendChild(badge);
}
list.appendChild(item);
});
body.appendChild(list);
// Language selector
const langBox = document.createElement('div');
langBox.className = 'bbe-lang-box';
const langLabel = document.createElement('label');
langLabel.className = 'bbe-lang-label';
langLabel.textContent = t('language');
const langSelect = document.createElement('select');
langSelect.className = 'bbe-lang-select';
Object.keys(LANG_NAMES).forEach(code => {
const opt = document.createElement('option');
opt.value = code;
opt.textContent = LANG_NAMES[code];
if (code === currentLang()) opt.selected = true;
langSelect.appendChild(opt);
});
langBox.appendChild(langLabel);
langBox.appendChild(langSelect);
body.appendChild(langBox);
const cancelBtn = document.createElement('button');
cancelBtn.className = 'bbe-btn ghost';
cancelBtn.textContent = t('cancel');
cancelBtn.onclick = () => overlay.remove();
const saveBtn = document.createElement('button');
saveBtn.className = 'bbe-btn primary';
saveBtn.textContent = t('saveContinue');
saveBtn.onclick = () => {
const enabled = Object.keys(checkboxMap)
.filter(s => checkboxMap[s].checked)
.filter(s => !MANDATORY_SECTIONS.includes(s));
const newPrefs = { enabled, lang: langSelect.value };
savePrefs(newPrefs);
overlay.remove();
onSave(newPrefs);
};
footer.appendChild(cancelBtn);
footer.appendChild(saveBtn);
overlay.appendChild(box);
}
// ---------- Main flow ----------
function collectAllSections() {
const info = getInfoGrid();
const sections = Object.keys(info);
MANDATORY_SECTIONS.forEach(s => {
if (!sections.includes(s)) sections.unshift(s);
});
const rest = sections.filter(s => !MANDATORY_SECTIONS.includes(s));
const all = [...MANDATORY_SECTIONS, ...rest];
if (getRating() && !all.includes(RATING_KEY)) all.push(RATING_KEY);
if (getBioText() && !all.includes(BIOGRAPHY_KEY)) all.push(BIOGRAPHY_KEY);
return all;
}
function startFlow() {
const prefs = loadPrefs();
const allSections = collectAllSections();
if (!prefs) {
showSettingsModal(allSections, defaultPrefs(), (newPrefs) => {
showGalleryInputModal(newPrefs);
});
} else {
showGalleryInputModal(prefs);
}
}
// ---------- Floating dropdown menu ----------
function buildMenu() {
injectStyles();
const root = document.createElement('div');
root.className = 'bbe-root';
const trigger = document.createElement('button');
trigger.className = 'bbe-trigger';
trigger.type = 'button';
const panel = document.createElement('div');
panel.className = 'bbe-panel';
const panelHeader = document.createElement('div');
panelHeader.className = 'bbe-panel-header';
const pt = document.createElement('p');
pt.className = 'bbe-panel-title';
const ps = document.createElement('p');
ps.className = 'bbe-panel-sub';
panelHeader.appendChild(pt);
panelHeader.appendChild(ps);
const panelBody = document.createElement('div');
panelBody.className = 'bbe-panel-body';
// --- menu view ---
const menuView = document.createElement('div');
menuView.className = 'bbe-menu-view';
function makeMenuItem({ icon, titleKey, descKey, onClick }) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'bbe-panel-item';
const ic = document.createElement('div');
ic.className = 'bbe-item-icon';
ic.textContent = icon;
const txt = document.createElement('div');
txt.className = 'bbe-item-text';
const ttl = document.createElement('div');
ttl.className = 'bbe-item-title';
ttl.dataset.i18n = titleKey;
const d = document.createElement('div');
d.className = 'bbe-item-desc';
d.dataset.i18n = descKey;
txt.appendChild(ttl);
txt.appendChild(d);
const chev = document.createElement('span');
chev.className = 'bbe-item-chevron';
chev.textContent = '›';
btn.appendChild(ic);
btn.appendChild(txt);
btn.appendChild(chev);
btn.addEventListener('click', () => {
closeMenu();
onClick();
});
return btn;
}
const bbcodeItem = makeMenuItem({
icon: '📋',
titleKey: 'genBBCode',
descKey: 'genBBCodeDesc',
onClick: () => startFlow(),
});
const downloadItem = makeMenuItem({
icon: '⬇️',
titleKey: 'download',
descKey: 'downloadDesc',
onClick: () => {
const urls = getTopImageUrls(6);
if (!urls.length) {
alert(t('noPhotos'));
return;
}
toast(t('downloading', { n: urls.length }));
downloadImages(urls);
},
});
const settingsBtn = document.createElement('button');
settingsBtn.type = 'button';
settingsBtn.className = 'bbe-panel-item';
settingsBtn.innerHTML = `
<div class="bbe-item-icon">⚙️</div>
<div class="bbe-item-text">
<div class="bbe-item-title" data-i18n="settings"></div>
<div class="bbe-item-desc" data-i18n="settingsDesc"></div>
</div>
<span class="bbe-item-chevron">›</span>
`;
menuView.appendChild(bbcodeItem);
menuView.appendChild(downloadItem);
const sep = document.createElement('div');
sep.className = 'bbe-panel-sep';
menuView.appendChild(sep);
menuView.appendChild(settingsBtn);
// --- settings view ---
const settingsView = document.createElement('div');
settingsView.className = 'bbe-settings-view';
const backBtn = document.createElement('button');
backBtn.type = 'button';
backBtn.className = 'bbe-back-btn';
backBtn.dataset.i18n = 'backToMenu';
const sectionList = document.createElement('div');
sectionList.className = 'bbe-section-list';
const checkboxMap = {};
function renderSectionList() {
sectionList.innerHTML = '';
const prefs = loadPrefs() || defaultPrefs();
const allSections = collectAllSections();
Object.keys(checkboxMap).forEach(k => delete checkboxMap[k]);
allSections.forEach(section => {
const mandatory = MANDATORY_SECTIONS.includes(section);
const item = document.createElement('label');
item.className = 'bbe-section-item' + (mandatory ? ' locked' : '');
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = mandatory || prefs.enabled.includes(section);
cb.disabled = mandatory;
checkboxMap[section] = cb;
const nameSpan = document.createElement('span');
nameSpan.className = 'bbe-section-name';
nameSpan.textContent = section;
item.appendChild(cb);
item.appendChild(nameSpan);
if (mandatory) {
const badge = document.createElement('span');
badge.className = 'bbe-lock-badge';
badge.textContent = t('always');
item.appendChild(badge);
}
sectionList.appendChild(item);
});
}
// Language selector (inline in settings view)
const langBox = document.createElement('div');
langBox.className = 'bbe-lang-box';
const langLabel = document.createElement('label');
langLabel.className = 'bbe-lang-label';
langLabel.dataset.i18n = 'language';
const langSelect = document.createElement('select');
langSelect.className = 'bbe-lang-select';
Object.keys(LANG_NAMES).forEach(code => {
const opt = document.createElement('option');
opt.value = code;
opt.textContent = LANG_NAMES[code];
langSelect.appendChild(opt);
});
langBox.appendChild(langLabel);
langBox.appendChild(langSelect);
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'bbe-save-btn';
saveBtn.dataset.i18n = 'saveChoice';
saveBtn.onclick = () => {
const enabled = Object.keys(checkboxMap)
.filter(s => checkboxMap[s].checked)
.filter(s => !MANDATORY_SECTIONS.includes(s));
savePrefs({ enabled, lang: langSelect.value });
applyTranslations();
toast(t('savedChoice'));
closeMenu();
};
settingsView.appendChild(backBtn);
settingsView.appendChild(sectionList);
settingsView.appendChild(langBox);
settingsView.appendChild(saveBtn);
panelBody.appendChild(menuView);
panelBody.appendChild(settingsView);
panel.appendChild(panelHeader);
panel.appendChild(panelBody);
root.appendChild(panel);
root.appendChild(trigger);
document.body.appendChild(root);
function applyTranslations() {
const lang = currentLang();
trigger.innerHTML = `<span class="bbe-icon">☰</span><span>${t('menu')}</span>`;
pt.textContent = t('menuTitle');
ps.textContent = t('menuSub');
// Translate all elements with data-i18n
root.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.dataset.i18n);
});
// Select current language
langSelect.value = lang;
}
// --- open/close logic ---
function openMenu() {
root.classList.add('open');
root.classList.remove('show-settings');
applyTranslations();
renderSectionList();
}
function closeMenu() {
root.classList.remove('open');
setTimeout(() => {
if (!root.classList.contains('open')) {
root.classList.remove('show-settings');
}
}, 180);
}
function toggleMenu() {
if (root.classList.contains('open')) closeMenu();
else openMenu();
}
trigger.addEventListener('click', (e) => {
e.stopPropagation();
toggleMenu();
});
backBtn.addEventListener('click', (e) => {
e.stopPropagation();
root.classList.remove('show-settings');
});
settingsBtn.addEventListener('click', (e) => {
e.stopPropagation();
renderSectionList();
applyTranslations();
root.classList.add('show-settings');
});
// Language change — update immediately in panel, save on saveBtn
langSelect.addEventListener('change', () => {
const prefs = loadPrefs() || defaultPrefs();
prefs.lang = langSelect.value;
savePrefs(prefs);
applyTranslations();
});
document.addEventListener('click', (e) => {
if (!root.contains(e.target)) closeMenu();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeMenu();
});
// Initial translate
applyTranslations();
}
// ---------- Init ----------
buildMenu();
})();