// ==UserScript==
// @name DataAnnotation Power-Up v1
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Includes multi-currency support, togglable filters, a project grabber, and a horrific notification system.
// @author adonno55
// @match https://app.dataannotation.tech/workers/payments*
// @match https://app.dataannotation.tech/workers/projects*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @connect api.frankfurter.app
// @connect self
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_PREFIX = "[DataAnnotation Power-Up v1]";
const ENABLE_DEBUG_LOGGING = true;
const CURRENCY_SYMBOLS = { USD: '$', EUR: '€', GBP: '£', JPY: '¥', CAD: 'C$', AUD: 'A$', CHF: 'CHF', CNY: '¥', INR: '₹' };
const Config = {
defaults: {
payThreshold: 25,
nameFilters: ['chatbot', 'ai', 'model', 'llm'],
checkInterval: 1,
enableSound: true,
enableCurrencyConversion: true,
targetCurrency: 'GBP',
includePayPalFee: true,
paypalFeePercentage: 3.0,
enablePayFilter: true,
enableNameFilter: true,
notifyForHighPay: true,
notifyForNameMatch: true,
notifyForOtherNew: true,
// Project Grabber Settings
enableProjectGrabber: false, // Defaulted to off for user safety
grabHighPay: true,
grabNameMatch: true,
grabberDelay: 2, // Delay in seconds between opening tabs
grabberMaxTabs: 1 // Safety limit: max tabs to open in one check cycle
},
settings: {},
async load() {
const loadedSettings = await GM_getValue('powerUpConfig_v10', {});
if (typeof loadedSettings.enableUnseenAlerts !== 'undefined') {
loadedSettings.notifyForOtherNew = loadedSettings.enableUnseenAlerts;
delete loadedSettings.enableUnseenAlerts;
}
this.settings = { ...this.defaults, ...loadedSettings };
if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Config] Settings loaded:`, this.settings);
},
async save() {
await GM_setValue('powerUpConfig_v10', this.settings);
if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Config] Settings saved:`, this.settings);
}
};
const SettingsGUI = {
init() { this._addStyles(); this._createSettingsButton(); },
_createSettingsButton() { const btn = document.createElement('button'); btn.id = 'powerup-settings-btn'; btn.textContent = '⚙️'; btn.title = 'Open Power-Up Settings'; btn.onclick = () => this._togglePanel(); document.body.appendChild(btn); },
_togglePanel() { let panel = document.getElementById('powerup-settings-panel'); if (panel) { panel.remove(); } else { this._createPanel(); } },
_createPanel() {
const panel = document.createElement('div'); panel.id = 'powerup-settings-panel';
panel.innerHTML = `
<div class="powerup-header"><h3>DA Power-Up Settings</h3><button class="powerup-close-btn">×</button></div>
<div class="powerup-content">
<h4>🔔 Notification Filters</h4>
<div class="powerup-setting-checkbox"><input type="checkbox" id="enablePayFilter" ${Config.settings.enablePayFilter ? 'checked' : ''}><label for="enablePayFilter">Enable High-Pay Filter</label></div>
<div class="powerup-setting ${!Config.settings.enablePayFilter ? 'disabled' : ''}"><label for="payThreshold">High-Pay Threshold ($/hr)</label><input type="number" id="payThreshold" min="0" value="${Config.settings.payThreshold}" ${!Config.settings.enablePayFilter ? 'disabled' : ''}></div>
<div class="powerup-setting-checkbox"><input type="checkbox" id="enableNameFilter" ${Config.settings.enableNameFilter ? 'checked' : ''}><label for="enableNameFilter">Enable Name Filter</label></div>
<div class="powerup-setting ${!Config.settings.enableNameFilter ? 'disabled' : ''}"><label for="nameFilters">Name Filters (one per line)</label><textarea id="nameFilters" rows="4" ${!Config.settings.enableNameFilter ? 'disabled' : ''}>${Config.settings.nameFilters.join('\n')}</textarea></div>
<h4>🎯 Notification Triggers</h4>
<p style="margin-top: -10px; font-size: 0.9em; color: #ccc;">Choose which types of new projects should trigger a notification.</p>
<div class="powerup-setting-checkbox"><input type="checkbox" id="notifyForHighPay" ${Config.settings.notifyForHighPay ? 'checked' : ''}><label for="notifyForHighPay">Notify for High-Pay projects</label></div>
<div class="powerup-setting-checkbox"><input type="checkbox" id="notifyForNameMatch" ${Config.settings.notifyForNameMatch ? 'checked' : ''}><label for="notifyForNameMatch">Notify for Name-Filter matches</label></div>
<div class="powerup-setting-checkbox"><input type="checkbox" id="notifyForOtherNew" ${Config.settings.notifyForOtherNew ? 'checked' : ''}><label for="notifyForOtherNew">Notify for other new projects (general)</label></div>
<h4>🤖 Project Grabber</h4>
<p style="margin-top: -10px; font-size: 0.9em; color: #ccc;">Automatically open matching new projects in a new tab. Use with caution.</p>
<div class="powerup-setting-checkbox"><input type="checkbox" id="enableProjectGrabber" ${Config.settings.enableProjectGrabber ? 'checked' : ''}><label for="enableProjectGrabber">Enable Project Grabber</label></div>
<div id="grabber-options" class="${!Config.settings.enableProjectGrabber ? 'disabled' : ''}">
<p style="margin-bottom: 5px; margin-top: 10px; font-size: 0.9em; color: #ccc;">Grab projects that match:</p>
<div class="powerup-setting-checkbox"><input type="checkbox" id="grabHighPay" ${Config.settings.grabHighPay ? 'checked' : ''} ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}><label for="grabHighPay">High-Pay Filter</label></div>
<div class="powerup-setting-checkbox"><input type="checkbox" id="grabNameMatch" ${Config.settings.grabNameMatch ? 'checked' : ''} ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}><label for="grabNameMatch">Name Filter</label></div>
<div class="powerup-setting"><label for="grabberMaxTabs">Max Tabs per Check</label><input type="number" id="grabberMaxTabs" min="1" max="5" value="${Config.settings.grabberMaxTabs}" ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}></div>
<div class="powerup-setting"><label for="grabberDelay">Delay Between Grabs (seconds)</label><input type="number" id="grabberDelay" min="0" value="${Config.settings.grabberDelay}" ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}></div>
</div>
<h4>⚙️ General Settings</h4>
<div class="powerup-setting"><label for="checkInterval">Check Interval (minutes)</label><input type="number" id="checkInterval" min="1" value="${Config.settings.checkInterval}"></div>
<div class="powerup-setting-checkbox"><input type="checkbox" id="enableSound" ${Config.settings.enableSound ? 'checked' : ''}><label for="enableSound">Enable Sound Notifications</label></div>
<h4>💱 Currency Converter</h4>
<div class="powerup-setting-checkbox"><input type="checkbox" id="enableCurrencyConversion" ${Config.settings.enableCurrencyConversion ? 'checked' : ''}><label for="enableCurrencyConversion">Enable Currency Conversion</label></div>
<div class="powerup-setting ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}"><label for="targetCurrency">Target Currency</label><select id="targetCurrency" ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}>
<option value="GBP" ${Config.settings.targetCurrency === 'GBP' ? 'selected' : ''}>🇬🇧 British Pound (GBP)</option>
<option value="EUR" ${Config.settings.targetCurrency === 'EUR' ? 'selected' : ''}>🇪🇺 Euro (EUR)</option>
<option value="JPY" ${Config.settings.targetCurrency === 'JPY' ? 'selected' : ''}>🇯🇵 Japanese Yen (JPY)</option>
<option value="CAD" ${Config.settings.targetCurrency === 'CAD' ? 'selected' : ''}>🇨🇦 Canadian Dollar (CAD)</option>
<option value="AUD" ${Config.settings.targetCurrency === 'AUD' ? 'selected' : ''}>🇦🇺 Australian Dollar (AUD)</option>
<option value="INR" ${Config.settings.targetCurrency === 'INR' ? 'selected' : ''}>🇮🇳 Indian Rupee (INR)</option>
</select></div>
<div class="powerup-setting-checkbox ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}"><input type="checkbox" id="includePayPalFee" ${Config.settings.includePayPalFee ? 'checked' : ''} ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}><label for="includePayPalFee">Deduct estimated PayPal Fee (${Config.settings.paypalFeePercentage}%)</label></div>
</div>
<div class="powerup-footer"><button id="powerup-save-btn">Save & Apply</button><span id="powerup-save-confirm" style="display:none;">✅ Saved!</span></div>`;
document.body.appendChild(panel);
panel.querySelector('.powerup-close-btn').onclick = () => this._togglePanel();
document.getElementById('powerup-save-btn').onclick = () => this._saveSettings();
const setupToggle = (checkId, fieldIds, containerSelector) => {
const checkbox = document.getElementById(checkId);
const updateState = () => {
const isDisabled = !checkbox.checked;
fieldIds.forEach(id => document.getElementById(id).disabled = isDisabled);
document.getElementById(fieldIds[0]).closest(containerSelector).classList.toggle('disabled', isDisabled);
};
checkbox.onchange = updateState;
};
setupToggle('enablePayFilter', ['payThreshold'], '.powerup-setting');
setupToggle('enableNameFilter', ['nameFilters'], '.powerup-setting');
setupToggle('enableCurrencyConversion', ['targetCurrency', 'includePayPalFee'], '.powerup-setting');
const grabberCheckbox = document.getElementById('enableProjectGrabber');
grabberCheckbox.onchange = () => {
const isDisabled = !grabberCheckbox.checked;
document.getElementById('grabber-options').classList.toggle('disabled', isDisabled);
document.querySelectorAll('#grabber-options input, #grabber-options select').forEach(input => input.disabled = isDisabled);
};
},
async _saveSettings() {
Config.settings.payThreshold = parseFloat(document.getElementById('payThreshold').value);
Config.settings.nameFilters = document.getElementById('nameFilters').value.split('\n').map(f => f.trim()).filter(Boolean);
Config.settings.enablePayFilter = document.getElementById('enablePayFilter').checked;
Config.settings.enableNameFilter = document.getElementById('enableNameFilter').checked;
Config.settings.notifyForHighPay = document.getElementById('notifyForHighPay').checked;
Config.settings.notifyForNameMatch = document.getElementById('notifyForNameMatch').checked;
Config.settings.notifyForOtherNew = document.getElementById('notifyForOtherNew').checked;
Config.settings.enableProjectGrabber = document.getElementById('enableProjectGrabber').checked;
Config.settings.grabHighPay = document.getElementById('grabHighPay').checked;
Config.settings.grabNameMatch = document.getElementById('grabNameMatch').checked;
Config.settings.grabberMaxTabs = parseInt(document.getElementById('grabberMaxTabs').value, 10);
Config.settings.grabberDelay = parseFloat(document.getElementById('grabberDelay').value);
Config.settings.checkInterval = parseInt(document.getElementById('checkInterval').value, 10);
Config.settings.enableSound = document.getElementById('enableSound').checked;
Config.settings.enableCurrencyConversion = document.getElementById('enableCurrencyConversion').checked;
Config.settings.targetCurrency = document.getElementById('targetCurrency').value;
Config.settings.includePayPalFee = document.getElementById('includePayPalFee').checked;
await Config.save();
const confirmMsg = document.getElementById('powerup-save-confirm'); confirmMsg.style.display = 'inline';
setTimeout(() => { this._togglePanel(); alert('Settings saved. Page will reload to apply changes.'); window.location.reload(); }, 1000);
},
_addStyles() { GM_addStyle(`#powerup-settings-btn { position: fixed; bottom: 20px; left: 20px; width: 50px; height: 50px; background: linear-gradient(135deg, #6a0dad, #a020f0); color: white; border: none; border-radius: 50%; font-size: 24px; cursor: pointer; z-index: 10000; box-shadow: 0 4px 12px rgba(0,0,0,0.4); } #powerup-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; max-height: 80vh; overflow-y: auto; background: #2d2d2d; color: #f0f0f0; border: 1px solid #6a0dad; border-radius: 12px; z-index: 10001; box-shadow: 0 10px 40px rgba(0,0,0,0.8); font-family: -apple-system, BlinkMacSystemFont, sans-serif; } .powerup-header { padding: 15px 20px; background: #444; border-bottom: 1px solid #555; display: flex; justify-content: space-between; align-items: center; border-radius: 10px 10px 0 0; position: sticky; top: 0; z-index: 1; } .powerup-header h3 { margin: 0; } .powerup-close-btn { background: none; border: none; color: white; font-size: 24px; cursor: pointer; } .powerup-content { padding: 20px; display: flex; flex-direction: column; gap: 15px; } .powerup-content h4 { margin: 10px 0 5px 0; color: #a020f0; border-bottom: 1px solid #555; padding-bottom: 5px; } .powerup-setting, .powerup-setting-checkbox, #grabber-options { transition: opacity 0.3s; } .powerup-setting.disabled, .powerup-setting-checkbox.disabled, #grabber-options.disabled { opacity: 0.5; } .powerup-setting label, .powerup-setting-checkbox label { margin-bottom: 5px; font-weight: bold; } .powerup-setting input, .powerup-setting textarea, .powerup-setting select { background: #222; border: 1px solid #666; color: white; padding: 10px; border-radius: 4px; width: 100%; box-sizing: border-box; } .powerup-setting-checkbox { display: flex; align-items: center; gap: 10px; } .powerup-footer { padding: 15px 20px; background: #444; border-top: 1px solid #555; text-align: right; border-radius: 0 0 10px 10px; position: sticky; bottom: 0; z-index: 1;} #powerup-save-btn { background: #6a0dad; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } #powerup-save-confirm { margin-left: 10px; color: lightgreen; }`); }
};
const Notifier = {
NOTIFICATION_SOUND_URL: 'https://www.myinstants.com/media/sounds/hell_AJWSn3e.mp3', _originalTitle: document.title,
send(project, reasons) {
if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Notifier] Sending notification for '${project.name}' for reasons:`, reasons);
const { title, text, highlight } = this._formatMessage(project, reasons);
GM_notification({ title, text, highlight, silent: false, timeout: 30000, onclick: () => window.focus() });
this._showBanner(title);
if (Config.settings.enableSound) { const audio = new Audio(this.NOTIFICATION_SOUND_URL); audio.play().catch(e => console.warn(`${SCRIPT_PREFIX} Audio notification was blocked.`)); }
},
_formatMessage(project, reasons) {
let title = "✨ New Project Available!"; let text = `${project.name} | $${project.pay.toFixed(2)}/hr | ${project.tasks} tasks`; let highlight = true;
const isHighPay = reasons.includes('highPay'); const isNameMatch = reasons.includes('nameMatch');
if (isHighPay && isNameMatch) { title = `💸🎯 High-Pay & Filter Match!`; }
else if (isHighPay) { title = `💸 High-Pay Project Found!`; }
else if (isNameMatch) { title = `🎯 Filter Match Project Found!`; }
return { title, text, highlight };
},
_showBanner(title) {
document.title = `(${title.slice(0, 1)}) New Project! | ${this._originalTitle}`;
let banner = document.getElementById('new-project-notifier-banner');
if (!banner) { banner = document.createElement('div'); banner.id = 'new-project-notifier-banner'; document.documentElement.appendChild(banner); }
banner.innerHTML = `<span>${title} Refresh the page to see the new project.</span><button title="Dismiss">×</button>`;
banner.querySelector('button').onclick = () => { banner.remove(); document.title = this._originalTitle; };
}
};
const ProjectTracker = {
_tabId: Date.now() + Math.random(), _isLeader: false, _isChecking: false, _checkTimeoutId: null,
async _updateLeaderTimestamp() {
if (this._isLeader) { const now = Date.now(); await GM_setValue('leaderInfo', { id: this._tabId, timestamp: now }); if (ENABLE_DEBUG_LOGGING) console.log(`%c[LEADERSHIP] Heartbeat updated at ${new Date(now).toLocaleTimeString()}`, 'color: cyan;'); }
},
_getProjectsFromDoc(doc) {
const projectsMap = new Map(); doc.querySelectorAll('tbody tr').forEach(row => { try { const cells = row.querySelectorAll('td'); if (cells.length < 4) return; const linkElement = cells[0].querySelector('a'); if (!linkElement) return; const projectId = new URL(linkElement.href, doc.baseURI).searchParams.get('project_id'); const name = linkElement.textContent.trim(); const pay = parseFloat(cells[1].textContent.trim().replace(/[^0-9.-]+/g, "")); const tasks = parseInt(cells[2].textContent.trim().replace(/,/g, ''), 10); const url = linkElement.href; if (projectId && name) { projectsMap.set(projectId, { id: projectId, name, pay: isNaN(pay) ? 0 : pay, tasks: isNaN(tasks) ? 0 : tasks, url }); } } catch (e) { console.error(`${SCRIPT_PREFIX} Error parsing a project row:`, e, row); } }); return projectsMap;
},
_fetchProjectsWithIframe() {
return new Promise((resolve, reject) => { const iframe = document.createElement('iframe'); iframe.id = 'powerup-fetch-iframe'; iframe.style.display = 'none'; const timeout = setTimeout(() => { cleanup(); reject(new Error('Iframe load timed out.')); }, 30000); const cleanup = () => { clearTimeout(timeout); const el = document.getElementById('powerup-fetch-iframe'); if (el) el.remove(); }; iframe.onload = () => { try { const projects = this._getProjectsFromDoc(iframe.contentDocument); resolve(projects); } catch (e) { reject(e); } finally { cleanup(); } }; iframe.onerror = () => { cleanup(); reject(new Error('Iframe failed to load.')); }; iframe.src = window.location.href; document.body.appendChild(iframe); });
},
async _checkForNewProjects() {
if (this._isChecking) return;
this._isChecking = true;
if (ENABLE_DEBUG_LOGGING) console.groupCollapsed(`${SCRIPT_PREFIX} [Tracker] Starting Project Check @ ${new Date().toLocaleTimeString()}`);
try {
await this._updateLeaderTimestamp();
const currentProjectsMap = await this._fetchProjectsWithIframe();
const currentProjectIds = Array.from(currentProjectsMap.keys());
const previousProjectIds = await GM_getValue('powerUpLastSeenProjects_v10', []);
const historicalDB = await GM_getValue('slimProjectDatabase_v10', {});
const previousProjectIdsSet = new Set(previousProjectIds);
let historicalDbWasModified = false;
const projectsToGrab = [];
if (ENABLE_DEBUG_LOGGING) {
console.log("Projects now:", currentProjectIds);
console.log("Projects last check:", previousProjectIds);
}
for (const [projectId, projectData] of currentProjectsMap.entries()) {
if (!previousProjectIdsSet.has(projectId)) {
if (ENABLE_DEBUG_LOGGING) console.log(`%c[NEW THIS CYCLE]`, 'color: lightgreen; font-weight: bold;', projectData);
const triggeringReasons = [];
const lowerCaseName = projectData.name.toLowerCase();
const isHighPay = Config.settings.enablePayFilter && projectData.pay >= Config.settings.payThreshold;
const isNameMatch = Config.settings.enableNameFilter && Config.settings.nameFilters.some(filter => lowerCaseName.includes(filter.toLowerCase()));
if (isHighPay && Config.settings.notifyForHighPay) { triggeringReasons.push('highPay'); }
if (isNameMatch && Config.settings.notifyForNameMatch) { triggeringReasons.push('nameMatch'); }
if (triggeringReasons.length > 0) { Notifier.send(projectData, triggeringReasons); }
else if (Config.settings.notifyForOtherNew) { Notifier.send(projectData, ['unseen']); }
if (Config.settings.enableProjectGrabber) {
const shouldGrabHighPay = isHighPay && Config.settings.grabHighPay;
const shouldGrabNameMatch = isNameMatch && Config.settings.grabNameMatch;
if (shouldGrabHighPay || shouldGrabNameMatch) {
projectsToGrab.push(projectData);
}
}
}
if (!historicalDB[projectId]) {
historicalDB[projectId] = { id: projectData.id, name: projectData.name, pay: projectData.pay };
historicalDbWasModified = true;
if (ENABLE_DEBUG_LOGGING) console.log(`%c[NEW TO HISTORY] Added '${projectData.name}' to the historical database.`, 'color: cyan;');
}
}
if (projectsToGrab.length > 0) {
if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Grabber] Found ${projectsToGrab.length} project(s) matching grab criteria.`);
const projectsToActuallyOpen = projectsToGrab.slice(0, Config.settings.grabberMaxTabs);
if (projectsToActuallyOpen.length < projectsToGrab.length) {
console.warn(`${SCRIPT_PREFIX} [Grabber] Max tabs limit (${Config.settings.grabberMaxTabs}) reached. Not opening all ${projectsToGrab.length} matched projects.`);
}
projectsToActuallyOpen.forEach((project, index) => {
const delay = index * Config.settings.grabberDelay * 1000;
setTimeout(() => {
console.log(`%c[GRABBING] Opening '${project.name}' in new tab...`, 'color: magenta; font-weight: bold;');
GM_openInTab(project.url, { active: true, setParent: true });
}, delay);
});
}
if (historicalDbWasModified) {
await GM_setValue('slimProjectDatabase_v10', historicalDB);
}
await GM_setValue('powerUpLastSeenProjects_v10', currentProjectIds);
} catch (error) {
console.error(`${SCRIPT_PREFIX} [Tracker] A critical error occurred during the project check:`, error);
} finally {
this._isChecking = false;
if (ENABLE_DEBUG_LOGGING) console.groupEnd();
}
},
async _electLeader() {
const leaderInfo = await GM_getValue('leaderInfo', {}); const now = Date.now(); let becameLeader = false; if (!leaderInfo.id || (now - leaderInfo.timestamp) > Config.settings.checkInterval * 60 * 1000 * 1.5) { this._isLeader = true; becameLeader = true; await this._updateLeaderTimestamp(); } else { this._isLeader = (leaderInfo.id === this._tabId); } if (ENABLE_DEBUG_LOGGING) { console.log(`%c[LEADERSHIP CHECK] This tab is ${this._isLeader ? 'the leader' : 'a follower'}. ${becameLeader ? '(Newly Elected)' : ''}`, 'color: orange;'); }
},
async init() {
if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Tracker] Initializing...`);
GM_addStyle(`#new-project-notifier-banner { position: fixed; top: 0; left: 0; width: 100%; background: linear-gradient(45deg, #6a0dad, #a020f0); color: white; text-align: center; padding: 15px; font-size: 1.2em; z-index: 9999; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 8px rgba(0,0,0,0.3); } #new-project-notifier-banner button { background:none; border:none; color:white; font-size:2em; cursor:pointer; margin-left:20px; } }`);
window.addEventListener('unload', async () => { if (this._isLeader) { if (ENABLE_DEBUG_LOGGING) console.log(`%c[LEADERSHIP] Leader tab closing. Abdicating leadership.`, 'color: red;'); await GM_setValue('leaderInfo', {}); } });
await this._electLeader();
if (this._isLeader) { if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Tracker] This is the leader tab. Performing an immediate initial check.`); this._checkForNewProjects(); }
const scheduleNextCheck = () => { clearTimeout(this._checkTimeoutId); this._checkTimeoutId = setTimeout(() => { if (this._isLeader) { this._checkForNewProjects(); } scheduleNextCheck(); }, Config.settings.checkInterval * 60 * 1000); };
setInterval(() => this._electLeader(), 5000);
scheduleNextCheck();
if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Tracker] Project tracker active. Checks every ${Config.settings.checkInterval} minutes.`);
}
};
const CurrencyConverter = {
FALLBACK_RATES: { GBP: 0.79, EUR: 0.92, JPY: 150.0, CAD: 1.35, AUD: 1.50, INR: 83.0 },
_fetchRate(callback) {
const target = Config.settings.targetCurrency;
const fallback = this.FALLBACK_RATES[target] || 1.0;
GM_xmlhttpRequest({
method: "GET", url: `https://api.frankfurter.app/latest?from=USD&to=${target}`,
onload: (res) => { try { callback(JSON.parse(res.responseText)?.rates?.[target] || fallback); } catch (e) { callback(fallback); } },
onerror: () => callback(fallback)
});
},
_convertPrices(rootNode, effectiveRate) {
const usdRegex = /\$(-?[\d,]+(?:\.\d{2})?)/g;
const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const p = node.parentElement;
if (!p || p.closest('[data-usd-converted]') || ['SCRIPT', 'STYLE'].includes(p.tagName)) continue;
if (node.nodeValue.includes('$')) {
const newText = node.nodeValue.replace(usdRegex, (_, numStr) => {
const symbol = CURRENCY_SYMBOLS[Config.settings.targetCurrency] || Config.settings.targetCurrency;
const value = parseFloat(numStr.replace(/,/g, '')) * effectiveRate;
const decimalPlaces = ['JPY'].includes(Config.settings.targetCurrency) ? 0 : 2;
return `${symbol}${value.toFixed(decimalPlaces)}`;
});
if (newText !== node.nodeValue) {
node.nodeValue = newText;
p.dataset.usdConverted = 'true';
}
}
}
},
init() {
if (!Config.settings.enableCurrencyConversion) return;
this._fetchRate(marketRate => {
const feeMultiplier = Config.settings.includePayPalFee ? (1 - (Config.settings.paypalFeePercentage / 100)) : 1;
const effectiveRate = marketRate * feeMultiplier;
this._convertPrices(document.body, effectiveRate);
new MutationObserver(mutations => mutations.forEach(m => m.addedNodes.forEach(n => {
if (n.nodeType === Node.ELEMENT_NODE && !n.closest('#powerup-fetch-iframe')) {
this._convertPrices(n, effectiveRate);
}
}))).observe(document.body, { childList: true, subtree: true });
});
}
};
const DebugConsole = {
_createButton() { GM_addStyle(`#debug-dump-btn { position: fixed; bottom: 20px; right: 20px; background-color: #c0392b; color: white; border: none; border-radius: 5px; padding: 10px 15px; font-size: 14px; z-index: 10000; box-shadow: 0 4px 8px rgba(0,0,0,0.4); }`); const btn = document.createElement('button'); btn.id = 'debug-dump-btn'; btn.textContent = 'Dump Project DB'; btn.addEventListener('click', this._dumpDatabaseToConsole); document.body.appendChild(btn); },
async _dumpDatabaseToConsole() {
console.groupCollapsed(`%c[DebugConsole] Project Database Dump`, 'color: #c0392b; font-weight: bold;');
const projectDB = await GM_getValue('slimProjectDatabase_v10', {});
const projectArray = Object.values(projectDB);
if (projectArray.length === 0) {
console.warn("The slim project database is currently empty.");
} else {
console.log(`Found ${projectArray.length} total projects in historical database.`);
console.table(projectArray);
}
console.groupEnd();
},
init() { if (ENABLE_DEBUG_LOGGING) { this._createButton(); } }
};
async function main() {
if (ENABLE_DEBUG_LOGGING) console.log(`${SCRIPT_PREFIX} Script loaded on: ${window.location.href}`);
await Config.load();
const url = window.location.href;
if (url.includes('/payments') || url.includes('/projects')) {
CurrencyConverter.init();
SettingsGUI.init();
DebugConsole.init();
}
if (url.includes('/projects')) {
setTimeout(() => ProjectTracker.init(), 2000);
}
}
main();
})();