Greasy Fork is available in English.
One-click access toolbar!
// ==UserScript==
// @version 1.15.4
// @name NavCarousel 🎞️
// @description One-click access toolbar!
// @icon https://fitgirl-repacks.site/favicon.ico
// @match *://fitgirl-repacks.site/*
// @match *://fitgirl-repacks.site/feed/*
// @grant GM_setClipboard
// @grant GM_addStyle
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @connect 127.0.0.1
// @connect localhost
// @run-at document-idle
// @license MIT
// @namespace name1or2-1510385-jd4dsc
// @author name1or2
// ==/UserScript==
(function() {
'use strict';
const CONFIG = {
CAROUSEL_MAX_WIDTH: 1100,
CAROUSEL_SCROLL_SPEED: 30,
CAROUSEL_MIN_BUTTONS: 4,
NOTIFICATION_DURATION: 2000,
BUTTON_FLASH_DURATION: 1500,
OBSERVER_DEBOUNCE: 500,
CACHE_MAX_ENTRIES: 100,
CACHE_KEY_PREFIX: 'fitgirl_cache_',
DEFAULT_SETTINGS: {
carousel_speed: 30,
use_native_notifications: true,
buttons_visible: {
steam: true, steam_search: true, steamgriddb: true,
pcgw: true, fuckingfast: true, jdownloader: true
}
}
};
GM_addStyle(`
article .fitgirl-button-bar-wrapper{max-width:${CONFIG.CAROUSEL_MAX_WIDTH}px;overflow:hidden;margin:10px 0;position:relative}
article .fitgirl-button-bar{display:inline-flex;gap:8px;padding:8px;background-color:#181a1b;border:1px solid #3e4446;border-radius:10px;flex-wrap:nowrap;white-space:nowrap;will-change:transform}
article .fitgirl-button{display:flex;align-items:center;justify-content:center;gap:6px;padding:6px 12px;margin:0 4px;font-size:12px;font-weight:500;color:white;border-radius:3px;transition:all .2s ease;cursor:pointer;border:none;height:36px;flex-shrink:0}
article .fitgirl-button.unavailable{filter:grayscale(100%);opacity:.4;cursor:not-allowed}
article .fitgirl-button.fuckingfast{background-color:#2b2a33}
article .fitgirl-button.fuckingfast:hover:not(.success):not(.error){background-color:#52525e}
article .fitgirl-button.success{background-color:#339966}
article .fitgirl-button.error{background-color:#ff3333}
article .fitgirl-button:active{transform:scale(.95)}
article .fitgirl-icon{width:24px;height:24px;flex-shrink:0}
article .fitgirl-button.fitgirl-steam-button{background-color:rgba(103,193,245,.2);color:#67c1f5;font-family:"Motiva Sans",Arial,Helvetica,sans-serif;font-size:13px;padding:0 15px;border-radius:3px;box-shadow:1px 1px 0 #0000001a;height:36px;line-height:30px;text-shadow:none}
article .fitgirl-button.fitgirl-steam-button:hover{background-color:#417a9b;color:#67c1f5;text-decoration:none}
article .fitgirl-button.fitgirl-steam-button:active{background-color:#2b485f;color:#67c1f5;box-shadow:inset 0 2px 4px rgba(0,0,0,.3);transform:none}
article .fitgirl-button.fitgirl-steam-search-button{background:#1a9fff;width:34px;height:34px;padding:0;border-radius:2px;display:flex;align-items:center;justify-content:center;border:none;color:#fff;cursor:pointer;transition:background .2s ease-out,box-shadow .2s ease-out,transform .2s ease-out;flex-shrink:0}
article .fitgirl-button.fitgirl-steam-search-button svg{width:18px;height:18px;transition:transform .2s ease-out}
article .fitgirl-button.fitgirl-steam-search-button:hover{background:#45acff;box-shadow:2px 2px 3px rgba(0,0,0,.2)}
article .fitgirl-button.fitgirl-steam-search-button:hover svg{transform:scale(1.2)}
article .fitgirl-button.fitgirl-steam-search-button:active{transform:translateY(1px)}
article .fitgirl-button.fitgirl-steamgriddb-button{background-color:#32414C;color:#5FB4F0;font-family:"Open Sans",sans-serif;font-size:11px;font-weight:600;text-transform:uppercase;text-decoration:underline;padding:6px 12px}
article .fitgirl-button.fitgirl-steamgriddb-button:hover:not(.success):not(.error){background-color:#263238;color:#8ECAF4}
article .fitgirl-button.fitgirl-steamgriddb-button img{height:20px;width:auto}
article .fitgirl-button.fitgirl-pcgw-button{background-color:#262A2B;padding:6px 12px}
article .fitgirl-button.fitgirl-pcgw-button:hover:not(.success):not(.error){background-color:#141516}
article .fitgirl-button.fitgirl-pcgw-button img{height:20px;width:auto}
article .fitgirl-button.fitgirl-jd-button{background-color:#0a6ab6;color:#ffffff;font-family:sans-serif;font-size:13px;font-weight:600}
article .fitgirl-button.fitgirl-jd-button:hover:not(.success):not(.error){background-color:#124700}
.fitgirl-settings-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0,0,0,.7);z-index:10000;display:flex;align-items:center;justify-content:center}
.fitgirl-settings-modal{background-color:#1e1e1e;border:1px solid #3e4446;border-radius:8px;padding:20px;max-width:500px;width:90%;color:white}
.fitgirl-settings-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:10px;border-bottom:1px solid #3e4446}
.fitgirl-settings-header h2{margin:0;font-size:18px}
.fitgirl-settings-close{background:none;border:none;color:#999;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;line-height:30px}
.fitgirl-settings-close:hover{color:white}
.fitgirl-settings-section{margin-bottom:20px}
.fitgirl-settings-section h3{margin:0 0 10px;font-size:14px;color:#999}
.fitgirl-settings-control{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
.fitgirl-settings-control label{font-size:13px}
.fitgirl-settings-slider{width:150px;height:4px;background:#3e4446;border-radius:2px;outline:none;-webkit-appearance:none}
.fitgirl-settings-slider::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:16px;height:16px;background:#67c1f5;cursor:pointer;border-radius:50%}
.fitgirl-settings-slider::-moz-range-thumb{width:16px;height:16px;background:#67c1f5;cursor:pointer;border-radius:50%;border:none}
.fitgirl-settings-checkbox{width:18px;height:18px;cursor:pointer}
.fitgirl-settings-value{font-size:13px;color:#67c1f5;min-width:40px;text-align:right}
.fitgirl-settings-buttons{display:flex;gap:10px;justify-content:flex-end;margin-top:20px;padding-top:20px;border-top:1px solid #3e4446}
.fitgirl-settings-button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:500}
.fitgirl-settings-button-primary{background-color:#0a6ab6;color:white}
.fitgirl-settings-button-primary:hover{background-color:#124700}
.fitgirl-settings-button-secondary{background-color:#3e4446;color:white}
.fitgirl-settings-button-secondary:hover{background-color:#52525e}
`);
function getCacheKey(postId) { return CONFIG.CACHE_KEY_PREFIX + postId; }
function getCachedData(postId) {
const data = localStorage.getItem(getCacheKey(postId));
return data ? JSON.parse(data) : null;
}
function setCachedData(postId, data) {
const key = getCacheKey(postId);
const keys = Object.keys(localStorage).filter(k => k.startsWith(CONFIG.CACHE_KEY_PREFIX));
if (keys.length >= CONFIG.CACHE_MAX_ENTRIES) {
localStorage.removeItem(keys.sort()[0]);
}
localStorage.setItem(key, JSON.stringify(data));
}
let userSettings = null;
function loadUserSettings() {
const stored = GM_getValue('user_settings', null);
if (stored) {
const parsed = JSON.parse(stored);
userSettings = {
carousel_speed: parsed.carousel_speed ?? CONFIG.DEFAULT_SETTINGS.carousel_speed,
use_native_notifications: parsed.use_native_notifications ?? CONFIG.DEFAULT_SETTINGS.use_native_notifications,
buttons_visible: {
steam: parsed.buttons_visible?.steam ?? CONFIG.DEFAULT_SETTINGS.buttons_visible.steam,
steam_search: parsed.buttons_visible?.steam_search ?? CONFIG.DEFAULT_SETTINGS.buttons_visible.steam_search,
steamgriddb: parsed.buttons_visible?.steamgriddb ?? CONFIG.DEFAULT_SETTINGS.buttons_visible.steamgriddb,
pcgw: parsed.buttons_visible?.pcgw ?? CONFIG.DEFAULT_SETTINGS.buttons_visible.pcgw,
fuckingfast: parsed.buttons_visible?.fuckingfast ?? CONFIG.DEFAULT_SETTINGS.buttons_visible.fuckingfast,
jdownloader: parsed.buttons_visible?.jdownloader ?? CONFIG.DEFAULT_SETTINGS.buttons_visible.jdownloader
}
};
saveUserSettings(userSettings);
} else {
userSettings = JSON.parse(JSON.stringify(CONFIG.DEFAULT_SETTINGS));
}
return userSettings;
}
function saveUserSettings(settings) {
userSettings = settings;
GM_setValue('user_settings', JSON.stringify(settings));
}
function openSettingsModal() {
const overlay = document.createElement('div');
overlay.className = 'fitgirl-settings-overlay';
overlay.innerHTML = `
<div class="fitgirl-settings-modal">
<div class="fitgirl-settings-header">
<h2>FitGirl Enhanced Settings</h2>
<button class="fitgirl-settings-close">×</button>
</div>
<div class="fitgirl-settings-section">
<h3>CAROUSEL</h3>
<div class="fitgirl-settings-control">
<label>Scroll Speed</label>
<div style="display:flex;align-items:center;gap:10px;">
<input type="range" class="fitgirl-settings-slider" id="carousel-speed" min="10" max="100" value="${userSettings.carousel_speed}">
<span class="fitgirl-settings-value" id="carousel-speed-value">${userSettings.carousel_speed} px/s</span>
</div>
</div>
</div>
<div class="fitgirl-settings-section">
<h3>NOTIFICATIONS</h3>
<div class="fitgirl-settings-control">
<label>Use Native Browser Notifications</label>
<input type="checkbox" class="fitgirl-settings-checkbox" id="native-notifications" ${userSettings.use_native_notifications ? 'checked' : ''}>
</div>
</div>
<div class="fitgirl-settings-section">
<h3>BUTTON VISIBILITY</h3>
<div class="fitgirl-settings-control"><label>Steam Store Button</label><input type="checkbox" class="fitgirl-settings-checkbox" id="btn-steam" ${userSettings.buttons_visible.steam ? 'checked' : ''}></div>
<div class="fitgirl-settings-control"><label>Steam Search Button</label><input type="checkbox" class="fitgirl-settings-checkbox" id="btn-steam-search" ${userSettings.buttons_visible.steam_search ? 'checked' : ''}></div>
<div class="fitgirl-settings-control"><label>SteamGridDB Button</label><input type="checkbox" class="fitgirl-settings-checkbox" id="btn-steamgriddb" ${userSettings.buttons_visible.steamgriddb ? 'checked' : ''}></div>
<div class="fitgirl-settings-control"><label>PCGamingWiki Button</label><input type="checkbox" class="fitgirl-settings-checkbox" id="btn-pcgw" ${userSettings.buttons_visible.pcgw ? 'checked' : ''}></div>
<div class="fitgirl-settings-control"><label>FuckingFast Button</label><input type="checkbox" class="fitgirl-settings-checkbox" id="btn-fuckingfast" ${userSettings.buttons_visible.fuckingfast ? 'checked' : ''}></div>
<div class="fitgirl-settings-control"><label>JDownloader Button</label><input type="checkbox" class="fitgirl-settings-checkbox" id="btn-jdownloader" ${userSettings.buttons_visible.jdownloader ? 'checked' : ''}></div>
</div>
<div class="fitgirl-settings-buttons">
<button class="fitgirl-settings-button fitgirl-settings-button-secondary" id="settings-cancel">Cancel</button>
<button class="fitgirl-settings-button fitgirl-settings-button-primary" id="settings-save">Save</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const speedSlider = overlay.querySelector('#carousel-speed');
const speedValue = overlay.querySelector('#carousel-speed-value');
speedSlider.addEventListener('input', () => { speedValue.textContent = `${speedSlider.value} px/s`; });
const closeModal = () => { document.body.removeChild(overlay); };
overlay.querySelector('.fitgirl-settings-close').addEventListener('click', closeModal);
overlay.querySelector('#settings-cancel').addEventListener('click', closeModal);
overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); });
overlay.querySelector('#settings-save').addEventListener('click', () => {
const newSettings = {
carousel_speed: parseInt(speedSlider.value),
use_native_notifications: overlay.querySelector('#native-notifications').checked,
buttons_visible: {
steam: overlay.querySelector('#btn-steam').checked,
steam_search: overlay.querySelector('#btn-steam-search').checked,
steamgriddb: overlay.querySelector('#btn-steamgriddb').checked,
pcgw: overlay.querySelector('#btn-pcgw').checked,
fuckingfast: overlay.querySelector('#btn-fuckingfast').checked,
jdownloader: overlay.querySelector('#btn-jdownloader').checked
}
};
saveUserSettings(newSettings);
showNotification('Settings saved! Refresh the page to apply changes.', '#339966');
closeModal();
});
}
function extractSteamId(postElement) {
// V- TRY VIDEO/IMG SOURCES FIRST -V
const videoSources = postElement.querySelectorAll('video source[src*="steam"]');
for (const source of videoSources) {
const match = source.src.match(/\/apps\/(\d+)\//);
if (match) {
console.log('FitGirl Enhanced: Found Steam ID', match[1], 'from video source');
return match[1];
}
}
const imgSources = postElement.querySelectorAll('img[src*="steam"]');
for (const img of imgSources) {
const match = img.src.match(/\/apps\/(\d+)\//);
if (match) {
console.log('FitGirl Enhanced: Found Steam ID', match[1], 'from img source');
return match[1];
}
}
// V- FALLBACK TO HTML SEARCH -V
const patterns = [
/steam\/apps\/(\d+)\//,
/store_trailers\/(\d+)\//,
/store_item_assets\/steam\/apps\/(\d+)\//,
/steamstatic\.com\/.*?\/apps\/(\d+)\//,
/steampowered\.com\/app\/(\d+)\//
];
for (const pattern of patterns) {
const match = postElement.innerHTML.match(pattern);
if (match) {
console.log('FitGirl Enhanced: Found Steam ID', match[1], 'with pattern', pattern);
return match[1];
}
}
console.log('FitGirl Enhanced: No Steam ID found for post', postElement.id);
return null;
}
function extractGameName(postElement) {
const strongElement = postElement.querySelector('.entry-content h3 strong');
if (!strongElement) return null;
const fullText = strongElement.textContent;
const graySpan = strongElement.querySelector('span[style*="128, 128, 128"]');
return graySpan ? fullText.replace(graySpan.textContent, '').trim() : fullText.trim();
}
function getPostData(postElement) {
const postId = postElement.id;
let postData = getCachedData(postId);
if (!postData) {
postData = { steam_id: extractSteamId(postElement), game_name: extractGameName(postElement) };
setCachedData(postId, postData);
}
return postData;
}
function createButtonBar() {
const posts = document.querySelectorAll('article[id^="post-"]');
posts.forEach(post => {
if (post.querySelector('.fitgirl-button-bar-wrapper') || !isGamePost(post)) return;
const titleElement = post.querySelector('h1.entry-title');
if (!titleElement) return;
const postData = getPostData(post);
const wrapper = document.createElement('div');
wrapper.className = 'fitgirl-button-bar-wrapper';
const buttonBar = document.createElement('div');
buttonBar.className = 'fitgirl-button-bar';
wrapper.appendChild(buttonBar);
titleElement.insertAdjacentElement('afterend', wrapper);
const buttonsData = addAllButtons(buttonBar, postData, post);
setupCarousel(wrapper, buttonBar, buttonsData);
});
}
function isGamePost(post) {
const categoryLink = post.querySelector('.cat-links a');
const entryMetaLinks = post.querySelectorAll('.entry-meta a[rel="category tag"]');
const downloadSection = post.querySelector('.entry-content h3');
const hasRepackCategory = (
(categoryLink && (categoryLink.textContent.includes('Lossless Repack') ||
categoryLink.href.includes('/lossless-repack/') ||
categoryLink.textContent.includes('Repack'))) ||
Array.from(entryMetaLinks).some(link =>
link.textContent.includes('Lossless Repack') ||
link.href.includes('/lossless-repack/') ||
link.textContent.includes('Repack'))
);
const hasDownloadSection = downloadSection && downloadSection.textContent.includes('Download Mirrors');
return hasRepackCategory || hasDownloadSection;
}
function addAllButtons(buttonBar, postData, postElement) {
const buttonsData = [];
if (userSettings.buttons_visible.steam) {
buttonsData.push({ type: 'steam', element: addSteamButton(buttonBar, postData) });
}
if (userSettings.buttons_visible.steam_search) {
buttonsData.push({ type: 'steam_search', element: addSteamSearchButton(buttonBar, postData) });
}
if (userSettings.buttons_visible.steamgriddb) {
buttonsData.push({ type: 'steamgriddb', element: addSteamgriddbButton(buttonBar, postData) });
}
if (userSettings.buttons_visible.pcgw) {
buttonsData.push({ type: 'pcgw', element: addPcgwButton(buttonBar, postData) });
}
if (userSettings.buttons_visible.fuckingfast) {
buttonsData.push({ type: 'fuckingfast', element: addFuckingfastButton(buttonBar, postElement) });
}
if (userSettings.buttons_visible.jdownloader) {
buttonsData.push({ type: 'jdownloader', element: addJdownloaderButton(buttonBar, postElement) });
}
return buttonsData;
}
const clonedListeners = new WeakMap();
function setupCarousel(wrapper, buttonBar, buttonsData) {
setTimeout(() => {
if (buttonsData.length < CONFIG.CAROUSEL_MIN_BUTTONS) return;
cloneButtonsForCarousel(buttonBar, buttonsData);
cloneButtonsForCarousel(buttonBar, buttonsData);
initJsCarousel(wrapper, buttonBar);
}, 0);
}
function cloneButtonsForCarousel(buttonBar, buttonsData) {
buttonsData.forEach(btnData => {
const cloned = btnData.element.cloneNode(true);
const originalListeners = btnData.element.__listeners;
if (originalListeners) {
clonedListeners.set(cloned, originalListeners);
cloned.addEventListener('click', originalListeners.click);
}
buttonBar.appendChild(cloned);
});
}
function initJsCarousel(wrapper, buttonBar) {
const totalWidth = buttonBar.scrollWidth;
const thirdWidth = totalWidth / 3;
const speedPerFrame = userSettings.carousel_speed / 60;
let currentPos = -0.01;
let isPaused = false;
function wrapPosition(pos) {
if (pos <= -thirdWidth) pos += thirdWidth;
else if (pos >= 0) pos -= thirdWidth;
return pos;
}
wrapper.addEventListener('mouseenter', () => { isPaused = true; });
wrapper.addEventListener('mouseleave', () => { isPaused = false; });
let supportsPassive = false;
try {
const opts = Object.defineProperty({}, 'passive', { get: function() { supportsPassive = true; } });
window.addEventListener('test', null, opts);
window.removeEventListener('test', null, opts);
} catch (e) {}
wrapper.addEventListener('wheel', e => {
e.preventDefault();
currentPos -= e.deltaY;
currentPos = wrapPosition(currentPos);
buttonBar.style.transform = `translateX(${currentPos}px)`;
}, supportsPassive ? { passive: false } : false);
function animate() {
if (!isPaused) currentPos -= speedPerFrame;
currentPos = wrapPosition(currentPos);
buttonBar.style.transform = `translateX(${currentPos}px)`;
requestAnimationFrame(animate);
}
animate();
}
function addButton(barElement, options) {
const button = document.createElement('button');
button.className = `fitgirl-button ${options.class}`;
if (options.unavailable) {
button.classList.add('unavailable');
button.title = options.unavailable_reason || 'Not available for this post';
} else if (options.tooltip) {
button.title = options.tooltip;
}
if (options.icon) {
const icon = document.createElement('img');
icon.src = options.icon;
icon.className = 'fitgirl-icon';
icon.alt = '';
button.appendChild(icon);
}
if (options.text) {
const text = document.createElement('span');
text.textContent = options.text;
button.appendChild(text);
}
const clickHandler = async () => {
if (options.unavailable) {
showNotification(options.unavailable_reason || 'Not available', '#ff3333');
return;
}
const originalClass = button.className;
try {
await options.action();
button.className = `fitgirl-button ${options.class} success`;
setTimeout(() => { button.className = originalClass; }, CONFIG.BUTTON_FLASH_DURATION);
} catch (error) {
console.error(error);
button.className = `fitgirl-button ${options.class} error`;
showNotification(error.message || 'Operation failed', '#ff3333');
setTimeout(() => { button.className = originalClass; }, CONFIG.BUTTON_FLASH_DURATION);
}
};
button.addEventListener('click', clickHandler);
button.__listeners = { click: clickHandler };
barElement.appendChild(button);
return button;
}
function addSteamButton(barElement, postData) {
const unavailable = !postData.steam_id;
const steamUrl = postData.steam_id ? `https://store.steampowered.com/app/${postData.steam_id}/` : null;
return addButton(barElement, {
text: 'Steam',
icon: 'https://store.steampowered.com/favicon.ico',
class: 'fitgirl-steam-button',
tooltip: 'Open on Steam Store',
unavailable: unavailable,
unavailable_reason: 'Steam ID not found for this post',
action: async () => { GM_openInTab(steamUrl, { active: true }); }
});
}
function addSteamSearchButton(barElement, postData) {
const unavailable = !postData.game_name;
const searchUrl = postData.game_name ? `https://store.steampowered.com/search?term=${encodeURIComponent(postData.game_name)}` : null;
const button = addButton(barElement, {
class: 'fitgirl-steam-search-button',
tooltip: 'Search Steam Store by title',
unavailable: unavailable,
unavailable_reason: 'Game name not found for this post',
action: async () => { GM_openInTab(searchUrl, { active: true }); }
});
button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none" aria-label="Search"><path fill="currentColor" d="M13.8296 12.0786C14.8347 10.6321 15.2623 8.86133 15.0284 7.11496C14.7945 5.36859 13.9159 3.77313 12.5656 2.64269C11.2153 1.51224 9.49114 0.928708 7.73254 1.00696C5.97394 1.08522 4.30831 1.8196 3.06357 3.06552C1.81882 4.31144 1.08514 5.97864 1.00696 7.7389.928776 9.49916 1.51176 11.2249 2.64114 12.5765C3.77052 13.9281 5.36446 14.8075 7.10919 15.0417 8.85391 15.2758 10.623 14.8477 12.0682 13.8417L15.2185 17 15.3997 16.8187 16.8188 15.3982 17 15.2168 13.8296 12.0786ZM8.04222 12.5824C7.14643 12.5824 6.27075 12.3165 5.52593 11.8183 4.7811 11.3202 4.20058 10.6122 3.85777 9.78376 3.51497 8.95538 3.42528 8.04384 3.60004 7.16443 3.7748 6.28502 4.20616 5.47723 4.83958 4.84321 5.47301 4.20919 6.28004 3.77742 7.15862 3.60249 8.0372 3.42757 8.94787 3.51734 9.77548 3.86047 10.6031 4.2036 11.3104 4.78467 11.8081 5.5302 12.3058 6.27573 12.5714 7.15223 12.5714 8.04887 12.5714 9.25123 12.0943 10.4043 11.2449 11.2545 10.3955 12.1047 9.24344 12.5824 8.04222 12.5824V12.5824Z"></path></svg>`;
return button;
}
function addSteamgriddbButton(barElement, postData) {
const unavailable = !postData.game_name;
const searchUrl = postData.game_name ? `https://www.steamgriddb.com/search/grids?term=${encodeURIComponent(postData.game_name)}` : null;
return addButton(barElement, {
text: 'SteamGridDB',
icon: 'https://www.steamgriddb.com/static/img/logo-512.png',
class: 'fitgirl-steamgriddb-button',
tooltip: 'Search SteamGridDB for artwork',
unavailable: unavailable,
unavailable_reason: 'Game name not found for this post',
action: async () => { GM_openInTab(searchUrl, { active: true }); }
});
}
function addPcgwButton(barElement, postData) {
const unavailable = !postData.steam_id;
const pcgwUrl = postData.steam_id ? `https://pcgamingwiki.com/api/appid.php?appid=${postData.steam_id}` : null;
return addButton(barElement, {
icon: 'https://pcgamingwiki.ams3.digitaloceanspaces.com/6/61/PCGamingWiki_wide_white.svg',
class: 'fitgirl-pcgw-button',
tooltip: 'Open on PCGamingWiki',
unavailable: unavailable,
unavailable_reason: 'Steam ID not found for this post',
action: async () => { GM_openInTab(pcgwUrl, { active: true }); }
});
}
function addFuckingfastButton(barElement, postElement) {
return addButton(barElement, {
text: 'FUCKINGFAST',
icon: 'https://fuckingfast.co/static/favicon.ico',
class: 'fuckingfast',
action: () => copyFuckingfastLinks(postElement)
});
}
async function copyFuckingfastLinks(postElement) {
const links = getFuckingfastLinks(postElement);
if (!links.length) throw new Error('No FuckingFast links found');
await GM_setClipboard(links.join('\n'));
showNotification(`${links.length} FuckingFast links copied!`, '#339966');
}
function getFuckingfastLinks(postElement) {
return Array.from(postElement.querySelectorAll('a'))
.map(a => a.href)
.filter(h => h.startsWith('https://fuckingfast.co/'));
}
function addJdownloaderButton(barElement, postElement) {
return addButton(barElement, {
text: "Click'n'Load",
icon: 'http://jdownloader.org/lib/tpl/arctic/images/favicon.ico',
class: 'fitgirl-jd-button',
tooltip: 'Send to JDownloader',
action: () => sendToJdownloader(postElement)
});
}
function sendToJdownloader(postElement) {
return new Promise((resolve, reject) => {
const links = getFuckingfastLinks(postElement);
if (!links.length) {
reject(new Error('No FuckingFast links found'));
return;
}
const urls = links.join('\r\n');
const source = window.location.href;
const params = new URLSearchParams();
params.append('urls', urls);
params.append('source', source);
GM_xmlhttpRequest({
method: 'POST',
url: 'http://127.0.0.1:9666/flash/add',
data: params.toString(),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
showNotification(`${links.length} links sent to JDownloader!`, '#339966');
resolve();
} else {
reject(new Error(`JDownloader returned error ${response.status}`));
}
},
onerror: function(error) {
reject(new Error('Could not connect to JDownloader. Is it running?'));
},
ontimeout: function() {
reject(new Error('JDownloader connection timed out'));
}
});
});
}
function showNotification(message, color) {
if (userSettings.use_native_notifications && typeof GM_notification !== 'undefined') {
GM_notification({
text: message,
title: 'FitGirl Enhanced',
silent: false,
timeout: CONFIG.NOTIFICATION_DURATION
});
} else {
showOverlayNotification(message, color);
}
}
function showOverlayNotification(message, color) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `position:fixed;top:20px;right:20px;background-color:${color};color:white;padding:10px 15px;border-radius:4px;z-index:10000`;
document.body.appendChild(notification);
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, CONFIG.NOTIFICATION_DURATION);
}
let observer = null;
try { loadUserSettings(); } catch (error) {
console.error('FitGirl Enhanced: Error loading settings, using defaults', error);
userSettings = JSON.parse(JSON.stringify(CONFIG.DEFAULT_SETTINGS));
}
try { GM_registerMenuCommand('Settings', openSettingsModal, { title: 'Configure FitGirl Enhanced settings' }); } catch (error) {
console.error('FitGirl Enhanced: Error registering menu command', error);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createButtonBar);
} else {
createButtonBar();
}
observer = new MutationObserver(debounce(() => { createButtonBar(); }, CONFIG.OBSERVER_DEBOUNCE));
observer.observe(document.body, { childList: true, subtree: true });
window.addEventListener('beforeunload', () => {
if (observer) { observer.disconnect(); observer = null; }
});
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => { clearTimeout(timeout); func(...args); };
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
})();