Greasy Fork is available in English.
Automated Thumbs Up/Down. Settings page with themes, list transfer, and more.
// ==UserScript==
// @name FunnyJunk Ultimate Manager
// @namespace http://tampermonkey.net/
// @version 10.0
// @description Automated Thumbs Up/Down. Settings page with themes, list transfer, and more.
// @author Emanon
// @match *://funnyjunk.com/*
// @match *://*.funnyjunk.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
console.log("FJ Manager: Script execution started.");
const loadInterval = setInterval(() => {
if (document.body) {
clearInterval(loadInterval);
try {
initScript();
} catch (e) {
console.error("FJ Manager: CRITICAL FAILURE during init:", e);
alert("FJ Manager Error: " + e.message);
}
}
}, 100);
function initScript() {
// ─── STATE ───────────────────────────────────────────────────────────────
let isRunning = false;
let currentMode = 'skip';
let permanentBlocklist = new Set();
let tempTargetSet = new Set();
let isSettingsOpen = false;
// ─── SETTINGS STORAGE KEY ────────────────────────────────────────────────
const STORAGE_KEY = 'fj_user_blocklist';
const SETTINGS_KEY = 'fj_manager_settings';
// ─── THEMES ──────────────────────────────────────────────────────────────
const THEMES = {
green: { name: 'Green', accent: '#4CAF50', accentHover: '#66BB6A', accentText: '#fff' },
blue: { name: 'Blue', accent: '#2196F3', accentHover: '#42A5F5', accentText: '#fff' },
purple: { name: 'Purple', accent: '#9C27B0', accentHover: '#AB47BC', accentText: '#fff' },
red: { name: 'Red', accent: '#F44336', accentHover: '#EF5350', accentText: '#fff' },
orange: { name: 'Orange', accent: '#FF9800', accentHover: '#FFA726', accentText: '#fff' },
cyan: { name: 'Cyan', accent: '#00BCD4', accentHover: '#26C6DA', accentText: '#fff' },
pink: { name: 'Pink', accent: '#E91E63', accentHover: '#EC407A', accentText: '#fff' },
mono: { name: 'Mono', accent: '#9E9E9E', accentHover: '#BDBDBD', accentText: '#fff' },
};
// ─── DEFAULT SETTINGS ────────────────────────────────────────────────────
let settings = {
theme: 'green',
defaultMode: 'skip',
defaultMinimized: false,
};
function loadSettings() {
try {
const saved = localStorage.getItem(SETTINGS_KEY);
if (saved) settings = Object.assign(settings, JSON.parse(saved));
} catch(e) {}
currentMode = settings.defaultMode;
}
function saveSettings() {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
applyTheme();
modeSelect.value = currentMode;
}
// ─── BLOCKLIST STORAGE ───────────────────────────────────────────────────
function loadBlocklist() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) permanentBlocklist = new Set(JSON.parse(saved));
} catch (e) { permanentBlocklist = new Set(); }
}
function saveBlocklist() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(permanentBlocklist)));
renderBlocklistUI();
}
// ─── HELPERS ─────────────────────────────────────────────────────────────
function getUsernameFromLink(linkElement) {
if (!linkElement) return null;
const href = linkElement.getAttribute('href');
if (!href) return null;
const match = href.match(/\/user\/([^\/]+)/);
return (match && match[1]) ? match[1].toLowerCase() : null;
}
function addToBlocklist(username) {
if (!username) return;
permanentBlocklist.add(username.toLowerCase().trim());
saveBlocklist();
}
function removeFromBlocklist(username) {
permanentBlocklist.delete(username);
saveBlocklist();
}
function triggerHumanClick(element) {
if (!element) return;
['mouseover', 'mousedown', 'mouseup', 'click'].forEach(type => {
element.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
});
}
// ─── THEME APPLICATION ───────────────────────────────────────────────────
function applyTheme() {
const t = THEMES[settings.theme] || THEMES.green;
mainPanel.style.borderColor = isRunning ? '#FF0000' : t.accent;
btn.style.backgroundColor = t.accent;
settingsBtn.style.backgroundColor = t.accent;
restoreBtn.style.backgroundColor = t.accent;
// Update theme swatch selections in settings if open
document.querySelectorAll('.fj-theme-swatch').forEach(sw => {
sw.style.outline = (sw.dataset.theme === settings.theme)
? `3px solid #fff` : '3px solid transparent';
});
// Update mode select highlight
if (modeSelect.value !== currentMode) modeSelect.value = currentMode;
}
// ─── SCAN & INJECT ───────────────────────────────────────────────────────
function performScan() {
const thumbs = document.querySelectorAll('span[id^="up_"]');
const usersOnPage = new Set();
let injectedCount = 0;
thumbs.forEach(thumb => {
const container = thumb.closest('div[id^="c"]');
if (!container) return;
const userLink = container.querySelector('a[href*="/user/"]');
if (userLink) {
const cleanName = getUsernameFromLink(userLink);
if (cleanName) usersOnPage.add(cleanName);
if (!container.querySelector('.fj-auto-checkbox')) {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'fj-auto-checkbox';
checkbox.dataset.cleanName = cleanName;
Object.assign(checkbox.style, {
marginRight: '8px', cursor: 'pointer',
width: '18px', height: '18px',
accentColor: THEMES[settings.theme].accent,
verticalAlign: 'middle'
});
checkbox.title = `Target User: ${cleanName}`;
userLink.parentNode.insertBefore(checkbox, userLink);
injectedCount++;
}
}
});
updatePageUserDropdown(usersOnPage);
return injectedCount;
}
function buildTempList() {
tempTargetSet.clear();
document.querySelectorAll('.fj-auto-checkbox:checked').forEach(box => {
if (box.dataset.cleanName) tempTargetSet.add(box.dataset.cleanName);
});
}
// ─── CLICK ENGINE ────────────────────────────────────────────────────────
function clickNextThumb() {
if (!isRunning) return;
const allThumbs = document.querySelectorAll('span[id^="up_"]');
const visibleThumbs = Array.from(allThumbs)
.filter(el => !el.hasAttribute('data-processed') && el.offsetParent !== null);
if (visibleThumbs.length > 0) {
const upBtn = visibleThumbs[0];
const container = upBtn.closest('div[id^="c"]');
let currentName = null;
let isDirectlyChecked = false;
if (container) {
currentName = getUsernameFromLink(container.querySelector('a[href*="/user/"]'));
const localCheckbox = container.querySelector('.fj-auto-checkbox');
if (localCheckbox && localCheckbox.checked) isDirectlyChecked = true;
}
const isTargeted = (currentName && permanentBlocklist.has(currentName))
|| (currentName && tempTargetSet.has(currentName))
|| isDirectlyChecked;
if (isTargeted) {
if (currentMode === 'downvote') {
let downBtn = upBtn.id?.startsWith('up_')
? document.getElementById(upBtn.id.replace('up_', 'dn_'))
: null;
if (!downBtn && container) downBtn = container.querySelector('.thDn, .thDn_i');
if (downBtn) {
if (!(downBtn.className.includes('_i') || downBtn.classList.length > 1))
triggerHumanClick(downBtn);
}
}
upBtn.setAttribute('data-processed', 'true');
} else {
const isUpActive = upBtn.className.includes('_i') || upBtn.classList.length > 1;
if (!isUpActive) triggerHumanClick(upBtn);
else upBtn.setAttribute('data-processed', 'true');
}
statusText.innerText = `${visibleThumbs.length - 1} left`;
setTimeout(clickNextThumb, 150);
} else {
finishClicking();
}
}
function finishClicking() {
isRunning = false;
const t = THEMES[settings.theme] || THEMES.green;
statusText.innerText = 'Finished!';
mainPanel.style.borderColor = t.accent;
setTimeout(() => { statusText.innerText = 'Ready'; }, 3000);
}
function toggleProcess() {
if (isRunning) {
isRunning = false;
statusText.innerText = 'Stopped';
mainPanel.style.borderColor = THEMES[settings.theme].accent;
return;
}
performScan();
buildTempList();
statusText.innerText = 'Running...';
mainPanel.style.borderColor = '#FF0000';
isRunning = true;
clickNextThumb();
}
// ─── EXPORT / IMPORT HELPERS ─────────────────────────────────────────────
function exportList() {
if (permanentBlocklist.size === 0) return '';
const payload = JSON.stringify({ v: 1, users: Array.from(permanentBlocklist).sort() });
return btoa(unescape(encodeURIComponent(payload)));
}
function importList(str) {
// Returns { added: number, dupes: number } or null on error
try {
const decoded = decodeURIComponent(escape(atob(str.trim())));
const parsed = JSON.parse(decoded);
if (!parsed.users || !Array.isArray(parsed.users)) return null;
let added = 0, dupes = 0;
parsed.users.forEach(u => {
const clean = u.toLowerCase().trim();
if (permanentBlocklist.has(clean)) dupes++;
else { permanentBlocklist.add(clean); added++; }
});
if (added > 0) saveBlocklist();
return { added, dupes };
} catch(e) { return null; }
}
function bulkAdd(raw) {
const names = raw.split(/[\n,]+/).map(s => s.toLowerCase().trim()).filter(Boolean);
let added = 0;
names.forEach(n => { if (!permanentBlocklist.has(n)) { permanentBlocklist.add(n); added++; } });
if (added > 0) saveBlocklist();
return { added, total: names.length };
}
// ═══════════════════════════════════════════════════════════════════════
// ─── UI CONSTRUCTION ──────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════════
// Inject global styles for the settings overlay
const styleEl = document.createElement('style');
styleEl.textContent = `
.fj-settings-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
z-index: 2147483646; display: flex;
align-items: center; justify-content: center;
}
.fj-settings-panel {
background: #1a1a1a; color: #fff;
border-radius: 12px; width: 380px; max-height: 85vh;
overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.9);
font-family: Arial, sans-serif; font-size: 13px;
}
.fj-settings-panel::-webkit-scrollbar { width: 5px; }
.fj-settings-panel::-webkit-scrollbar-track { background: #111; }
.fj-settings-panel::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
.fj-settings-header {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 16px; border-bottom: 1px solid #333;
position: sticky; top: 0; background: #1a1a1a; z-index: 1;
}
.fj-settings-header h2 { margin: 0; font-size: 15px; font-weight: bold; }
.fj-settings-close {
cursor: pointer; background: #333; border: none; color: #fff;
width: 28px; height: 28px; border-radius: 50%;
font-size: 16px; display: flex; align-items: center; justify-content: center;
}
.fj-settings-close:hover { background: #555; }
.fj-section {
padding: 14px 16px; border-bottom: 1px solid #2a2a2a;
}
.fj-section-title {
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
color: #888; margin-bottom: 10px; font-weight: bold;
}
.fj-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.fj-theme-swatch {
width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
border: none; transition: outline 0.15s;
}
.fj-toggle-row {
display: flex; align-items: center; justify-content: space-between;
padding: 6px 0;
}
.fj-toggle-label { color: #ccc; font-size: 13px; }
.fj-toggle {
position: relative; width: 38px; height: 21px;
}
.fj-toggle input { opacity: 0; width: 0; height: 0; }
.fj-slider {
position: absolute; inset: 0; background: #444;
border-radius: 21px; cursor: pointer; transition: background 0.2s;
}
.fj-slider:before {
content: ''; position: absolute; width: 15px; height: 15px;
left: 3px; top: 3px; background: #fff; border-radius: 50%;
transition: transform 0.2s;
}
.fj-toggle input:checked + .fj-slider { background: var(--fj-accent, #4CAF50); }
.fj-toggle input:checked + .fj-slider:before { transform: translateX(17px); }
.fj-mode-btn {
flex: 1; padding: 7px 10px; border-radius: 6px; cursor: pointer;
border: 1.5px solid #444; background: #2a2a2a; color: #ccc;
font-size: 12px; font-weight: bold; text-align: center; transition: all 0.15s;
}
.fj-mode-btn.active {
border-color: var(--fj-accent, #4CAF50);
background: color-mix(in srgb, var(--fj-accent, #4CAF50) 20%, #1a1a1a);
color: #fff;
}
.fj-mode-btn:hover:not(.active) { border-color: #666; color: #fff; }
.fj-input {
width: 100%; background: #2a2a2a; border: 1px solid #444;
color: #fff; border-radius: 6px; padding: 7px 10px;
font-size: 12px; box-sizing: border-box; resize: vertical;
}
.fj-input:focus { outline: none; border-color: var(--fj-accent, #4CAF50); }
.fj-btn {
padding: 7px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;
font-weight: bold; border: none; background: #333; color: #fff; transition: background 0.15s;
}
.fj-btn:hover { background: #444; }
.fj-btn.primary { background: var(--fj-accent, #4CAF50); }
.fj-btn.primary:hover { background: var(--fj-accent-hover, #66BB6A); }
.fj-btn.danger { background: #c62828; }
.fj-btn.danger:hover { background: #e53935; }
.fj-notice {
padding: 6px 10px; border-radius: 6px; font-size: 12px;
margin-top: 6px; display: none;
}
.fj-notice.ok { background: #1b5e20; color: #a5d6a7; display: block; }
.fj-notice.err { background: #b71c1c; color: #ffcdd2; display: block; }
.fj-notice.info { background: #0d47a1; color: #bbdefb; display: block; }
.fj-bl-item {
display: flex; justify-content: space-between; align-items: center;
padding: 3px 6px; border-radius: 4px; font-size: 12px;
}
.fj-bl-item:hover { background: #2a2a2a; }
.fj-bl-del { cursor: pointer; color: #888; font-size: 14px; padding: 0 4px; }
.fj-bl-del:hover { color: #f44336; }
.fj-bl-scroll {
max-height: 130px; overflow-y: auto; background: #111;
border: 1px solid #333; border-radius: 6px; padding: 4px;
}
.fj-bl-scroll::-webkit-scrollbar { width: 4px; }
.fj-bl-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
`;
document.head.appendChild(styleEl);
// ─── RESTORE BUTTON ──────────────────────────────────────────────────────
const restoreBtn = document.createElement('div');
restoreBtn.innerText = 'M';
Object.assign(restoreBtn.style, {
position: 'fixed', top: '100px', right: '10px', zIndex: '2147483647',
backgroundColor: THEMES[settings.theme].accent, color: 'white',
width: '30px', height: '30px', borderRadius: '50%',
display: 'none', justifyContent: 'center', alignItems: 'center',
cursor: 'pointer', fontWeight: 'bold', fontSize: '14px',
boxShadow: '0 2px 5px rgba(0,0,0,0.5)', border: '2px solid white'
});
restoreBtn.title = 'Restore Manager';
// ─── MAIN PANEL ──────────────────────────────────────────────────────────
const mainPanel = document.createElement('div');
Object.assign(mainPanel.style, {
position: 'fixed', top: '100px', right: '10px', zIndex: '2147483647',
backgroundColor: '#222', color: 'white', padding: '10px',
borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.8)',
fontFamily: 'Arial, sans-serif', fontSize: '12px', width: '220px',
border: '2px solid #4CAF50', display: 'flex', flexDirection: 'column', gap: '8px'
});
function minimizePanel() {
mainPanel.style.display = 'none';
restoreBtn.style.display = 'flex';
}
function restorePanel() {
mainPanel.style.display = 'flex';
restoreBtn.style.display = 'none';
}
restoreBtn.addEventListener('click', restorePanel);
// ─── HEADER ROW ──────────────────────────────────────────────────────────
const headerRow = document.createElement('div');
Object.assign(headerRow.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
const statusText = document.createElement('div');
statusText.innerText = 'Ready';
statusText.style.fontWeight = 'bold';
statusText.style.fontSize = '14px';
const headerBtns = document.createElement('div');
headerBtns.style.display = 'flex';
headerBtns.style.gap = '4px';
// Settings button
const settingsBtn = document.createElement('div');
settingsBtn.innerHTML = '⚙';
settingsBtn.title = 'Settings';
Object.assign(settingsBtn.style, {
cursor: 'pointer', fontWeight: 'bold', padding: '0 5px',
backgroundColor: THEMES[settings.theme].accent,
color: '#fff', border: '1px solid #555', borderRadius: '3px',
fontSize: '13px', lineHeight: '20px'
});
// Minimize button
const minBtn = document.createElement('div');
minBtn.innerText = '_';
minBtn.style.cursor = 'pointer';
minBtn.style.fontWeight = 'bold';
minBtn.style.padding = '0 5px';
minBtn.style.color = '#aaa';
minBtn.title = 'Hide Panel';
minBtn.style.border = '1px solid #555';
minBtn.style.borderRadius = '3px';
minBtn.addEventListener('click', minimizePanel);
headerBtns.appendChild(settingsBtn);
headerBtns.appendChild(minBtn);
headerRow.appendChild(statusText);
headerRow.appendChild(headerBtns);
// ─── START/STOP BUTTON ───────────────────────────────────────────────────
const btn = document.createElement('button');
btn.innerText = 'START / STOP';
Object.assign(btn.style, {
backgroundColor: THEMES[settings.theme].accent, color: 'white',
border: 'none', padding: '8px', cursor: 'pointer',
fontWeight: 'bold', borderRadius: '4px'
});
btn.addEventListener('click', toggleProcess);
// ─── MODE SELECT ─────────────────────────────────────────────────────────
const modeSelect = document.createElement('select');
modeSelect.style.padding = '5px';
modeSelect.add(new Option('Mode: Skip Targets', 'skip'));
modeSelect.add(new Option('Mode: Downvote Targets', 'downvote'));
modeSelect.addEventListener('change', (e) => { currentMode = e.target.value; });
// ─── DIVIDER ─────────────────────────────────────────────────────────────
const divider = document.createElement('hr');
Object.assign(divider.style, { width: '100%', borderColor: '#555', margin: '5px 0' });
// ─── BLOCKLIST SECTION ───────────────────────────────────────────────────
const listHeader = document.createElement('div');
listHeader.innerText = 'Permanent List [+]';
Object.assign(listHeader.style, {
fontWeight: 'bold', cursor: 'pointer', userSelect: 'none',
padding: '5px', backgroundColor: '#333', borderRadius: '4px', textAlign: 'center'
});
const listContent = document.createElement('div');
Object.assign(listContent.style, { display: 'none', flexDirection: 'column', gap: '8px' });
let isListExpanded = false;
listHeader.addEventListener('click', () => {
isListExpanded = !isListExpanded;
listContent.style.display = isListExpanded ? 'flex' : 'none';
listHeader.innerText = isListExpanded ? 'Permanent List [-]' : 'Permanent List [+]';
});
const refreshRow = document.createElement('div');
refreshRow.style.display = 'flex';
refreshRow.style.gap = '5px';
const pageUserSelect = document.createElement('select');
pageUserSelect.style.width = '100%';
pageUserSelect.innerHTML = '<option value="">-- Load Users --</option>';
const refreshBtn = document.createElement('button');
refreshBtn.innerText = '⟳';
refreshBtn.title = 'Scan page for new users';
refreshBtn.style.cursor = 'pointer';
refreshBtn.style.fontWeight = 'bold';
refreshBtn.addEventListener('click', () => {
const count = performScan();
statusText.innerText = `Scanned ${count || 'page'}`;
setTimeout(() => { if (!isRunning) statusText.innerText = 'Ready'; }, 2000);
});
refreshRow.appendChild(pageUserSelect);
refreshRow.appendChild(refreshBtn);
const addFromPageBtn = document.createElement('button');
addFromPageBtn.innerText = 'Add Selected User';
addFromPageBtn.style.cursor = 'pointer';
addFromPageBtn.addEventListener('click', () => {
if (pageUserSelect.value) {
addToBlocklist(pageUserSelect.value);
pageUserSelect.value = '';
}
});
const manualInput = document.createElement('input');
manualInput.placeholder = 'Type username...';
manualInput.style.width = '95%';
const addManualBtn = document.createElement('button');
addManualBtn.innerText = 'Add Manual User';
addManualBtn.style.cursor = 'pointer';
addManualBtn.addEventListener('click', () => {
if (manualInput.value) { addToBlocklist(manualInput.value); manualInput.value = ''; }
});
const blocklistView = document.createElement('div');
Object.assign(blocklistView.style, {
maxHeight: '100px', overflowY: 'auto', backgroundColor: '#333',
padding: '5px', borderRadius: '4px', border: '1px solid #555'
});
function renderBlocklistUI() {
blocklistView.innerHTML = '';
if (permanentBlocklist.size === 0) { blocklistView.innerText = '(List empty)'; return; }
permanentBlocklist.forEach(user => {
const row = document.createElement('div');
Object.assign(row.style, { display: 'flex', justifyContent: 'space-between', marginBottom: '2px' });
const nameSpan = document.createElement('span');
nameSpan.innerText = user;
const delBtn = document.createElement('span');
delBtn.innerText = '❌';
delBtn.style.cursor = 'pointer';
delBtn.onclick = () => removeFromBlocklist(user);
row.appendChild(nameSpan);
row.appendChild(delBtn);
blocklistView.appendChild(row);
});
}
function updatePageUserDropdown(usersSet) {
const previousVal = pageUserSelect.value;
pageUserSelect.innerHTML = '<option value="">-- Select from Page --</option>';
const sortedUsers = Array.from(usersSet).sort();
if (sortedUsers.length === 0) {
pageUserSelect.innerHTML = '<option value="">No users found yet</option>';
}
sortedUsers.forEach(user => pageUserSelect.add(new Option(user, user)));
if (usersSet.has(previousVal)) pageUserSelect.value = previousVal;
}
// ─── ASSEMBLE MAIN PANEL ─────────────────────────────────────────────────
mainPanel.appendChild(headerRow);
mainPanel.appendChild(btn);
mainPanel.appendChild(modeSelect);
mainPanel.appendChild(divider);
mainPanel.appendChild(listHeader);
listContent.appendChild(refreshRow);
listContent.appendChild(addFromPageBtn);
listContent.appendChild(manualInput);
listContent.appendChild(addManualBtn);
listContent.appendChild(blocklistView);
mainPanel.appendChild(listContent);
// ═══════════════════════════════════════════════════════════════════════
// ─── SETTINGS OVERLAY ────────────────────────────────────────────────
// ═══════════════════════════════════════════════════════════════════════
function buildSettingsOverlay() {
const overlay = document.createElement('div');
overlay.className = 'fj-settings-overlay';
overlay.addEventListener('click', (e) => { if (e.target === overlay) closeSettings(overlay); });
const panel = document.createElement('div');
panel.className = 'fj-settings-panel';
// Inject CSS variable for accent
panel.style.setProperty('--fj-accent', THEMES[settings.theme].accent);
panel.style.setProperty('--fj-accent-hover', THEMES[settings.theme].accentHover);
// ── Header ──
const hdr = document.createElement('div');
hdr.className = 'fj-settings-header';
hdr.innerHTML = '<h2>⚙ FJ Manager Settings</h2>';
const closeX = document.createElement('button');
closeX.className = 'fj-settings-close';
closeX.innerHTML = '✕';
closeX.addEventListener('click', () => closeSettings(overlay));
hdr.appendChild(closeX);
panel.appendChild(hdr);
// ── Section: Defaults ────────────────────────────────────────────
const secDefaults = document.createElement('div');
secDefaults.className = 'fj-section';
secDefaults.innerHTML = '<div class="fj-section-title">Defaults</div>';
// Default Mode
const modeLabel = document.createElement('div');
modeLabel.style.cssText = 'color:#aaa;font-size:12px;margin-bottom:6px;';
modeLabel.innerText = 'Default mode on startup';
secDefaults.appendChild(modeLabel);
const modeRow = document.createElement('div');
modeRow.className = 'fj-row';
modeRow.style.marginBottom = '12px';
const mkModeBtn = (label, val) => {
const b = document.createElement('div');
b.className = 'fj-mode-btn' + (settings.defaultMode === val ? ' active' : '');
b.dataset.val = val;
b.innerText = label;
b.addEventListener('click', () => {
settings.defaultMode = val;
currentMode = val;
modeRow.querySelectorAll('.fj-mode-btn').forEach(x => {
x.classList.toggle('active', x.dataset.val === val);
});
modeSelect.value = val;
});
return b;
};
modeRow.appendChild(mkModeBtn('Skip Targets', 'skip'));
modeRow.appendChild(mkModeBtn('Downvote Targets', 'downvote'));
secDefaults.appendChild(modeRow);
// Default Minimized toggle
const minToggleRow = document.createElement('div');
minToggleRow.className = 'fj-toggle-row';
const minToggleLabel = document.createElement('span');
minToggleLabel.className = 'fj-toggle-label';
minToggleLabel.innerText = 'Start minimized by default';
const minToggleWrap = document.createElement('label');
minToggleWrap.className = 'fj-toggle';
const minToggleInput = document.createElement('input');
minToggleInput.type = 'checkbox';
minToggleInput.checked = settings.defaultMinimized;
minToggleInput.addEventListener('change', () => { settings.defaultMinimized = minToggleInput.checked; });
const minSlider = document.createElement('span');
minSlider.className = 'fj-slider';
minToggleWrap.appendChild(minToggleInput);
minToggleWrap.appendChild(minSlider);
minToggleRow.appendChild(minToggleLabel);
minToggleRow.appendChild(minToggleWrap);
secDefaults.appendChild(minToggleRow);
panel.appendChild(secDefaults);
// ── Section: Theme ───────────────────────────────────────────────
const secTheme = document.createElement('div');
secTheme.className = 'fj-section';
secTheme.innerHTML = '<div class="fj-section-title">Color Theme</div>';
const swatchRow = document.createElement('div');
swatchRow.className = 'fj-row';
swatchRow.style.flexWrap = 'wrap';
swatchRow.style.gap = '10px';
Object.entries(THEMES).forEach(([key, t]) => {
const swatch = document.createElement('button');
swatch.className = 'fj-theme-swatch';
swatch.dataset.theme = key;
swatch.style.backgroundColor = t.accent;
swatch.style.outline = (key === settings.theme) ? '3px solid #fff' : '3px solid transparent';
swatch.style.outlineOffset = '2px';
swatch.title = t.name;
swatch.addEventListener('click', () => {
settings.theme = key;
panel.style.setProperty('--fj-accent', t.accent);
panel.style.setProperty('--fj-accent-hover', t.accentHover);
swatchRow.querySelectorAll('.fj-theme-swatch').forEach(sw => {
sw.style.outline = (sw.dataset.theme === key) ? '3px solid #fff' : '3px solid transparent';
});
});
swatchRow.appendChild(swatch);
});
const themeNames = document.createElement('div');
themeNames.style.cssText = 'font-size:11px;color:#666;margin-top:4px;';
themeNames.innerText = Object.values(THEMES).map(t => t.name).join(' · ');
secTheme.appendChild(swatchRow);
secTheme.appendChild(themeNames);
panel.appendChild(secTheme);
// ── Section: Export ──────────────────────────────────────────────
const secExport = document.createElement('div');
secExport.className = 'fj-section';
secExport.innerHTML = `<div class="fj-section-title">Export List (${permanentBlocklist.size} users)</div>`;
const exportTA = document.createElement('textarea');
exportTA.className = 'fj-input';
exportTA.readOnly = true;
exportTA.rows = 3;
exportTA.placeholder = 'Click Generate to create export string...';
exportTA.style.fontFamily = 'monospace';
const exportRow = document.createElement('div');
exportRow.className = 'fj-row';
exportRow.style.marginTop = '6px';
const genBtn = document.createElement('button');
genBtn.className = 'fj-btn primary';
genBtn.innerText = 'Generate';
genBtn.addEventListener('click', () => {
const str = exportList();
if (!str) { showNotice(exportNotice, 'List is empty', 'err'); return; }
exportTA.value = str;
showNotice(exportNotice, `${permanentBlocklist.size} users exported`, 'ok');
});
const copyBtn = document.createElement('button');
copyBtn.className = 'fj-btn';
copyBtn.innerText = '📋 Copy';
copyBtn.addEventListener('click', () => {
if (!exportTA.value) { showNotice(exportNotice, 'Generate first', 'err'); return; }
navigator.clipboard.writeText(exportTA.value).then(() => {
showNotice(exportNotice, 'Copied to clipboard!', 'ok');
}).catch(() => {
exportTA.select();
showNotice(exportNotice, 'Select all + Ctrl+C to copy', 'info');
});
});
const exportNotice = document.createElement('div');
exportNotice.className = 'fj-notice';
exportRow.appendChild(genBtn);
exportRow.appendChild(copyBtn);
secExport.appendChild(exportTA);
secExport.appendChild(exportRow);
secExport.appendChild(exportNotice);
panel.appendChild(secExport);
// ── Section: Import ──────────────────────────────────────────────
const secImport = document.createElement('div');
secImport.className = 'fj-section';
secImport.innerHTML = '<div class="fj-section-title">Import & Merge</div>';
const importTA = document.createElement('textarea');
importTA.className = 'fj-input';
importTA.rows = 3;
importTA.placeholder = 'Paste an exported list string here...';
importTA.style.fontFamily = 'monospace';
const importRow = document.createElement('div');
importRow.className = 'fj-row';
importRow.style.marginTop = '6px';
const importBtn = document.createElement('button');
importBtn.className = 'fj-btn primary';
importBtn.innerText = 'Merge into List';
const importNotice = document.createElement('div');
importNotice.className = 'fj-notice';
importBtn.addEventListener('click', () => {
const result = importList(importTA.value);
if (!result) {
showNotice(importNotice, 'Invalid export string — check and try again', 'err');
return;
}
importTA.value = '';
showNotice(importNotice, `Added ${result.added} user(s) · ${result.dupes} already in list`, 'ok');
renderBlocklistUI();
// Update export section heading
secExport.querySelector('.fj-section-title').innerText = `Export List (${permanentBlocklist.size} users)`;
});
importRow.appendChild(importBtn);
secImport.appendChild(importTA);
secImport.appendChild(importRow);
secImport.appendChild(importNotice);
panel.appendChild(secImport);
// ── Section: Bulk Add ────────────────────────────────────────────
const secBulk = document.createElement('div');
secBulk.className = 'fj-section';
secBulk.innerHTML = '<div class="fj-section-title">Bulk Add Usernames</div>';
const bulkTA = document.createElement('textarea');
bulkTA.className = 'fj-input';
bulkTA.rows = 3;
bulkTA.placeholder = 'One per line or comma-separated\nuser1\nuser2, user3';
const bulkRow = document.createElement('div');
bulkRow.className = 'fj-row';
bulkRow.style.marginTop = '6px';
const bulkBtn = document.createElement('button');
bulkBtn.className = 'fj-btn primary';
bulkBtn.innerText = 'Add All';
const bulkNotice = document.createElement('div');
bulkNotice.className = 'fj-notice';
bulkBtn.addEventListener('click', () => {
if (!bulkTA.value.trim()) { showNotice(bulkNotice, 'Enter some usernames first', 'err'); return; }
const result = bulkAdd(bulkTA.value);
bulkTA.value = '';
showNotice(bulkNotice, `Added ${result.added} new · ${result.total - result.added} already in list`, 'ok');
renderBlocklistUI();
});
bulkRow.appendChild(bulkBtn);
secBulk.appendChild(bulkTA);
secBulk.appendChild(bulkRow);
secBulk.appendChild(bulkNotice);
panel.appendChild(secBulk);
// ── Section: Current List Preview ────────────────────────────────
const secList = document.createElement('div');
secList.className = 'fj-section';
secList.innerHTML = `<div class="fj-section-title">Current Blocklist (${permanentBlocklist.size})</div>`;
const blScroll = document.createElement('div');
blScroll.className = 'fj-bl-scroll';
const renderSettingsBL = () => {
blScroll.innerHTML = '';
if (permanentBlocklist.size === 0) {
blScroll.innerHTML = '<div style="color:#666;font-size:12px;padding:4px;">(empty)</div>';
return;
}
Array.from(permanentBlocklist).sort().forEach(u => {
const row = document.createElement('div');
row.className = 'fj-bl-item';
const nameEl = document.createElement('span');
nameEl.innerText = u;
const delEl = document.createElement('span');
delEl.className = 'fj-bl-del';
delEl.innerText = '✕';
delEl.onclick = () => {
removeFromBlocklist(u);
renderSettingsBL();
secList.querySelector('.fj-section-title').innerText = `Current Blocklist (${permanentBlocklist.size})`;
};
row.appendChild(nameEl);
row.appendChild(delEl);
blScroll.appendChild(row);
});
};
renderSettingsBL();
secList.appendChild(blScroll);
panel.appendChild(secList);
// ── Footer: Save ─────────────────────────────────────────────────
const footer = document.createElement('div');
footer.className = 'fj-section';
footer.style.display = 'flex';
footer.style.gap = '8px';
const saveBtn = document.createElement('button');
saveBtn.className = 'fj-btn primary';
saveBtn.style.flex = '1';
saveBtn.innerText = '✔ Save Settings';
saveBtn.addEventListener('click', () => {
saveSettings();
closeSettings(overlay);
});
const cancelBtn = document.createElement('button');
cancelBtn.className = 'fj-btn';
cancelBtn.innerText = 'Cancel';
cancelBtn.addEventListener('click', () => closeSettings(overlay));
footer.appendChild(saveBtn);
footer.appendChild(cancelBtn);
panel.appendChild(footer);
overlay.appendChild(panel);
return overlay;
}
function showNotice(el, msg, type) {
el.className = `fj-notice ${type}`;
el.innerText = msg;
}
function openSettings() {
if (isSettingsOpen) return;
isSettingsOpen = true;
const overlay = buildSettingsOverlay();
document.body.appendChild(overlay);
}
function closeSettings(overlay) {
isSettingsOpen = false;
overlay.remove();
}
settingsBtn.addEventListener('click', openSettings);
// ─── INJECT INTO PAGE ────────────────────────────────────────────────────
document.body.appendChild(restoreBtn);
document.body.appendChild(mainPanel);
console.log("FJ Manager: UI Injected successfully.");
// ─── INIT ────────────────────────────────────────────────────────────────
loadSettings();
loadBlocklist();
renderBlocklistUI();
modeSelect.value = currentMode;
applyTheme();
if (settings.defaultMinimized) {
minimizePanel();
}
setTimeout(performScan, 2000);
}
})();