// ==UserScript==
// @name Makaytron Etsy Ads Keyword Manager
// @name:tr Makaytron Etsy Ads Keyword Manager
// @name:en Makaytron Etsy Ads Keyword Manager
// @version 1.0.3
// @description Etsy Ads anahtar kelime eşleşmelerini form tabanlı bir panelden yönetin.
// @description:tr Etsy Ads anahtar kelime eşleşmelerini form tabanlı bir panelden yönetin.
// @description:en Manage Etsy Ads keyword matches from a form-based control panel.
// @namespace https://github.com/Makaytron/EtsyScript
// @author Makaytron (@Makaytron)
// @license MIT
// @antifeature tracking
// @homepageURL https://github.com/Makaytron/Etsy-Automation-Tools/tree/main/scripts/etsy-ads-keyword-manager
// @supportURL https://github.com/Makaytron/Etsy-Automation-Tools/issues
// @updateURL https://raw.githubusercontent.com/Makaytron/Etsy-Automation-Tools/main/scripts/etsy-ads-keyword-manager/Makaytron-Etsy-Ads-Keyword-Manager.user.js
// @downloadURL https://raw.githubusercontent.com/Makaytron/Etsy-Automation-Tools/main/scripts/etsy-ads-keyword-manager/Makaytron-Etsy-Ads-Keyword-Manager.user.js
// @match https://www.etsy.com/your/shops/me/advertising/listings/*
// @icon https://raw.githubusercontent.com/Makaytron/Etsy-Automation-Tools/main/assets/makaytron-logo.png
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.addValueChangeListener
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addElement
// @grant GM_xmlhttpRequest
// @connect raw.githubusercontent.com
// @connect sjwibgcflufmzaorlwqe.supabase.co
// @noframes
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ─── Constants ────────────────────────────────────────────────────────────
const SCRIPT_SOURCE_URL = 'https://raw.githubusercontent.com/Makaytron/Etsy-Automation-Tools/main/scripts/etsy-ads-keyword-manager/Makaytron-Etsy-Ads-Keyword-Manager.user.js';
const WORD_LIST_URL = 'https://raw.githubusercontent.com/Makaytron/Etsy-Automation-Tools/main/scripts/etsy-ads-keyword-manager/keyword-rules.txt';
const APP_VERSION = '1.0.3';
const TELEMETRY_ENDPOINT = 'https://sjwibgcflufmzaorlwqe.supabase.co/functions/v1/telemetry-ingest';
const TELEMETRY_HEADER_NAME = 'x-makaytron-telemetry';
const TELEMETRY_HEADER_VALUE = '1';
const TELEMETRY_SCRIPT_ID = 'etsy-ads-keyword-manager';
const TELEMETRY_ALLOWED_EVENTS = new Set(['script_opened', 'ads_keyword_change_applied']);
const TELEMETRY_ALLOWED_ERROR_CODES = new Set(['selector_ads_table', 'selector_ads_control', 'selector_ads_pagination', 'selector_ads_verification', 'network_rules_update', 'runtime_keyword_change', 'storage_rules_state']);
const TELEMETRY_PRIVACY_URL = 'https://github.com/Makaytron/Etsy-Automation-Tools/blob/main/PRIVACY.en.md';
const TELEMETRY_STORAGE_PREFIX = `makaytron-telemetry:${TELEMETRY_SCRIPT_ID}:v1`;
const TELEMETRY_KEYS = Object.freeze({
installationId: `${TELEMETRY_STORAGE_PREFIX}:installation-id`,
enabled: `${TELEMETRY_STORAGE_PREFIX}:enabled`,
enablePending: `${TELEMETRY_STORAGE_PREFIX}:enable-pending`,
consentOperation: `${TELEMETRY_STORAGE_PREFIX}:consent-operation`,
noticeSeen: `${TELEMETRY_STORAGE_PREFIX}:notice-seen:error-codes-v1`,
sentDays: `${TELEMETRY_STORAGE_PREFIX}:sent-days`,
});
const TELEMETRY_MISSING_VALUE = `${TELEMETRY_STORAGE_PREFIX}:missing`;
const TELEMETRY_CONSENT_OPERATION_TTL_MS = 30_000;
const TELEMETRY_SENT_DAYS_OWNER_KEY = '__installationId';
const TELEMETRY_SENT_DAYS_LOCK_NAME = `${TELEMETRY_STORAGE_PREFIX}:sent-days-operation`;
const TELEMETRY_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const telemetryPendingEvents = new Set();
const telemetryRequests = new Set();
let telemetrySentDaysWriteChain = Promise.resolve(false);
let telemetryOperationChain = Promise.resolve(false);
let telemetryInstallationPromise = null;
let telemetryInstallationListenerPromise = null;
let telemetryInstallationListenerReady = false;
let telemetryInstallationChangeChain = Promise.resolve(false);
let telemetryRemoteSentDaysRevision = 0;
let telemetryLatestRemoteSentDays = null;
let telemetryBlockedInSession = false;
let telemetryMenuRegistered = false;
async function telemetryReadValue(key, fallback) {
try {
if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') return { ok: true, value: await GM.getValue(key, fallback) };
if (typeof GM_getValue === 'function') return { ok: true, value: GM_getValue(key, fallback) };
} catch { return { ok: false, value: fallback }; }
return { ok: false, value: fallback };
}
async function telemetryGetValue(key, fallback) {
return (await telemetryReadValue(key, fallback)).value;
}
async function telemetrySetValue(key, value) {
try {
if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') await GM.setValue(key, value);
else if (typeof GM_setValue === 'function') GM_setValue(key, value);
else return false;
return true;
} catch { return false; }
}
async function telemetryDeleteValue(key) {
try {
if (typeof GM !== 'undefined' && typeof GM.deleteValue === 'function') await GM.deleteValue(key);
else if (typeof GM_deleteValue === 'function') GM_deleteValue(key);
else return telemetrySetValue(key, null);
return true;
} catch { return false; }
}
function telemetrySuppressed() {
return navigator.webdriver === true
|| navigator.doNotTrack === '1'
|| globalThis.doNotTrack === '1'
|| navigator.globalPrivacyControl === true;
}
function telemetryUuid() {
if (typeof globalThis.crypto?.randomUUID === 'function') return globalThis.crypto.randomUUID();
const bytes = new Uint8Array(16);
if (typeof globalThis.crypto?.getRandomValues === 'function') globalThis.crypto.getRandomValues(bytes);
else return '';
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes].map(value => value.toString(16).padStart(2, '0'));
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`;
}
function telemetryWebLocksAvailable() {
const locks = typeof navigator !== 'undefined' ? navigator.locks : null;
return !!locks && typeof locks.request === 'function';
}
function withTelemetrySentDaysLock(operation) {
const locks = typeof navigator !== 'undefined' ? navigator.locks : null;
if (locks && typeof locks.request === 'function') {
return locks.request(
TELEMETRY_SENT_DAYS_LOCK_NAME,
{ mode: 'exclusive' },
() => operation(true),
);
}
return Promise.resolve(false);
}
async function telemetryDeleteAndConfirmMissing(key) {
await telemetryDeleteValue(key);
const readback = await telemetryReadValue(key, TELEMETRY_MISSING_VALUE);
const cleared = readback.ok && readback.value === TELEMETRY_MISSING_VALUE;
if (!cleared) telemetryBlockedInSession = true;
return cleared;
}
async function clearTelemetrySentDaysForInstallationLocked(installationId) {
const remoteRevisionBefore = telemetryRemoteSentDaysRevision;
const read = await telemetryReadValue(TELEMETRY_KEYS.sentDays, TELEMETRY_MISSING_VALUE);
if (!read.ok) { telemetryBlockedInSession = true; return false; }
if (read.value === TELEMETRY_MISSING_VALUE) return true;
const stored = read.value && typeof read.value === 'object' && !Array.isArray(read.value) ? read.value : null;
const owner = stored?.[TELEMETRY_SENT_DAYS_OWNER_KEY];
if (typeof owner === 'string' && TELEMETRY_UUID_PATTERN.test(owner) && owner !== installationId) return true;
const cleared = await telemetryDeleteAndConfirmMissing(TELEMETRY_KEYS.sentDays);
const remoteValue = telemetryLatestRemoteSentDays;
const remoteOwner = remoteValue?.[TELEMETRY_SENT_DAYS_OWNER_KEY];
if (telemetryRemoteSentDaysRevision !== remoteRevisionBefore
&& typeof remoteOwner === 'string' && TELEMETRY_UUID_PATTERN.test(remoteOwner)
&& remoteOwner !== installationId) {
await telemetrySetValue(TELEMETRY_KEYS.sentDays, remoteValue);
const restored = await telemetryReadValue(TELEMETRY_KEYS.sentDays, null);
const preserved = restored.ok
&& restored.value?.[TELEMETRY_SENT_DAYS_OWNER_KEY] === remoteOwner;
if (!preserved) telemetryBlockedInSession = true;
return preserved;
}
return cleared;
}
function clearTelemetrySentDaysForInstallation(installationId) {
return withTelemetrySentDaysLock(
() => clearTelemetrySentDaysForInstallationLocked(installationId),
{ destructive: true },
);
}
async function reconcileReplacedTelemetryInstallation(oldInstallationId) {
const [deleted, sentDaysCleared] = await Promise.all([
telemetryDeleteInstallation(oldInstallationId),
clearTelemetrySentDaysForInstallation(oldInstallationId),
]);
if (!deleted || !sentDaysCleared) telemetryBlockedInSession = true;
return deleted && sentDaysCleared;
}
function queueTelemetryInstallationReconciliation(oldValue, newValue, remote) {
if (remote !== true
|| typeof oldValue !== 'string'
|| !TELEMETRY_UUID_PATTERN.test(oldValue)
|| oldValue === newValue) return;
const cleanup = telemetryInstallationChangeChain.then(
() => reconcileReplacedTelemetryInstallation(oldValue),
() => reconcileReplacedTelemetryInstallation(oldValue),
);
telemetryInstallationChangeChain = cleanup.catch(() => {
telemetryBlockedInSession = true;
return false;
});
}
async function ensureTelemetryInstallationListener() {
if (telemetryInstallationListenerReady) return true;
if (telemetryInstallationListenerPromise) return telemetryInstallationListenerPromise;
const pending = (async () => {
try {
const modern = typeof GM !== 'undefined' && typeof GM.addValueChangeListener === 'function'
? GM.addValueChangeListener.bind(GM)
: null;
const legacy = typeof GM_addValueChangeListener === 'function' ? GM_addValueChangeListener : null;
const register = modern || legacy;
if (!register) return false;
const installationListenerId = await register(
TELEMETRY_KEYS.installationId,
(_key, oldValue, newValue, remote) => queueTelemetryInstallationReconciliation(oldValue, newValue, remote),
);
const sentDaysListenerId = await register(
TELEMETRY_KEYS.sentDays,
(_key, _oldValue, newValue, remote) => {
if (remote !== true) return;
telemetryRemoteSentDaysRevision += 1;
telemetryLatestRemoteSentDays = newValue && typeof newValue === 'object' && !Array.isArray(newValue)
? { ...newValue } : null;
},
);
telemetryInstallationListenerReady = installationListenerId !== undefined && installationListenerId !== null
&& sentDaysListenerId !== undefined && sentDaysListenerId !== null;
return telemetryInstallationListenerReady;
} catch { return false; }
})();
telemetryInstallationListenerPromise = pending;
try { return await pending; }
finally { if (telemetryInstallationListenerPromise === pending) telemetryInstallationListenerPromise = null; }
}
function normalizeTelemetryConsentOperation(value) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
const token = typeof value.token === 'string' ? value.token : '';
const intent = value.intent === 'enable' || value.intent === 'disable' ? value.intent : '';
const startedAt = Number(value.startedAt);
return token && intent && Number.isFinite(startedAt) ? { token, intent, startedAt } : null;
}
function telemetryConsentOperationActive(operation) {
const age = Date.now() - Number(operation?.startedAt);
return !!operation && age >= -5_000 && age < TELEMETRY_CONSENT_OPERATION_TTL_MS;
}
function telemetryConsentOperationToken() {
const uuid = telemetryUuid();
return TELEMETRY_UUID_PATTERN.test(uuid)
? uuid
: `op-${Date.now()}-${Math.random().toString(36).slice(2, 14)}`;
}
async function telemetryConsentIdle() {
const read = await telemetryReadValue(TELEMETRY_KEYS.consentOperation, TELEMETRY_MISSING_VALUE);
if (!read.ok) { telemetryBlockedInSession = true; return false; }
if (read.value === TELEMETRY_MISSING_VALUE) return true;
const operation = normalizeTelemetryConsentOperation(read.value);
if (!operation) { telemetryBlockedInSession = true; return false; }
return !telemetryConsentOperationActive(operation);
}
async function beginTelemetryConsentOperation(intent) {
const existingRead = await telemetryReadValue(TELEMETRY_KEYS.consentOperation, TELEMETRY_MISSING_VALUE);
if (!existingRead.ok) { telemetryBlockedInSession = true; return ''; }
if (existingRead.value !== TELEMETRY_MISSING_VALUE) {
const existing = normalizeTelemetryConsentOperation(existingRead.value);
if (!existing || telemetryConsentOperationActive(existing)) return '';
}
const operation = { token: telemetryConsentOperationToken(), intent, startedAt: Date.now() };
await telemetrySetValue(TELEMETRY_KEYS.consentOperation, operation);
let verified = await telemetryReadValue(TELEMETRY_KEYS.consentOperation, null);
if (!verified.ok || normalizeTelemetryConsentOperation(verified.value)?.token !== operation.token) return '';
await new Promise((resolve) => setTimeout(resolve, 0));
verified = await telemetryReadValue(TELEMETRY_KEYS.consentOperation, null);
return verified.ok && normalizeTelemetryConsentOperation(verified.value)?.token === operation.token
? operation.token : '';
}
async function telemetryConsentOperationOwned(token, intent) {
const read = await telemetryReadValue(TELEMETRY_KEYS.consentOperation, null);
const operation = read.ok ? normalizeTelemetryConsentOperation(read.value) : null;
return !!operation && operation.token === token && operation.intent === intent
&& telemetryConsentOperationActive(operation);
}
async function releaseTelemetryConsentOperation(token, intent) {
if (!await telemetryConsentOperationOwned(token, intent)) return false;
await telemetryDeleteValue(TELEMETRY_KEYS.consentOperation);
const readback = await telemetryReadValue(TELEMETRY_KEYS.consentOperation, TELEMETRY_MISSING_VALUE);
const released = readback.ok && readback.value === TELEMETRY_MISSING_VALUE;
if (!released) telemetryBlockedInSession = true;
return released;
}
async function telemetryInstallationId() {
if (telemetryInstallationPromise) return telemetryInstallationPromise;
const pending = (async () => {
const listenerReady = await ensureTelemetryInstallationListener();
const storedRead = await telemetryReadValue(TELEMETRY_KEYS.installationId, '');
if (!storedRead.ok) return '';
const stored = storedRead.value;
if (typeof stored === 'string' && TELEMETRY_UUID_PATTERN.test(stored)) return stored;
if (!listenerReady && !telemetryWebLocksAvailable()) return '';
const created = telemetryUuid();
if (!TELEMETRY_UUID_PATTERN.test(created) || !await telemetrySetValue(TELEMETRY_KEYS.installationId, created)) return '';
const verifiedRead = await telemetryReadValue(TELEMETRY_KEYS.installationId, '');
return verifiedRead.ok && typeof verifiedRead.value === 'string' && TELEMETRY_UUID_PATTERN.test(verifiedRead.value)
? verifiedRead.value : '';
})();
telemetryInstallationPromise = pending;
try { return await pending; }
finally { if (telemetryInstallationPromise === pending) telemetryInstallationPromise = null; }
}
function sendTelemetry(method, payload) {
return new Promise(resolve => {
const modernRequest = typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function'
? GM.xmlHttpRequest.bind(GM)
: null;
const request = modernRequest || (typeof GM_xmlhttpRequest === 'function' ? GM_xmlhttpRequest : null);
if (!request) { resolve(false); return; }
let settled = false;
const finish = value => {
if (settled) return;
settled = true;
resolve(Boolean(value));
};
const options = {
method,
url: TELEMETRY_ENDPOINT,
headers: {
'Content-Type': 'application/json',
[TELEMETRY_HEADER_NAME]: TELEMETRY_HEADER_VALUE,
},
data: JSON.stringify(payload),
anonymous: true,
timeout: 8000,
onload: response => finish(Number(response?.status) === 202),
onerror: () => finish(false),
onabort: () => finish(false),
ontimeout: () => finish(false),
};
try {
const pending = request(options);
if (pending && typeof pending.then === 'function') {
pending.then(response => finish(Number(response?.status) === 202)).catch(() => finish(false));
}
} catch { finish(false); }
});
}
function dispatchTelemetry(method, payload) {
const request = sendTelemetry(method, payload);
telemetryRequests.add(request);
void request.finally(() => telemetryRequests.delete(request));
return request;
}
function withTelemetryOperationLock(callback) {
const run = () => {
const locks = typeof navigator !== 'undefined' ? navigator.locks : null;
return locks && typeof locks.request === 'function'
? locks.request(`${TELEMETRY_STORAGE_PREFIX}:operation`, { mode: 'exclusive' }, callback)
: callback();
};
const operation = telemetryOperationChain.then(run, run);
telemetryOperationChain = operation.catch(() => false);
return operation;
}
function telemetryDeleteInstallation(installationId) {
if (typeof installationId !== 'string' || !TELEMETRY_UUID_PATTERN.test(installationId)) return Promise.resolve(false);
return dispatchTelemetry('DELETE', {
schema: 1,
installation_id: installationId,
script_id: TELEMETRY_SCRIPT_ID,
app_version: APP_VERSION,
});
}
async function compensateTelemetryInstallation(installationId) {
telemetryBlockedInSession = true;
return telemetryDeleteInstallation(installationId);
}
async function telemetrySignalStillEnabled(installationId) {
if (telemetryBlockedInSession) return false;
if (!await telemetryConsentIdle()) return false;
const enabledRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
if (!enabledRead.ok || enabledRead.value !== true || telemetryBlockedInSession) return false;
const pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
if (!pendingRead.ok || pendingRead.value !== TELEMETRY_MISSING_VALUE || telemetryBlockedInSession) return false;
const installationRead = await telemetryReadValue(TELEMETRY_KEYS.installationId, '');
return installationRead.ok && installationRead.value === installationId && !telemetryBlockedInSession;
}
function markTelemetrySignalSent(signalKey, utcDay, installationId) {
const write = telemetrySentDaysWriteChain.then(() => withTelemetrySentDaysLock(async (lockHeld) => {
const failClosed = async (clearSentDays = false) => {
if (clearSentDays && lockHeld) {
await clearTelemetrySentDaysForInstallationLocked(installationId);
}
await compensateTelemetryInstallation(installationId);
return false;
};
if (!await telemetrySignalStillEnabled(installationId)) return failClosed();
const storedDaysRead = await telemetryReadValue(TELEMETRY_KEYS.sentDays, {});
if (!storedDaysRead.ok) return failClosed();
const storedDays = storedDaysRead.value;
const sentDays = storedDays && typeof storedDays === 'object' && !Array.isArray(storedDays)
&& storedDays[TELEMETRY_SENT_DAYS_OWNER_KEY] === installationId ? storedDays : {};
if (!await telemetrySetValue(TELEMETRY_KEYS.sentDays, {
...sentDays,
[TELEMETRY_SENT_DAYS_OWNER_KEY]: installationId,
[signalKey]: utcDay,
})) return failClosed(true);
if (await telemetrySignalStillEnabled(installationId)) return true;
return failClosed(true);
}));
telemetrySentDaysWriteChain = write.catch(() => false);
return write;
}
async function trackTelemetry(eventName) {
if (!TELEMETRY_ALLOWED_EVENTS.has(eventName) || telemetrySuppressed() || !telemetryWebLocksAvailable()) return false;
return withTelemetryOperationLock(async () => {
if (telemetryBlockedInSession || telemetryPendingEvents.has(eventName)) return false;
telemetryPendingEvents.add(eventName);
try {
const enabledRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
if (!enabledRead.ok || enabledRead.value !== true || telemetryBlockedInSession) return false;
if (!await telemetryConsentIdle()) return false;
const pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
if (!pendingRead.ok || pendingRead.value !== TELEMETRY_MISSING_VALUE || telemetryBlockedInSession) return false;
const installationId = await telemetryInstallationId();
if (!installationId || telemetryBlockedInSession) return false;
const utcDay = new Date().toISOString().slice(0, 10);
const storedDaysRead = await telemetryReadValue(TELEMETRY_KEYS.sentDays, {});
if (!storedDaysRead.ok) return false;
const storedDays = storedDaysRead.value;
const sentDays = storedDays && typeof storedDays === 'object' && !Array.isArray(storedDays)
&& storedDays[TELEMETRY_SENT_DAYS_OWNER_KEY] === installationId ? storedDays : {};
if (sentDays[eventName] === utcDay) return false;
if (telemetryBlockedInSession) return false;
const accepted = await dispatchTelemetry('POST', {
schema: 1,
installation_id: installationId,
script_id: TELEMETRY_SCRIPT_ID,
event_name: eventName,
app_version: APP_VERSION,
});
if (!accepted) return false;
if (!await telemetrySignalStillEnabled(installationId)) {
await compensateTelemetryInstallation(installationId);
return false;
}
return markTelemetrySignalSent(eventName, utcDay, installationId);
} catch { return false; }
finally { telemetryPendingEvents.delete(eventName); }
});
}
async function trackTelemetryError(errorCode) {
if (!TELEMETRY_ALLOWED_ERROR_CODES.has(errorCode) || telemetrySuppressed() || !telemetryWebLocksAvailable()) return false;
const signalKey = `error:${errorCode}`;
return withTelemetryOperationLock(async () => {
if (telemetryBlockedInSession || telemetryPendingEvents.has(signalKey)) return false;
telemetryPendingEvents.add(signalKey);
try {
const enabledRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
if (!enabledRead.ok || enabledRead.value !== true || telemetryBlockedInSession) return false;
if (!await telemetryConsentIdle()) return false;
const pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
if (!pendingRead.ok || pendingRead.value !== TELEMETRY_MISSING_VALUE || telemetryBlockedInSession) return false;
const installationId = await telemetryInstallationId();
if (!installationId || telemetryBlockedInSession) return false;
const utcDay = new Date().toISOString().slice(0, 10);
const storedDaysRead = await telemetryReadValue(TELEMETRY_KEYS.sentDays, {});
if (!storedDaysRead.ok) return false;
const storedDays = storedDaysRead.value;
const sentDays = storedDays && typeof storedDays === 'object' && !Array.isArray(storedDays)
&& storedDays[TELEMETRY_SENT_DAYS_OWNER_KEY] === installationId ? storedDays : {};
if (sentDays[signalKey] === utcDay || telemetryBlockedInSession) return false;
const accepted = await dispatchTelemetry('POST', {
schema: 1,
installation_id: installationId,
script_id: TELEMETRY_SCRIPT_ID,
error_code: errorCode,
app_version: APP_VERSION,
});
if (!accepted) return false;
if (!await telemetrySignalStillEnabled(installationId)) {
await compensateTelemetryInstallation(installationId);
return false;
}
return markTelemetrySignalSent(signalKey, utcDay, installationId);
} catch { return false; }
finally { telemetryPendingEvents.delete(signalKey); }
});
}
async function disableTelemetryAndDelete() {
return withTelemetryOperationLock(async () => {
telemetryBlockedInSession = true;
const consentToken = await beginTelemetryConsentOperation('disable');
if (!consentToken) return { disabled: false, deleted: false };
let outcome = { disabled: false, deleted: false };
try {
outcome = await (async () => {
const settingSaved = await telemetrySetValue(TELEMETRY_KEYS.enabled, false);
const settingRead = settingSaved
? await telemetryReadValue(TELEMETRY_KEYS.enabled, true)
: { ok: false, value: true };
if (!settingRead.ok || settingRead.value !== false) return { disabled: false, deleted: false };
if (!await telemetryConsentOperationOwned(consentToken, 'disable')) return { disabled: false, deleted: false };
await Promise.allSettled([...telemetryRequests]);
await telemetrySentDaysWriteChain.catch(() => false);
const installationRead = await telemetryReadValue(TELEMETRY_KEYS.installationId, '');
if (!installationRead.ok) return { disabled: true, deleted: false };
if (!await telemetryConsentOperationOwned(consentToken, 'disable')) return { disabled: false, deleted: false };
const installationId = installationRead.value;
let deleted = true;
if (typeof installationId === 'string' && TELEMETRY_UUID_PATTERN.test(installationId)) {
deleted = await dispatchTelemetry('DELETE', {
schema: 1,
installation_id: installationId,
script_id: TELEMETRY_SCRIPT_ID,
app_version: APP_VERSION,
});
}
if (!await telemetryConsentOperationOwned(consentToken, 'disable')) return { disabled: false, deleted: false };
if (deleted) {
const sentDaysCleared = await withTelemetrySentDaysLock(
() => telemetryDeleteAndConfirmMissing(TELEMETRY_KEYS.sentDays),
{ destructive: true },
);
if (!sentDaysCleared) return { disabled: true, deleted: false };
const installationCleared = await telemetryDeleteAndConfirmMissing(TELEMETRY_KEYS.installationId);
const pendingCleared = await telemetryDeleteAndConfirmMissing(TELEMETRY_KEYS.enablePending);
return { disabled: true, deleted: installationCleared && sentDaysCleared && pendingCleared };
}
return { disabled: true, deleted: false };
})();
} finally {
const released = await releaseTelemetryConsentOperation(consentToken, 'disable');
if (!released && outcome.disabled) outcome = { disabled: false, deleted: false };
}
return outcome;
});
}
async function enableTelemetry() {
if (!telemetryWebLocksAvailable()) {
telemetryBlockedInSession = true;
return false;
}
return withTelemetryOperationLock(async () => {
telemetryBlockedInSession = true;
const consentToken = await beginTelemetryConsentOperation('enable');
if (!consentToken) return false;
let enabled = false;
try {
const installationId = await telemetryInstallationId();
if (!installationId || !await telemetryConsentOperationOwned(consentToken, 'enable')) return false;
await telemetrySetValue(TELEMETRY_KEYS.enablePending, installationId);
let pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
if (!pendingRead.ok || pendingRead.value !== installationId) {
pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
if (!pendingRead.ok || pendingRead.value !== installationId) return false;
}
if (!await telemetryConsentOperationOwned(consentToken, 'enable')) return false;
await telemetrySetValue(TELEMETRY_KEYS.enabled, true);
let settingRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, false);
if (!settingRead.ok || settingRead.value !== true) {
settingRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, false);
}
let installationRead = await telemetryReadValue(TELEMETRY_KEYS.installationId, '');
if (!installationRead.ok || installationRead.value !== installationId) {
installationRead = await telemetryReadValue(TELEMETRY_KEYS.installationId, '');
}
if (settingRead.ok && settingRead.value === true
&& installationRead.ok && installationRead.value === installationId
&& await telemetryConsentOperationOwned(consentToken, 'enable')) {
if (!await telemetryDeleteAndConfirmMissing(TELEMETRY_KEYS.enablePending)) return false;
enabled = true;
} else if (await telemetryConsentOperationOwned(consentToken, 'enable')) {
await telemetrySetValue(TELEMETRY_KEYS.enabled, false);
let rollbackRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
if (!rollbackRead.ok || rollbackRead.value !== false) {
rollbackRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
}
if (rollbackRead.ok && rollbackRead.value === false) {
await telemetryDeleteAndConfirmMissing(TELEMETRY_KEYS.enablePending);
}
}
} finally {
const released = await releaseTelemetryConsentOperation(consentToken, 'enable');
if (!released) enabled = false;
telemetryBlockedInSession = !enabled;
}
return enabled;
});
}
async function showTelemetryFirstRunNotice() {
const noticeId = `makaytron-telemetry-notice-${TELEMETRY_SCRIPT_ID}`;
if (!document.documentElement || document.getElementById(noticeId)) return;
const enabledRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
if (!enabledRead.ok) { telemetryBlockedInSession = true; return; }
if (enabledRead.value !== true) return;
if (!await telemetryConsentIdle()) return;
const pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
if (!pendingRead.ok || pendingRead.value !== TELEMETRY_MISSING_VALUE) { telemetryBlockedInSession = true; return; }
const noticeSeenRead = await telemetryReadValue(TELEMETRY_KEYS.noticeSeen, false);
if (!noticeSeenRead.ok) { telemetryBlockedInSession = true; return; }
if (noticeSeenRead.value === true) return;
const notice = document.createElement('aside');
notice.id = noticeId;
notice.setAttribute('role', 'status');
notice.style.cssText = 'all:initial;position:fixed;left:16px;bottom:16px;z-index:2147483647;box-sizing:border-box;width:min(380px,calc(100vw - 32px));padding:14px;border:1px solid #d6d6d6;border-radius:10px;background:#fff;color:#202020;box-shadow:0 16px 42px rgba(0,0,0,.2);font:13px/1.45 Inter,system-ui,sans-serif';
notice.innerHTML = `Privacy-preserving usage metricsUsage metrics are enabled by default. Only the script ID, version, a random installation ID, allowlisted open/success signals, and fixed error codes are sent. No raw error text or Etsy content is collected.
`;
document.documentElement.appendChild(notice);
const noticeSaved = await telemetrySetValue(TELEMETRY_KEYS.noticeSeen, true);
const noticeVerified = noticeSaved
? await telemetryReadValue(TELEMETRY_KEYS.noticeSeen, false)
: { ok: false, value: false };
if (!noticeVerified.ok || noticeVerified.value !== true) telemetryBlockedInSession = true;
notice.querySelector('[data-close]')?.addEventListener('click', () => notice.remove());
notice.querySelector('[data-disable]')?.addEventListener('click', async event => {
const button = event.currentTarget;
button.disabled = true;
const result = await disableTelemetryAndDelete();
const message = notice.querySelector('[data-message]');
if (message) message.textContent = !result.disabled
? 'Usage metrics could not be disabled because the setting was not saved. Metrics remain blocked in this tab; retry from the userscript menu.'
: result.deleted
? 'Usage metrics are disabled and server data was deleted.'
: 'Usage metrics are disabled, but cleanup could not be completed; retry from the userscript menu.';
button.disabled = false;
});
}
async function openTelemetrySettings() {
const modalId = `makaytron-telemetry-settings-${TELEMETRY_SCRIPT_ID}`;
if (!document.documentElement || document.getElementById(modalId)) return;
const modal = document.createElement('div');
modal.id = modalId;
modal.style.cssText = 'all:initial;position:fixed;inset:0;z-index:2147483647;display:grid;place-items:center;padding:16px;background:rgba(0,0,0,.42);font:13px/1.45 Inter,system-ui,sans-serif';
modal.innerHTML = `Privacy-preserving usage metrics
`;
document.documentElement.appendChild(modal);
const renderState = async result => {
const enabledRead = await telemetryReadValue(TELEMETRY_KEYS.enabled, true);
const pendingRead = await telemetryReadValue(TELEMETRY_KEYS.enablePending, TELEMETRY_MISSING_VALUE);
const consentIdle = await telemetryConsentIdle();
if (!enabledRead.ok || !pendingRead.ok) telemetryBlockedInSession = true;
const enabledState = !enabledRead.ok || !pendingRead.ok || !consentIdle
|| pendingRead.value !== TELEMETRY_MISSING_VALUE
|| (enabledRead.value !== false && telemetryBlockedInSession)
? 'unavailable'
: enabledRead.value === false ? 'disabled' : 'enabled';
const state = modal.querySelector('[data-state]');
const output = modal.querySelector('[data-result]');
const enableButton = modal.querySelector('[data-enable]');
if (state) state.textContent = enabledState === 'enabled'
? 'Status: enabled. Allowlisted open/success signals and fixed error codes only; no raw error text or Etsy content is collected.'
: enabledState === 'disabled'
? 'Status: disabled.'
: 'Status: unavailable. Usage metrics remain blocked in this tab until the setting can be read and verified.';
if (output) output.textContent = result || '';
if (enableButton) enableButton.hidden = enabledState === 'enabled';
};
modal.querySelector('[data-close]')?.addEventListener('click', () => modal.remove());
modal.addEventListener('click', event => { if (event.target === modal) modal.remove(); });
modal.querySelector('[data-enable]')?.addEventListener('click', async () => {
await renderState(await enableTelemetry() ? 'Usage metrics enabled.' : 'The setting could not be saved.');
});
modal.querySelector('[data-disable]')?.addEventListener('click', async () => {
const result = await disableTelemetryAndDelete();
await renderState(!result.disabled
? 'Usage metrics could not be disabled because the setting was not saved. Metrics remain blocked in this tab; retry here.'
: result.deleted
? 'Usage metrics disabled; server data deleted.'
: 'Usage metrics are disabled, but cleanup could not be completed. You can retry here.');
});
await renderState('');
}
function telemetryPanelOpened() {
if (telemetrySuppressed()) return;
void showTelemetryFirstRunNotice()
.then(() => trackTelemetry('script_opened'))
.catch(() => {});
}
function registerTelemetryMenuCommand() {
if (telemetryMenuRegistered) return;
try {
if (typeof GM !== 'undefined' && typeof GM.registerMenuCommand === 'function') GM.registerMenuCommand('Makaytron · Usage metrics settings', openTelemetrySettings);
else if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('Makaytron · Usage metrics settings', openTelemetrySettings);
else return;
telemetryMenuRegistered = true;
} catch {}
}
const MAKAYTRON_WEBSITE_URL = 'https://makaytron.com';
// Keep the visible UI self-contained; opening an Etsy Ads page must not fetch a branding asset.
const MAKAYTRON_LOGO_URL = 'data:image/png;base64,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';
const PANEL_ROOT_ID = 'makaytron-ad-wordlist';
const TOAST_ROOT_ID = 'makaytron-ad-wordlist-toasts';
const EDITOR_MODAL_ID = 'makaytron-ad-wordlist-editor';
const PANEL_COLLAPSED_KEY = 'adWordlistPanelCollapsed';
const LANGUAGE_KEY = 'adWordlistLanguage';
const WORDLIST_BACKUP_KEY = 'adWordlistBackup';
const SCRIPT_UPDATE_STATE_KEY = 'adKeywordManagerScriptUpdateState';
const SCRIPT_UPDATE_INTERVAL_MS = 24 * 60 * 60 * 1000;
const MAX_REMOTE_SCRIPT_BYTES = 1_000_000;
const KEYWORD_ACCORDION_SELECTOR = '#listing-detail-targeted-keywords-accordion';
const KEYWORD_ROW_SELECTOR = `${KEYWORD_ACCORDION_SELECTOR} tbody tr.wt-table__row`;
const PAGE_CHANGE_TIMEOUT_MS = 15_000;
const INITIAL_ROWS_TIMEOUT_MS = 15_000;
const MAX_AUTO_PAGES = 100;
const MAX_WORDLIST_BYTES = 250_000;
const DEFAULT_WORDLIST = `dtf
svg
png
sticker
zip
hat
=hoody
=hoddy
=hoodie
=womens hoodie
=womens sweatshirts
=women's sweatshirts
=women hoodies
=graphic hoodies
=graphic hoodie
=hoddies' for women
=graphic hoodies for women
=sweatshirt
=sweater
=mens hoodies
=hoodie women
=hoodies for men
=trendy hoodies
=trendy
=graphic sweatshirt
=plus size hoodie
=toddler
=halloween
=basketball
slippers
=men's hoodies
doll
design
=nurse
=shirt
decal
embroider
transfer
iron on
pants
earrings
jewelry
purse
ornament
headband
bracelet
necklace
decor
patch
cardigan
skirt
mug
bag
tumbler
=football
dxf
=mens sweatshirt
=gender-neutral adult sweatshirts
=gender-neutral adult hoodies
vinyl
glitter
cricut
=baseball
jacket
=tshirt
=comfort colors
=graphic tees
socks
ready to press
poster
bottle
wall art
shoes
shorts
plushie
plush
pitcher
helmet
glove
blanket
=womens hoodie
=womens sweatshirts
=women's sweatshirts
=women hoodies
=graphic hoodies
=graphic hoodie
=hoddies' for women
=graphic hoodies for women
=sweatshirt
=sweater
=mens hoodies
=hoodie women
=hoodies for men
=trendy hoodies
=trendy
=graphic sweatshirt
=plus size hoodie
=toddler
=halloween
=basketball
=popular shirts
=shirt for woman
=mom
=trendy shirt
=tshirt
=mama
=popular shirt
=trending shirt
=popular now shirt
=trend
=comfort colors tshirt
=trendy shirts
=plus size graphic tees
=preppy shirt
=mother
=comfort colors
=women shirt
=popular
=women shirts
=graphic tees for women
=womens graphic t shirts
=women’s shirts
=t shirt
=shirts for women
=comfort colors® shirt
=graphic tshirt women
=plus size t shirts
=trend shirt
=trend shirt
=womens tshirts
=bestseller shirt
=trending now shirt
=trendy comfort colors shirt
=comfort colors shirts
=t shirts for women
=trendy tshirts
=comfort colors graphic tee
=comfort tshirt
=comfort tshirt
=comfort colors tee
=adult shirt
=trendy tee
=trendy shirt for women
=womens clothing
=shirt trend
=trendy tshirt
=plus size clothing
=graphic shirts
=trendy shirt women
=trending tshirt
=popular shirts
=womens shirts trendy
=hottrend tshirt
=plus size graphic tee
=graphic tee popular now
=plus size graphic tshirts
=trendy tshirt for women
=women graphic tees
=retro shirts
=plus size t shirt
keychain
=comfort color tshirts
=comfort color t shirts
=womens comfort colors shirts
=comfort color t-shirts
=t shirts
=women graphic shirt
=graphic tee for women
=womens shirts
=womens shirt
=t-shirt
=comfort colors t shirt
=comfort colors shirt
=comfort colors t-shirt
=trending tees
=best selling tshirts
=womens plus size clothing
=trending t shirt
=comfort colors tshirts
=women graphic tee
=women’s tshirts
=women’s graphic tees
=women’s clothing
=womens tshirts trendy
=womens tshirt
=womens t shirts trendy
=womens shirts
=womens shirt
=womens plus size shirts
=womens graphic tshirts
=womens graphic tshirt
=women's t-shirts comfort colors
=women's shirts
=women's shirt
=women's graphic tees
=women's graphic tee
=women's comfort colors tshirt
=women's comfort colors shirt
=women tshirt
=women short sleeve shirts
=women shirt trendy
=women graphic shirts
=tshirts popular now
=tshirts for women trendy
=tshirts for women
=tshirt trendy
=tshirt trending
=tshirt for women trendy
=tshirt for women
=trendy womens shirt
=trendy women shirt
=trendy tshirts women
=trendy tshirts for women
=trendy tees
=trendy t shirts for women
=trendy t shirts
=trendy shirts for women
=trendy graphic tee
=trendy graphic shirts
=trendy gifts
=trendy clothing
=trending tshirts
=trending t shirts
=trending shirts
=t shirt popular now
=t shirt women trendy
=t shirts for women trendy
=this awesome
=stripes shirts
=stripes shirt
=striped t shirt
=striped shirts
=striped shirt women
=striped graphic tees
=stripe tshirt
=shirts trending
=shirts for women trendy
=shirts for women comfort colors
=shirt trendy
=shirt trending today
=shirt trending
=shirt popular now
=shirt for women trendy
=shirt for women
=retro tshirts
=retro tshirt
=retro striped shirt
=popular shirts now
=popular t shirts
=popular tees
=popular tshirt
=preppy shirts
=preppy tshirt
=plus size shirt
=popular gifts
=popular graphic shirt
=popular graphic tee
=popular graphic tshirts
popular now
=popular shirt now
=popular shirts for women
=plus size clothing for women
oversized
=now popular shirts
=graphic shirt
=graphic shirt women
=graphic shirts for women
=graphic tee shirt
=gender-neutral adult clothing
=gift shirt
=comfort colors women`;
// ─── Localization ─────────────────────────────────────────────────────────
const I18N = Object.freeze({
tr: Object.freeze({
panelAria: 'Makaytron Etsy Ads Keyword Manager kontrol paneli',
subtitle: 'Makaytron · Reklam Araçları',
ready: 'Hazır',
running: 'Çalışıyor',
completed: 'Tamamlandı',
warning: 'Uyarı',
error: 'Hata',
logoOpen: 'Makaytron web sitesini aç',
collapsePanel: 'Paneli gizle',
expandPanel: 'Paneli aç',
switchLanguage: 'İngilizceye geç',
languageChanged: 'Dil Türkçe olarak değiştirildi.',
languageChangeFailed: 'Dil tercihi kaydedilemedi: {error}',
keywordTableWaiting: 'Anahtar kelime tablosu bekleniyor.',
statPage: 'Sayfa',
statRows: 'Satır',
statMatches: 'Eşleşen',
statHighRatio: 'Yüksek oran',
closeCurrentPage: 'Bu sayfadaki eşleşmeleri kapat',
openCurrentPage: 'Bu sayfadaki eşleşmeleri aç',
closeAllPages: 'Tüm sayfalardaki eşleşmeleri kapat',
editWordlist: 'Kelime listesi',
updateWordlist: 'Listeyi güncelle',
checkScriptUpdate: 'Sürümü denetle',
scriptUpdateChecking: 'Yeni script sürümü denetleniyor…',
scriptUpdateCurrent: 'Etsy Ads Keyword Manager güncel · v{version}',
scriptUpdateAvailable: 'Yeni sürüm hazır: v{version}',
scriptUpdateInstall: 'Kurulum sayfasını aç',
scriptUpdateFailed: 'Sürüm denetlenemedi. Ana araç çalışmaya devam eder: {error}',
scriptUpdateInvalid: 'Uzak script geçerli bir sürüm bilgisi içermiyor.',
scriptUpdateBlocked: 'Sürüm denetimi için önce açık editörü veya devam eden kelime işlemini tamamlayın.',
scriptInstallBlocked: 'Kurulum sayfası, açık editör veya devam eden kelime işlemi varken açılamaz.',
scriptUpdateExternalSource: 'Güncellemeler kurulum kaynağınız tarafından yönetiliyor; özel GitHub denetimi çalıştırılmadı.',
scriptUpdateUnknownSource: 'Kurulum kaynağı GitHub olarak doğrulanamadı; özel GitHub denetimi çalıştırılmadı.',
confirmScriptInstall: 'v{version} kurulum sayfası yeni sekmede açılsın mı? Tampermonkey son onayı sizden isteyecek; hiçbir sürüm sessizce kurulmaz.',
scriptInstallOpening: 'v{version} kurulum sayfası açılıyor.',
currentPage: 'mevcut sayfa',
allPages: 'tüm sayfalar',
safeMatchMode: 'Kullanıcı kontrollü eşleşme',
requestTimeout: 'İstek zaman aşımına uğradı.',
rowOne: 'satır',
rowMany: 'satır',
matchOne: 'eşleşme',
matchMany: 'eşleşme',
keywordOne: 'kelime',
keywordMany: 'kelime',
ruleOne: 'kural',
ruleMany: 'kural',
enabled: 'açıldı',
disabled: 'kapatıldı',
keywordFallback: 'Kelime',
keywordChangeFailed: '{keyword} değiştirilemedi.',
keywordChanged: '{keyword}\nKelime {state}',
nextButtonMissing: 'Sonraki sayfa düğmesi bulunamadı; işlem güvenli biçimde durduruldu.',
nextPageTimeout: 'Sonraki anahtar kelime sayfası zamanında yüklenmedi.',
keywordRowsFailed: 'Anahtar kelime satırları yüklenemedi.',
repeatedPage: 'Aynı anahtar kelime sayfası tekrar algılandı; işlem durduruldu.',
processingPage: '{page}. sayfadaki eşleşmeler işleniyor.',
processingKeywords: 'Kelimeler işleniyor',
maxPagesReached: 'Güvenlik sınırı olan {count} sayfaya ulaşıldı.',
totalChanged: 'Toplam {keywords} {state}.',
operationBusy: 'Başka bir kelime işlemi zaten devam ediyor.',
editorOperationBlocked: 'Önce açık kelime listesi formunu kapatın veya kaydedin.',
allPagesWillProcess: 'Tüm sayfalardaki eşleşmeler işlenecek.',
confirmAllPages: 'Tüm sayfalardaki eşleşen Etsy Ads anahtar kelimeleri kapatılacak. Devam edilsin mi?',
confirmCurrentPageShortcut: 'Klavye kısayolu bu sayfadaki eşleşen Etsy Ads anahtar kelimelerini kapatacak. Devam edilsin mi?',
currentPageProcessing: 'Mevcut sayfa işleniyor.',
operationStopped: 'İşlem durduruldu: {error}',
operationStoppedFallback: 'İşlem durduruldu.',
consoleOperationFailed: 'İşlem başarısız:',
remoteChecking: 'Uzak kelime listesi denetleniyor.',
confirmListUpdate: 'Kayıtlı kelime listeniz GitHub’daki varsayılan listeyle değiştirilecek. Mevcut listenin yedeği alınarak devam edilsin mi?',
confirmListRestore: 'Yedeklenen kelime listesi geri yüklensin mi?',
listUpdating: 'Liste güncelleniyor',
remoteListEmpty: 'Uzak kelime listesi boş.',
remoteListTooLarge: 'Uzak kelime listesi beklenenden büyük.',
fetchingKeywords: 'Kelimeler alınıyor',
keywordsUpdated: '{keywords} güncellendi.',
keywordsUpdatedSuccess: '{keywords} başarıyla güncellendi.',
backupListMissing: 'Geri yüklenecek kelime listesi yedeği bulunamadı.',
backupRestored: 'Yedek kelime listesi geri yüklendi.',
backupRestoredStatus: 'Yedek geri yüklendi · {rules} · {matches}.',
backupRestoreFailed: 'Yedek kelime listesi geri yüklenemedi: {error}',
consoleListFailed: 'Kelime listesi alınamadı:',
listFetchFailed: 'Kelimeler alınamadı: {error}',
listFetchFailedFallback: 'Kelime listesi alınamadı.',
editorTitle: 'Kelime Listesi',
close: 'Kapat',
editorNote: 'Teknik işaret kullanmanıza gerek yok. Kelime veya ifadeyi yazın, ardından listedeki hangi anahtar kelimeleri bulacağını seçin.',
addRuleTitle: 'Yeni kelime filtresi ekle',
editRuleTitle: 'Eklenen filtreyi düzenle',
ruleValueLabel: 'Filtrelemek istediğiniz kelime veya ifade',
ruleValuePlaceholder: 'Örn. t-shirt veya comfort colors',
matchTypeLabel: 'Listedeki hangi anahtar kelimeleri bulsun?',
matchContains: 'İfadeyi içeriyor',
matchExact: 'Birebir aynı',
matchRegex: 'Özel kural',
matchContainsOption: 'İfadeyi içeriyorsa (önerilen)',
matchExactOption: 'Yalnızca birebir aynıysa',
matchRegexOption: 'Özel arama kuralı (ileri seviye)',
matchContainsHelp: 'Örnek: “bts” yazarsanız “bts shirt” ve “cute bts gift” gibi içinde “bts” geçen anahtar kelimeler bulunur.',
matchExactHelp: 'Örnek: “bts shirt” yazarsanız yalnızca aynı anahtar kelime bulunur; önünde veya sonunda başka kelime olanlar bulunmaz.',
matchRegexHelp: 'Yalnızca özel arama kalıplarını bilen ileri düzey kullanıcılar içindir. Emin değilseniz ilk seçeneği kullanın.',
customRuleValueLabel: 'Özel arama kuralı',
customRulePlaceholder: 'Örn. bts|army',
addRule: 'Listeye ekle',
updateRule: 'Değişikliği uygula',
cancelEdit: 'Düzenlemeyi iptal et',
ruleListTitle: 'Eklenen filtreler',
searchRulesPlaceholder: 'Kurallarda ara',
searchRulesAria: 'Kural listesinde ara',
noRules: 'Henüz kural eklenmedi.',
noSearchResults: 'Aramanızla eşleşen kural bulunamadı.',
editRule: 'Kuralı düzenle',
removeRule: 'Kuralı sil',
ruleRequired: 'Bir kelime veya ifade yazın.',
invalidRegex: 'Özel arama kuralı geçerli değil. İşaretleri kontrol edip yeniden deneyin.',
duplicateRule: 'Bu kural listede zaten var.',
pendingRule: 'Önce yazdığınız filtreyi “Listeye ekle” düğmesiyle ekleyin veya alanı temizleyin.',
pendingEdit: 'Önce değişikliği uygulayın veya düzenlemeyi iptal edin.',
discardChanges: 'Kaydedilmemiş değişiklikler silinsin mi?',
localListTooLarge: 'Kural listesi izin verilen boyutu aşıyor.',
ruleAdded: 'Kural eklendi.',
ruleUpdated: 'Kural güncellendi.',
ruleRemoved: 'Kural silindi.',
cancel: 'Vazgeç',
save: 'Kaydet',
editorDirty: 'Kaydedilmedi · {matches} mevcut',
editorSaved: 'Kayıtlı · {matches} mevcut',
atLeastOneRule: 'En az bir kural gerekli.',
emptyListCannotSave: 'Kelime listesi boş kaydedilemez.',
rulesSaved: '{rules} kaydedildi.',
rulesSavedStatus: '{rules} kaydedildi · {matches}.',
rulesSavedPreviewFailed: '{rules} kaydedildi; ancak sayfadaki önizleme yenilenemedi: {error}',
editorSaveFailed: 'Kelime listesi kaydedilemedi: {error}',
menuCloseCurrent: 'Bu sayfadaki eşleşmeleri kapat',
menuOpenCurrent: 'Bu sayfadaki eşleşmeleri aç',
menuCloseAll: 'Tüm sayfalarda kapat',
menuUpdateList: 'Kelime listesini güncelle',
menuRestoreList: 'Yedek kelime listesini geri yükle',
menuEditList: 'Kelime listesini düzenle',
menuTogglePanel: 'Paneli aç veya gizle',
menuSwitchLanguage: 'İngilizceye geç',
menuCheckScriptUpdate: 'Script sürümünü denetle',
loadingKeywordTable: 'Anahtar kelime tablosu yükleniyor.',
preparing: 'Hazırlanıyor',
keywordTableFailed: 'Anahtar kelime tablosu yüklenemedi.',
rowsReady: '{rows} bulundu · {matches} hazır.',
appReady: 'Makaytron Etsy Ads Keyword Manager hazır · CTRL + ALT + K',
consoleInitializationFailed: 'Başlatma başarısız:',
appStartFailed: 'Kelime Yöneticisi başlatılamadı: {error}',
appStartFailedFallback: 'Kelime Yöneticisi başlatılamadı.',
}),
en: Object.freeze({
panelAria: 'Makaytron Etsy Ads Keyword Manager control panel',
subtitle: 'Makaytron · Ads Toolkit',
ready: 'Ready',
running: 'Running',
completed: 'Completed',
warning: 'Warning',
error: 'Error',
logoOpen: 'Open the Makaytron website',
collapsePanel: 'Hide panel',
expandPanel: 'Open panel',
switchLanguage: 'Switch to Turkish',
languageChanged: 'Language changed to English.',
languageChangeFailed: 'Could not save the language preference: {error}',
keywordTableWaiting: 'Waiting for the keyword table.',
statPage: 'Page',
statRows: 'Rows',
statMatches: 'Matches',
statHighRatio: 'High ratio',
closeCurrentPage: 'Disable matches on this page',
openCurrentPage: 'Enable matches on this page',
closeAllPages: 'Disable matches on all pages',
editWordlist: 'Keyword list',
updateWordlist: 'Update list',
checkScriptUpdate: 'Check version',
scriptUpdateChecking: 'Checking for a newer script version…',
scriptUpdateCurrent: 'Etsy Ads Keyword Manager is current · v{version}',
scriptUpdateAvailable: 'A new version is ready: v{version}',
scriptUpdateInstall: 'Open install page',
scriptUpdateFailed: 'The version check failed. The main tool remains available: {error}',
scriptUpdateInvalid: 'The remote script does not contain valid version metadata.',
scriptUpdateBlocked: 'Finish the open editor or keyword operation before checking the script version.',
scriptInstallBlocked: 'The install page cannot open while an editor or keyword operation is active.',
scriptUpdateExternalSource: 'Updates are managed by your installation source; the private GitHub check was not run.',
scriptUpdateUnknownSource: 'The installation source could not be verified as GitHub; the private GitHub check was not run.',
confirmScriptInstall: 'Open the v{version} install page in a new tab? Tampermonkey will ask for final approval; no version is installed silently.',
scriptInstallOpening: 'Opening the v{version} install page.',
currentPage: 'current page',
allPages: 'all pages',
safeMatchMode: 'User-controlled matching',
requestTimeout: 'The request timed out.',
rowOne: 'row',
rowMany: 'rows',
matchOne: 'match',
matchMany: 'matches',
keywordOne: 'keyword',
keywordMany: 'keywords',
ruleOne: 'rule',
ruleMany: 'rules',
enabled: 'enabled',
disabled: 'disabled',
keywordFallback: 'Keyword',
keywordChangeFailed: '{keyword} could not be changed.',
keywordChanged: '{keyword}\nKeyword {state}',
nextButtonMissing: 'The Next button was not found; the operation stopped safely.',
nextPageTimeout: 'The next keyword page did not load in time.',
keywordRowsFailed: 'Keyword rows could not be loaded.',
repeatedPage: 'The same keyword page was detected again; the operation stopped.',
processingPage: 'Processing matches on page {page}.',
processingKeywords: 'Processing keywords',
maxPagesReached: 'The safety limit of {count} pages was reached.',
totalChanged: '{keywords} {state} in total.',
operationBusy: 'Another keyword operation is already running.',
editorOperationBlocked: 'Close or save the open keyword-list form first.',
allPagesWillProcess: 'Matches on every page will be processed.',
confirmAllPages: 'Matching Etsy Ads keywords on every page will be disabled. Continue?',
confirmCurrentPageShortcut: 'The keyboard shortcut will disable matching Etsy Ads keywords on this page. Continue?',
currentPageProcessing: 'Processing the current page.',
operationStopped: 'Operation stopped: {error}',
operationStoppedFallback: 'The operation stopped.',
consoleOperationFailed: 'Operation failed:',
remoteChecking: 'Checking the remote keyword list.',
confirmListUpdate: 'Your saved keyword list will be replaced with the default list from GitHub. Continue after backing up the current list?',
confirmListRestore: 'Restore the backed-up keyword list?',
listUpdating: 'Updating list',
remoteListEmpty: 'The remote keyword list is empty.',
remoteListTooLarge: 'The remote keyword list is larger than expected.',
fetchingKeywords: 'Fetching keywords',
keywordsUpdated: '{keywords} updated.',
keywordsUpdatedSuccess: '{keywords} updated successfully.',
backupListMissing: 'No backed-up keyword list is available to restore.',
backupRestored: 'The backed-up keyword list was restored.',
backupRestoredStatus: 'Backup restored · {rules} · {matches}.',
backupRestoreFailed: 'Could not restore the backed-up keyword list: {error}',
consoleListFailed: 'Could not retrieve the keyword list:',
listFetchFailed: 'Could not retrieve keywords: {error}',
listFetchFailedFallback: 'Could not retrieve the keyword list.',
editorTitle: 'Keyword List',
close: 'Close',
editorNote: 'No technical symbols are required. Enter a keyword or phrase, then choose which keywords in the list it should find.',
addRuleTitle: 'Add a new keyword filter',
editRuleTitle: 'Edit the selected filter',
ruleValueLabel: 'Keyword or phrase you want to filter',
ruleValuePlaceholder: 'e.g. t-shirt or comfort colors',
matchTypeLabel: 'Which keywords in the list should this find?',
matchContains: 'Contains keyword',
matchExact: 'Exact wording',
matchRegex: 'Custom rule',
matchContainsOption: 'Contains entered text (recommended)',
matchExactOption: 'Exact same wording only',
matchRegexOption: 'Custom search rule (advanced)',
matchContainsHelp: 'Example: entering “bts” finds keywords such as “bts shirt” and “cute bts gift”.',
matchExactHelp: 'Example: entering “bts shirt” finds only that exact keyword; keywords with extra words are excluded.',
matchRegexHelp: 'For advanced users who already know custom search patterns. If you are unsure, use the first option.',
customRuleValueLabel: 'Custom search rule',
customRulePlaceholder: 'e.g. bts|army',
addRule: 'Add to list',
updateRule: 'Apply change',
cancelEdit: 'Cancel editing',
ruleListTitle: 'Added filters',
searchRulesPlaceholder: 'Search rules',
searchRulesAria: 'Search the rule list',
noRules: 'No rules have been added yet.',
noSearchResults: 'No rules match your search.',
editRule: 'Edit rule',
removeRule: 'Remove rule',
ruleRequired: 'Enter a keyword or phrase.',
invalidRegex: 'That custom search rule is not valid. Check the symbols and try again.',
duplicateRule: 'This rule already exists in the list.',
pendingRule: 'Use “Add to list” to add the filter you entered, or clear the field first.',
pendingEdit: 'Apply the change or cancel editing first.',
discardChanges: 'Discard unsaved changes?',
localListTooLarge: 'The rule list exceeds the allowed size.',
ruleAdded: 'Rule added.',
ruleUpdated: 'Rule updated.',
ruleRemoved: 'Rule removed.',
cancel: 'Cancel',
save: 'Save',
editorDirty: 'Unsaved · {matches} on this page',
editorSaved: 'Saved · {matches} on this page',
atLeastOneRule: 'At least one rule is required.',
emptyListCannotSave: 'The keyword list cannot be saved while empty.',
rulesSaved: '{rules} saved.',
rulesSavedStatus: '{rules} saved · {matches}.',
rulesSavedPreviewFailed: '{rules} saved, but the page preview could not be refreshed: {error}',
editorSaveFailed: 'Could not save the keyword list: {error}',
menuCloseCurrent: 'Disable matches on this page',
menuOpenCurrent: 'Enable matches on this page',
menuCloseAll: 'Disable matches on all pages',
menuUpdateList: 'Update keyword list',
menuRestoreList: 'Restore backed-up keyword list',
menuEditList: 'Edit keyword list',
menuTogglePanel: 'Show or hide panel',
menuSwitchLanguage: 'Switch to Turkish',
menuCheckScriptUpdate: 'Check script version',
loadingKeywordTable: 'Loading the keyword table.',
preparing: 'Preparing',
keywordTableFailed: 'The keyword table could not be loaded.',
rowsReady: '{rows} found · {matches} ready.',
appReady: 'Makaytron Etsy Ads Keyword Manager is ready · CTRL + ALT + K',
consoleInitializationFailed: 'Initialization failed:',
appStartFailed: 'Keyword Manager could not start: {error}',
appStartFailedFallback: 'Keyword Manager could not start.',
}),
});
let currentLanguage = 'tr';
let languageLoaded = false;
function t(key, params = {}) {
const template = I18N[currentLanguage]?.[key] ?? I18N.tr[key] ?? key;
return String(template).replace(/\{([a-zA-Z]\w*)\}/g, (match, name) => (
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : match
));
}
function quantity(count, unit) {
const value = Number(count) || 0;
const form = value === 1 ? 'One' : 'Many';
return `${value} ${t(`${unit}${form}`)}`;
}
async function loadLanguage() {
if (languageLoaded) return currentLanguage;
try {
currentLanguage = (await GM.getValue(LANGUAGE_KEY, 'tr')) === 'en' ? 'en' : 'tr';
} catch {
currentLanguage = 'tr';
}
languageLoaded = true;
return currentLanguage;
}
// ─── Makaytron UI theme ───────────────────────────────────────────────────
function addStyle(css) {
try {
return GM_addElement('style', { type: 'text/css', textContent: css });
} catch {
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
return style;
}
}
addStyle(`
#${PANEL_ROOT_ID}{--maw-bg:#fff;--maw-fg:#171717;--maw-card:#fff;--maw-muted:#f7f7f7;--maw-muted-2:#f2f2f2;--maw-muted-fg:#737373;--maw-border:#e7e7e7;--maw-input:#dedede;--maw-primary:#1f1f1f;--maw-primary-fg:#fafafa;--maw-danger:#b91c1c;--maw-danger-soft:#fff1f1;--maw-warning:#8a5a00;--maw-warning-soft:#fff8ed;--maw-success:#276749;--maw-success-soft:#eef8f1;position:fixed;right:18px;top:86px;width:372px;z-index:2147483645;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:var(--maw-fg);font-variant-numeric:tabular-nums}
#${PANEL_ROOT_ID} *,#${EDITOR_MODAL_ID} *{box-sizing:border-box}
#${PANEL_ROOT_ID} .maw-card{max-height:calc(100vh - 104px);max-height:calc(100dvh - 104px);display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--maw-border);border-radius:11px;background:var(--maw-card);box-shadow:0 1px 3px rgba(15,23,42,.08),0 12px 30px rgba(15,23,42,.08)}
#${PANEL_ROOT_ID} .maw-card[hidden],#${PANEL_ROOT_ID} .maw-collapsed-tab[hidden]{display:none!important}
#${PANEL_ROOT_ID} .maw-head{padding:13px 14px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--maw-border);background:#fff}
#${PANEL_ROOT_ID} .maw-brand,.maw-modal-brand{min-width:0;display:flex;align-items:center;gap:10px}
#${PANEL_ROOT_ID} .maw-logo-shell,.maw-modal-logo-shell{width:48px;height:32px;display:flex;align-items:center;justify-content:center;flex:0 0 auto;text-decoration:none}
#${PANEL_ROOT_ID} .maw-logo,.maw-modal-logo{display:block;width:43px;height:auto;object-fit:contain}
#${PANEL_ROOT_ID} .maw-brand-copy{min-width:0}
#${PANEL_ROOT_ID} .maw-title{margin:0;font-size:13.5px;font-weight:700;letter-spacing:-.015em;line-height:1.22}
#${PANEL_ROOT_ID} .maw-sub{margin-top:3px;color:var(--maw-muted-fg);font-size:11.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
#${PANEL_ROOT_ID} .maw-head-tools{display:flex;align-items:center;gap:7px;flex:0 0 auto}
#${PANEL_ROOT_ID} .maw-head-meta{display:flex;flex-direction:column;align-items:flex-end;gap:5px}
#${PANEL_ROOT_ID} .maw-version,#${PANEL_ROOT_ID} .maw-pill{height:20px;padding:0 7px;display:inline-flex;align-items:center;border:1px solid var(--maw-border);border-radius:999px;background:var(--maw-muted);color:#525252;font-size:10.5px;font-weight:650;white-space:nowrap}
#${PANEL_ROOT_ID} .maw-version{min-height:20px!important;cursor:pointer}
#${PANEL_ROOT_ID} .maw-version:hover{border-color:#cfcfcf;background:#fff;color:#171717}
#${PANEL_ROOT_ID} .maw-pill{gap:5px;background:#fff}
#${PANEL_ROOT_ID} .maw-pill::before{content:'';width:6px;height:6px;border-radius:50%;background:#a3a3a3}
#${PANEL_ROOT_ID} .maw-pill[data-state='running']::before{background:#171717;box-shadow:0 0 0 3px rgba(23,23,23,.08)}
#${PANEL_ROOT_ID} .maw-pill[data-state='success']{color:var(--maw-success);background:var(--maw-success-soft);border-color:#c9dfd1}
#${PANEL_ROOT_ID} .maw-pill[data-state='success']::before{background:var(--maw-success)}
#${PANEL_ROOT_ID} .maw-pill[data-state='warning']{color:var(--maw-warning);background:var(--maw-warning-soft);border-color:#eed69a}
#${PANEL_ROOT_ID} .maw-pill[data-state='warning']::before{background:var(--maw-warning)}
#${PANEL_ROOT_ID} .maw-pill[data-state='error']{color:#991b1b;background:var(--maw-danger-soft);border-color:#f1c0c0}
#${PANEL_ROOT_ID} .maw-pill[data-state='error']::before{background:var(--maw-danger)}
#${PANEL_ROOT_ID} button,.maw-modal button{min-height:36px;padding:0 12px;display:inline-flex;align-items:center;justify-content:center;gap:6px;border-radius:6px;font:650 12.5px/1 Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;cursor:pointer;transition:background .15s ease,border-color .15s ease,color .15s ease,box-shadow .15s ease;white-space:nowrap}
#${PANEL_ROOT_ID} button:focus-visible,.maw-modal button:focus-visible,.maw-modal textarea:focus-visible{outline:2px solid #525252;outline-offset:2px}
#${PANEL_ROOT_ID} button:disabled,.maw-modal button:disabled{opacity:.5;cursor:not-allowed}
#${PANEL_ROOT_ID} .maw-icon-btn,.maw-modal .maw-icon-btn{width:30px;min-width:30px;height:30px;min-height:30px;padding:0;border:1px solid var(--maw-border,#e7e7e7);background:#fff;color:#737373}
#${PANEL_ROOT_ID} .maw-icon-btn:hover,.maw-modal .maw-icon-btn:hover{background:#f7f7f7;color:#171717}
#${PANEL_ROOT_ID} .maw-lang-btn{height:24px;min-height:24px!important;min-width:30px;padding:0 7px!important;border:1px solid var(--maw-border);border-radius:6px;background:#fff;color:#404040;font-size:10.5px!important;font-weight:750!important;letter-spacing:.03em}
#${PANEL_ROOT_ID} .maw-lang-btn:hover{border-color:#cfcfcf;background:#f7f7f7}
.maw-svg{width:15px;height:15px;display:block;flex:0 0 auto;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
#${PANEL_ROOT_ID} .maw-body{min-height:0;padding:14px;overflow-x:hidden;overflow-y:auto}
#${PANEL_ROOT_ID} .maw-status-card{margin-bottom:12px;padding:10px 11px;display:flex;align-items:flex-start;gap:9px;border:1px solid var(--maw-border);border-radius:8px;background:var(--maw-muted)}
#${PANEL_ROOT_ID} .maw-status-icon{width:25px;height:25px;display:grid;place-items:center;flex:0 0 auto;border:1px solid var(--maw-border);border-radius:7px;background:#fff;color:#525252}
#${PANEL_ROOT_ID} .maw-status-copy{min-width:0}
#${PANEL_ROOT_ID} .maw-status-title{font-size:12.5px;font-weight:700;color:#262626}
#${PANEL_ROOT_ID} .maw-status-text{margin-top:2px;color:#525252;font-size:11.5px;line-height:1.42;overflow-wrap:anywhere}
#${PANEL_ROOT_ID} .maw-update-banner{margin:-2px 0 12px;padding:9px 10px;display:flex;align-items:center;justify-content:space-between;gap:9px;border:1px solid var(--maw-border);border-radius:8px;background:#fafafa;color:#525252;font-size:11.5px;line-height:1.4}
#${PANEL_ROOT_ID} .maw-update-banner[hidden]{display:none}
#${PANEL_ROOT_ID} .maw-update-banner[data-state='available']{border-color:#eed69a;background:var(--maw-warning-soft);color:var(--maw-warning)}
#${PANEL_ROOT_ID} .maw-update-banner[data-state='error']{border-color:#f1c0c0;background:var(--maw-danger-soft);color:#991b1b}
#${PANEL_ROOT_ID} .maw-update-banner[data-state='current']{border-color:#c9dfd1;background:var(--maw-success-soft);color:var(--maw-success)}
#${PANEL_ROOT_ID} .maw-update-message{min-width:0;overflow-wrap:anywhere}
#${PANEL_ROOT_ID} .maw-update-install{min-height:30px;padding:0 9px;flex:0 0 auto;border:1px solid currentColor;background:#fff;color:inherit;font-size:11px}
#${PANEL_ROOT_ID} .maw-update-install[hidden]{display:none}
#${PANEL_ROOT_ID} .maw-grid{margin-bottom:10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
#${PANEL_ROOT_ID} .maw-chip{min-width:0;padding:9px 10px;border:1px solid var(--maw-border);border-radius:8px;background:#fff}
#${PANEL_ROOT_ID} .maw-label{margin-bottom:4px;color:#8a8a8a;font-size:10px;font-weight:650;letter-spacing:.07em;text-transform:uppercase}
#${PANEL_ROOT_ID} .maw-value{overflow:hidden;color:#171717;font-size:13px;font-weight:700;line-height:1.3;text-overflow:ellipsis;white-space:nowrap}
#${PANEL_ROOT_ID} .maw-value[data-tone='warning']{color:var(--maw-warning)}
#${PANEL_ROOT_ID} .maw-value[data-tone='danger']{color:var(--maw-danger)}
#${PANEL_ROOT_ID} .maw-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:8px}
#${PANEL_ROOT_ID} .maw-actions>button{width:100%;min-width:0;height:auto;min-height:40px;padding-top:8px;padding-bottom:8px;line-height:1.25;white-space:normal}
#${PANEL_ROOT_ID} .maw-actions>button>span{min-width:0;overflow-wrap:anywhere}
#${PANEL_ROOT_ID} .maw-actions .maw-wide{grid-column:1/-1}
#${PANEL_ROOT_ID} .maw-primary{border:1px solid var(--maw-primary);background:var(--maw-primary);color:var(--maw-primary-fg);box-shadow:0 1px 2px rgba(0,0,0,.08)}
#${PANEL_ROOT_ID} .maw-primary:hover{border-color:#303030;background:#303030}
#${PANEL_ROOT_ID} .maw-secondary,.maw-modal .maw-secondary{border:1px solid var(--maw-input,#dedede);background:#fff;color:#171717;box-shadow:0 1px 2px rgba(0,0,0,.04)}
#${PANEL_ROOT_ID} .maw-secondary:hover,.maw-modal .maw-secondary:hover{border-color:#cfcfcf;background:#f7f7f7}
#${PANEL_ROOT_ID} .maw-warning{border:1px solid #eed69a;background:#fff;color:#8a5a00}
#${PANEL_ROOT_ID} .maw-warning:hover{border-color:#dfc26f;background:#fffaf0}
#${PANEL_ROOT_ID} .maw-help{margin-top:11px;padding:9px 10px;border:1px solid var(--maw-border);border-radius:8px;background:#fafafa;color:#737373;font-size:11px;line-height:1.45}
#${PANEL_ROOT_ID} .maw-key{padding:2px 5px;border:1px solid #d7d7d7;border-bottom-width:2px;border-radius:4px;background:#fff;color:#404040;font:650 10.5px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace}
#${PANEL_ROOT_ID} .maw-footer{margin-top:12px;padding-top:11px;display:flex;align-items:center;justify-content:space-between;gap:10px;border-top:1px solid var(--maw-border);font-size:11.5px}
#${PANEL_ROOT_ID} .maw-footer-tools{display:flex;align-items:center;justify-content:flex-end;gap:7px;min-width:0}
#${PANEL_ROOT_ID} .maw-footer a{display:inline-flex;align-items:center;gap:5px;color:#525252;font-weight:600;text-decoration:none}
#${PANEL_ROOT_ID} .maw-footer a:hover{color:#171717;text-decoration:underline;text-underline-offset:3px}
#${PANEL_ROOT_ID} .maw-collapsed-tab{width:62px;height:52px;min-height:52px;padding:0;border:1px solid #d7d7d7;border-right:0;border-radius:10px 0 0 10px;background:#fff;color:#171717;box-shadow:0 10px 26px rgba(0,0,0,.17)}
#${PANEL_ROOT_ID} .maw-collapsed-logo{display:block;width:43px;height:auto;object-fit:contain}
#${PANEL_ROOT_ID}.is-collapsed{right:0;top:148px;width:62px}
#${TOAST_ROOT_ID}{position:fixed;right:18px;bottom:18px;z-index:2147483647;max-width:390px;display:flex;flex-direction:column;gap:8px;font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;pointer-events:none}
.maw-toast{opacity:0;transform:translateY(8px);padding:11px 13px;border:1px solid rgba(255,255,255,.13);border-radius:8px;background:#262626;color:#fff;box-shadow:0 12px 28px rgba(15,23,42,.2);font-size:12.5px;line-height:1.45;white-space:pre-line;transition:opacity .2s ease,transform .2s ease}
.maw-toast.is-visible{opacity:1;transform:translateY(0)}.maw-toast.success{background:#276749}.maw-toast.warning{background:#8a5a00}.maw-toast.error{background:#b91c1c}
.maw-modal-backdrop{position:fixed;inset:0;z-index:2147483646;padding:6vh 18px 24px;display:flex;align-items:flex-start;justify-content:center;overflow-y:auto;background:rgba(0,0,0,.30);font-family:Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#171717}
.maw-modal{--maw-border:#e7e7e7;--maw-muted:#f7f7f7;--maw-input:#dedede;--maw-primary:#1f1f1f;width:min(820px,calc(100vw - 32px));max-height:88vh;display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--maw-border);border-radius:12px;background:#fff;box-shadow:0 25px 50px -12px rgba(0,0,0,.26)}
.maw-modal-head{min-height:64px;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--maw-border);background:#fff}
.maw-modal-logo{width:43px}.maw-modal-title{margin:0;font-size:15px;font-weight:700;letter-spacing:-.015em}.maw-modal-subtitle{margin-top:3px;color:#737373;font-size:11.5px;line-height:1.35}
.maw-modal-body{padding:18px;overflow:auto;background:#f7f7f7}
.maw-modal-note{margin-bottom:12px;padding:11px 12px;border:1px solid #e7e7e7;border-radius:8px;background:#fff;color:#525252;font-size:12px;line-height:1.5}
.maw-modal-note code{padding:2px 5px;border:1px solid #e1e1e1;border-radius:4px;background:#f5f5f5;color:#262626;font:11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace}
.maw-rule-form-card,.maw-rule-list-card{overflow:hidden;border:1px solid #d9d9d9;border-radius:9px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.04)}
.maw-rule-list-card{margin-top:12px}
.maw-section-head{min-height:43px;padding:8px 11px;display:flex;align-items:center;justify-content:space-between;gap:10px;border-bottom:1px solid #e7e7e7;background:#fafafa}
.maw-section-title{margin:0;color:#262626;font-size:12.5px;font-weight:700}.maw-section-count{color:#737373;font-size:11.5px;font-weight:650}
.maw-rule-form{padding:13px}.maw-form-grid{display:grid;grid-template-columns:minmax(220px,1fr) minmax(280px,1.15fr) auto;gap:10px;align-items:end}
.maw-field{min-width:0;display:grid;gap:5px}.maw-field-label{color:#404040;font-size:11.5px;font-weight:700}
.maw-rule-input,.maw-rule-select,.maw-rule-search{width:100%;height:40px;padding:0 11px;border:1px solid #d9d9d9;border-radius:7px;outline:none;background:#fff;color:#171717;font:12.5px/1.3 Inter,system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;transition:border-color .15s ease,box-shadow .15s ease}
.maw-rule-input:focus,.maw-rule-select:focus,.maw-rule-search:focus{border-color:#525252;box-shadow:0 0 0 3px rgba(23,23,23,.09)}
.maw-rule-submit{height:40px;min-height:40px!important}.maw-rule-form-meta{min-height:27px;margin-top:9px;display:flex;align-items:center;gap:10px}
.maw-match-help{min-width:0;flex:1;color:#737373;font-size:11.5px;line-height:1.4}.maw-form-feedback{color:#737373;font-size:11.5px;font-weight:650}.maw-form-feedback.is-success{color:#276749}.maw-form-feedback.is-error{color:#b91c1c}
.maw-cancel-edit{min-height:28px!important;height:28px;padding:0 8px!important;border:0!important;background:transparent!important;color:#737373!important;font-size:11.5px!important}.maw-cancel-edit[hidden]{display:none!important}.maw-cancel-edit:hover{color:#171717!important;text-decoration:underline}
.maw-rule-search-wrap{width:min(230px,48%)}.maw-rule-search{height:32px;padding:0 9px;font-size:11.5px}
.maw-rule-list{max-height:320px;overflow:auto}.maw-rule-row{min-height:55px;padding:9px 10px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:10px;border-bottom:1px solid #ededed}.maw-rule-row:last-child{border-bottom:0}
.maw-rule-badge{min-width:66px;padding:4px 7px;border:1px solid #dedede;border-radius:999px;background:#f7f7f7;color:#525252;font-size:10px;font-weight:750;text-align:center;white-space:nowrap}.maw-rule-badge[data-type='exact']{border-color:#eed69a;background:#fff8ed;color:#8a5a00}.maw-rule-badge[data-type='regex']{border-color:#d6d6d6;background:#f0f0f0;color:#262626}
.maw-rule-value{min-width:0;overflow:hidden;color:#262626;font:12.5px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.maw-rule-actions{display:flex;align-items:center;gap:5px}
.maw-row-btn{width:30px!important;min-width:30px!important;height:30px!important;min-height:30px!important;padding:0!important;border:1px solid #e1e1e1!important;border-radius:6px!important;background:#fff!important;color:#737373!important}.maw-row-btn:hover{border-color:#cfcfcf!important;background:#f7f7f7!important;color:#171717!important}.maw-row-btn.is-danger:hover{border-color:#f1c0c0!important;background:#fff1f1!important;color:#b91c1c!important}
.maw-rule-empty{min-height:120px;padding:24px;display:grid;place-items:center;color:#737373;font-size:12px;text-align:center}
.maw-modal-footer{padding:12px 16px;display:flex;align-items:center;justify-content:flex-end;gap:8px;border-top:1px solid var(--maw-border);background:#fafafa}
.maw-modal-status{margin-right:auto;color:#737373;font-size:11.5px}.maw-modal-status.is-dirty{color:#8a5a00}.maw-modal-status.is-saved{color:#276749}
.maw-modal .maw-primary{border:1px solid #1f1f1f;background:#1f1f1f;color:#fafafa}.maw-modal .maw-primary:hover{background:#303030;border-color:#303030}
@media(max-width:760px){#${PANEL_ROOT_ID}{top:auto;right:12px;bottom:12px;left:12px;width:auto}#${PANEL_ROOT_ID}.is-collapsed{top:auto;right:0;bottom:88px;left:auto;width:62px}#${PANEL_ROOT_ID} .maw-card{max-height:calc(100dvh - 24px)}#${PANEL_ROOT_ID} .maw-head{position:sticky;top:0;z-index:2}#${PANEL_ROOT_ID} .maw-grid,#${PANEL_ROOT_ID} .maw-actions{grid-template-columns:1fr}#${PANEL_ROOT_ID} .maw-actions .maw-wide{grid-column:auto}#${TOAST_ROOT_ID}{right:12px;bottom:12px;left:auto;width:min(320px,calc(100vw - 24px));max-width:none}#${TOAST_ROOT_ID} .maw-toast{width:100%}#${TOAST_ROOT_ID} .maw-toast:nth-last-child(n+3){display:none}.maw-modal-backdrop{padding:10px}.maw-modal{width:100%;max-height:calc(100dvh - 20px)}.maw-modal-body{padding:12px}.maw-form-grid{grid-template-columns:1fr}.maw-rule-input,.maw-rule-select,.maw-rule-search{height:44px;font-size:16px}.maw-rule-submit{width:100%;height:44px;min-height:44px!important}.maw-rule-form-meta{align-items:flex-start;flex-wrap:wrap}.maw-cancel-edit{margin-left:auto}.maw-section-head{align-items:flex-start;flex-wrap:wrap}.maw-rule-search-wrap{width:100%}.maw-rule-list{max-height:42vh}.maw-rule-row{grid-template-columns:1fr auto;gap:7px}.maw-rule-badge{grid-column:1;justify-self:start}.maw-rule-value{grid-column:1}.maw-rule-actions{grid-column:2;grid-row:1/3}.maw-row-btn{width:38px!important;min-width:38px!important;height:38px!important;min-height:38px!important}.maw-modal-footer{flex-wrap:wrap}.maw-modal-footer>button{flex:1}.maw-modal-status{width:100%;margin:0 0 4px}}
`);
const ICON_PATHS = {
shield: '',
power: '',
check: '',
layers: '',
edit: '',
refresh: '',
chevronRight: '',
chevronLeft: '',
close: '',
};
function uiIcon(name) {
return ``;
}
function notify(type, message, duration = 3400) {
let root = document.getElementById(TOAST_ROOT_ID);
if (!root) {
root = document.createElement('div');
root.id = TOAST_ROOT_ID;
document.documentElement.appendChild(root);
}
while (root.children.length >= 4) root.firstElementChild?.remove();
const toast = document.createElement('div');
toast.className = `maw-toast ${type || 'info'}`;
toast.setAttribute('role', type === 'error' ? 'alert' : 'status');
toast.textContent = String(message ?? '').replace(/
/gi, '\n');
root.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('is-visible'));
setTimeout(() => {
toast.classList.remove('is-visible');
setTimeout(() => toast.remove(), 220);
}, duration);
}
// ─── Helpers ──────────────────────────────────────────────────────────────
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const randInt = (lo, hi) => Math.floor(Math.random() * (hi - lo + 1)) + lo;
const HTML_ENTITY_PATTERN = /&(?:#\d+|#x[\da-f]+|[a-z][\da-z]+);/giu;
const APOSTROPHE_PATTERN = /[‘’‚‛′ʼ'`´]/g;
const decodeHtmlEntities = (() => {
const textarea = document.createElement('textarea');
return (value) => String(value ?? '').replace(HTML_ENTITY_PATTERN, (entity) => {
textarea.innerHTML = entity;
return textarea.value;
});
})();
function normalizeText(value) {
return decodeHtmlEntities(value)
.normalize('NFKC')
.replace(APOSTROPHE_PATTERN, "'")
.replace(/\s+/g, ' ')
.trim()
.toLocaleLowerCase('en-US');
}
async function waitUntil(predicate, timeoutMs, intervalMs = 200) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) return true;
await sleep(intervalMs);
}
return false;
}
function xmlGet(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
timeout: 15_000,
onload: (r) => (r.status === 200 ? resolve(r.responseText) : reject(new Error(`HTTP ${r.status}`))),
onerror: (e) => reject(e),
ontimeout: () => reject(new Error(t('requestTimeout'))),
});
});
}
// ─── Wordlist ─────────────────────────────────────────────────────────────
/**
* Load wordlist from storage; seed with defaults on first run.
* Returns a parsed array — never calls getFilteredRows recursively.
*/
function parseWordlist(raw) {
if (typeof raw !== 'string') return [];
return Array.from(new Set(raw
.split('\n')
.map((word) => word.replace('\r', '').trim())
.filter(Boolean)));
}
async function getRulesValue(key, fallback) {
try { return await GM.getValue(key, fallback); }
catch (error) { void trackTelemetryError('storage_rules_state'); throw error; }
}
async function setRulesValue(key, value) {
try { await GM.setValue(key, value); }
catch (error) { void trackTelemetryError('storage_rules_state'); throw error; }
}
async function loadWordlist() {
let raw = await getRulesValue('adWordlist', '');
if (typeof raw !== 'string' || !raw.trim()) {
raw = DEFAULT_WORDLIST;
await setRulesValue('adWordlist', raw);
}
return parseWordlist(raw);
}
function wordMatchesRow(word, rowWord) {
if (!rowWord) return false;
const decodedWord = decodeHtmlEntities(word).trim();
if (!decodedWord) return false;
const normalizedRow = normalizeText(rowWord);
if (decodedWord.startsWith('=')) {
return normalizedRow === normalizeText(decodedWord.slice(1));
}
if (decodedWord.startsWith('/') && decodedWord.endsWith('/') && decodedWord.length > 2) {
const pattern = decodedWord
.slice(1, -1)
.normalize('NFKC')
.replace(APOSTROPHE_PATTERN, "'");
try { return new RegExp(pattern, 'iu').test(normalizedRow); }
catch { return false; }
}
return normalizedRow.includes(normalizeText(decodedWord));
}
// ─── DOM helpers ──────────────────────────────────────────────────────────
function getReadableCellText(cellEl) {
if (!cellEl) return null;
const textEl = Array.from(cellEl.querySelectorAll('p, span')).find((candidate) => {
if (!candidate.textContent?.trim()) return false;
return !candidate.closest(
'[aria-hidden="true"], [hidden], .wt-table--responsive__title, '
+ '.wt-screen-reader-only, button, label, [role="button"], [role="switch"]'
);
});
return textEl?.textContent?.trim() || null;
}
/** Extract keyword text from a table row */
function getRowWord(rowEl) {
const wordCell = rowEl.querySelector('th.wt-table__row__cell');
return getReadableCellText(wordCell);
}
/** All keyword rows currently in the DOM */
function getAllRows() {
return Array.from(document.querySelectorAll(KEYWORD_ROW_SELECTOR)).filter((row) => {
const style = window.getComputedStyle(row);
return !row.hidden && row.getAttribute('aria-hidden') !== 'true' &&
style.display !== 'none' && style.visibility !== 'hidden';
});
}
function waitForKeywordRows(timeoutMs = INITIAL_ROWS_TIMEOUT_MS) {
return waitUntil(() => {
const rows = getAllRows();
return rows.length > 0 && rows.every((row) => Boolean(getRowWord(row)));
}, timeoutMs);
}
/** Rows whose keyword matches the current wordlist */
async function getFilteredRows() {
const wordlist = await loadWordlist();
const rows = getAllRows();
const rowWords = rows.map(getRowWord);
if (rowWords.some((word) => !word)) {
throw adsOperationError(t('keywordRowsFailed'), 'selector_ads_table');
}
return rows.filter((row, index) => wordlist.some((word) => wordMatchesRow(word, rowWords[index])));
}
// ─── ROAS colouring ───────────────────────────────────────────────────────
function getMetricValue(row, columnNames) {
const table = row.closest('table');
if (!table) return 0;
const normalizedNames = (Array.isArray(columnNames) ? columnNames : [columnNames])
.map(normalizeText);
const semanticCell = Array.from(row.querySelectorAll(':scope > td')).find(
(cell) => normalizedNames.includes(normalizeText(
cell.querySelector(':scope > .wt-table--responsive__title')?.textContent
))
);
const headers = Array.from(table.querySelectorAll('thead th'));
const columnIndex = headers.findIndex(
(header) => normalizedNames.includes(normalizeText(header.textContent))
);
const cell = semanticCell || (columnIndex >= 0 ? row.children[columnIndex] : null);
if (!cell) return 0;
const rawValue = getReadableCellText(cell) ?? '';
const integerText = rawValue.replace(/[^\d-]/g, '');
return integerText ? Number(integerText) : 0;
}
function isHighClickRatioRow(row) {
const clicks = getMetricValue(row, ['Clicks', 'Click', 'Tıklamalar', 'Tıklama']);
const orders = getMetricValue(row, ['Orders', 'Order', 'Siparişler', 'Sipariş']);
return (orders > 0 && clicks / orders > 25) || (orders === 0 && clicks > 25);
}
function setManagedRowColor(row, color, accent) {
row.style.backgroundColor = color;
row.style.boxShadow = `inset 3px 0 ${accent}`;
row.dataset.adWordlistColor = 'true';
}
function clearManagedRowColors() {
getAllRows().forEach((row) => {
if (row.dataset.adWordlistColor === 'true') {
row.style.removeProperty('background-color');
row.style.removeProperty('box-shadow');
delete row.dataset.adWordlistColor;
}
});
}
function colorRoas() {
getAllRows().forEach((row) => {
if (isHighClickRatioRow(row)) setManagedRowColor(row, '#fff1f1', '#b91c1c');
});
}
async function decorateRows() {
clearManagedRowColors();
const filteredRows = await getFilteredRows();
filteredRows.forEach((row) => setManagedRowColor(row, '#fff8ed', '#8a5a00'));
colorRoas();
updatePanelStats(filteredRows);
return filteredRows;
}
// ─── Makaytron control panel ──────────────────────────────────────────────
let panelEl = null;
let panelCollapsed = false;
let languageChangePromise = null;
let panelStatusModel = { messageKey: 'keywordTableWaiting', state: 'ready', titleKey: '', params: {}, literal: false };
function renderPanelStatus() {
if (!panelEl) return;
const textEl = panelEl.querySelector('[data-panel-status-text]');
const titleEl = panelEl.querySelector('[data-panel-status-title]');
const pillEl = panelEl.querySelector('[data-panel-state]');
const labelKeys = { ready: 'ready', running: 'running', success: 'completed', warning: 'warning', error: 'error' };
const label = t(labelKeys[panelStatusModel.state] || 'ready');
const message = panelStatusModel.literal
? String(panelStatusModel.messageKey || t('ready'))
: t(panelStatusModel.messageKey || 'ready', panelStatusModel.params);
if (textEl) textEl.textContent = message;
if (titleEl) titleEl.textContent = panelStatusModel.titleKey
? t(panelStatusModel.titleKey, panelStatusModel.params)
: label;
if (pillEl) {
pillEl.dataset.state = panelStatusModel.state;
pillEl.textContent = label;
}
}
function setPanelStatus(messageKey, state = 'ready', titleKey = '', params = {}, literal = false) {
panelStatusModel = { messageKey, state, titleKey, params, literal };
renderPanelStatus();
}
function applyPanelLanguage() {
if (!panelEl) return;
const setText = (selector, value) => {
const element = panelEl.querySelector(selector);
if (element) element.textContent = value;
};
panelEl.setAttribute('aria-label', t('panelAria'));
const logoLink = panelEl.querySelector('.maw-logo-shell');
if (logoLink) logoLink.setAttribute('aria-label', t('logoOpen'));
setText('.maw-sub', t('subtitle'));
setText('[data-stat-label="page"]', t('statPage'));
setText('[data-stat-label="rows"]', t('statRows'));
setText('[data-stat-label="matches"]', t('statMatches'));
setText('[data-stat-label="high"]', t('statHighRatio'));
setText('[data-action-label="close-page"]', t('closeCurrentPage'));
setText('[data-action-label="open-page"]', t('openCurrentPage'));
setText('[data-action-label="close-all"]', t('closeAllPages'));
setText('[data-action-label="edit"]', t('editWordlist'));
setText('[data-action-label="update"]', t('updateWordlist'));
setText('[data-current-page-label]', t('currentPage'));
setText('[data-all-pages-label]', t('allPages'));
setText('[data-safe-mode]', t('safeMatchMode'));
const collapseButton = panelEl.querySelector('[data-collapse]');
if (collapseButton) {
collapseButton.setAttribute('aria-label', t('collapsePanel'));
collapseButton.title = t('collapsePanel');
}
const expandButton = panelEl.querySelector('[data-expand]');
if (expandButton) {
expandButton.setAttribute('aria-label', t('expandPanel'));
expandButton.title = t('expandPanel');
}
const languageButton = panelEl.querySelector('[data-language-toggle]');
if (languageButton) {
languageButton.textContent = currentLanguage === 'tr' ? 'EN' : 'TR';
languageButton.setAttribute('aria-label', t('switchLanguage'));
languageButton.title = t('switchLanguage');
}
const updateCheckButton = panelEl.querySelector('[data-script-update-check]');
if (updateCheckButton) {
updateCheckButton.setAttribute('aria-label', t('checkScriptUpdate'));
updateCheckButton.title = t('checkScriptUpdate');
}
const updateInstallButton = panelEl.querySelector('[data-script-update-install]');
if (updateInstallButton) updateInstallButton.textContent = t('scriptUpdateInstall');
renderPanelStatus();
renderScriptUpdateStatus();
}
function switchLanguage() {
if (languageChangePromise) return languageChangePromise;
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return null;
}
const previousLanguage = currentLanguage;
const nextLanguage = currentLanguage === 'tr' ? 'en' : 'tr';
const languageButton = panelEl?.querySelector('[data-language-toggle]');
if (languageButton) languageButton.disabled = true;
languageChangePromise = GM.setValue(LANGUAGE_KEY, nextLanguage)
.then(() => {
currentLanguage = nextLanguage;
applyPanelLanguage();
applyEditorLanguage();
registerMenuCommands();
setPanelStatus('languageChanged', 'success');
notify('success', t('languageChanged'));
})
.catch((error) => {
currentLanguage = previousLanguage;
applyPanelLanguage();
notify('error', t('languageChangeFailed', { error: error?.message || error }));
})
.finally(() => {
languageChangePromise = null;
const currentButton = panelEl?.querySelector('[data-language-toggle]');
if (currentButton) currentButton.disabled = false;
});
return languageChangePromise;
}
function setPanelBusy(busy) {
panelEl?.querySelectorAll('[data-busy-action]').forEach((button) => {
button.disabled = Boolean(busy);
});
}
function updatePanelStats(filteredRows = []) {
if (!panelEl) return;
const rows = getAllRows();
const highCount = rows.filter(isHighClickRatioRow).length;
const currentPage = document.querySelector(
`${KEYWORD_ACCORDION_SELECTOR} nav button[aria-current="true"]`
)?.textContent?.trim() || '—';
const values = {
page: currentPage,
rows: String(rows.length),
matches: String(filteredRows.length),
high: String(highCount),
};
Object.entries(values).forEach(([key, value]) => {
const element = panelEl.querySelector(`[data-stat="${key}"]`);
if (element) element.textContent = value;
});
const matchEl = panelEl.querySelector('[data-stat="matches"]');
const highEl = panelEl.querySelector('[data-stat="high"]');
if (matchEl) matchEl.dataset.tone = filteredRows.length ? 'warning' : '';
if (highEl) highEl.dataset.tone = highCount ? 'danger' : '';
}
function renderPanelVisibility() {
if (!panelEl) return;
panelEl.classList.toggle('is-collapsed', panelCollapsed);
const card = panelEl.querySelector('.maw-card');
const launcher = panelEl.querySelector('.maw-collapsed-tab');
if (card) card.hidden = panelCollapsed;
if (launcher) launcher.hidden = !panelCollapsed;
}
async function setPanelCollapsed(collapsed) {
panelCollapsed = Boolean(collapsed);
renderPanelVisibility();
try { await GM.setValue(PANEL_COLLAPSED_KEY, panelCollapsed); }
catch { /* Panel state persistence is optional. */ }
if (!panelCollapsed && panelEl?.isConnected) telemetryPanelOpened();
}
async function mountMakaytronPanel() {
if (panelEl?.isConnected) return panelEl;
await loadLanguage();
document.getElementById(PANEL_ROOT_ID)?.remove();
try { panelCollapsed = Boolean(await GM.getValue(PANEL_COLLAPSED_KEY, false)); }
catch { panelCollapsed = false; }
panelEl = document.createElement('section');
panelEl.id = PANEL_ROOT_ID;
panelEl.setAttribute('aria-label', t('panelAria'));
panelEl.innerHTML = `
${uiIcon('shield')}
${t('ready')}
${t('keywordTableWaiting')}
Ctrl Space ${t('currentPage')} · Ctrl Alt K ${t('allPages')}
`;
document.documentElement.appendChild(panelEl);
renderPanelVisibility();
if (!panelCollapsed) telemetryPanelOpened();
applyPanelLanguage();
panelEl.querySelector('[data-collapse]')?.addEventListener('click', () => { void setPanelCollapsed(true); });
panelEl.querySelector('[data-expand]')?.addEventListener('click', () => { void setPanelCollapsed(false); });
panelEl.querySelector('[data-language-toggle]')?.addEventListener('click', () => { void switchLanguage(); });
panelEl.querySelector('[data-script-update-check]')?.addEventListener('click', () => { void checkScriptUpdate({ manual: true }); });
panelEl.querySelector('[data-script-update-install]')?.addEventListener('click', () => { openScriptInstallPage(); });
panelEl.querySelector('[data-action="close-page"]')?.addEventListener('click', () => { void runToggleRows(false); });
panelEl.querySelector('[data-action="open-page"]')?.addEventListener('click', () => { void runToggleRows(true); });
panelEl.querySelector('[data-action="close-all"]')?.addEventListener('click', () => { void requestDisableAllPages(); });
panelEl.querySelector('[data-action="edit"]')?.addEventListener('click', () => { void openWordlistEditor(); });
panelEl.querySelector('[data-action="update"]')?.addEventListener('click', () => { void ensureWord(); });
return panelEl;
}
// ─── Core actions ─────────────────────────────────────────────────────────
let activeOperation = null;
function adsOperationError(message, telemetryCode) {
const error = new Error(message);
error.telemetryCode = telemetryCode;
return error;
}
function adsExpectedOperationError(message) {
const error = new Error(message);
error.telemetryExpected = true;
return error;
}
function checkboxLifecyclePending(checkbox, row) {
const truthyAttribute = (element, name) => {
const value = element?.getAttribute?.(name);
return value === '' || value === 'true' || value === '1';
};
if (!checkbox || checkbox.disabled || truthyAttribute(checkbox, 'aria-busy')
|| truthyAttribute(checkbox, 'aria-disabled') || truthyAttribute(checkbox, 'data-loading')
|| truthyAttribute(checkbox, 'data-pending')) return true;
if (!row) return false;
if (truthyAttribute(row, 'aria-busy') || truthyAttribute(row, 'data-loading')
|| truthyAttribute(row, 'data-pending')) return true;
if (row.querySelector('[aria-busy="true"],[data-loading="true"],[data-pending="true"],[role="progressbar"],.wt-spinner')) {
return true;
}
const lifecycleClasses = `${checkbox.className || ''} ${row.className || ''}`.toLocaleLowerCase('en-US');
return /(?:^|[\s_-])(loading|pending|saving|updating|spinner)(?:$|[\s_-])/.test(lifecycleClasses);
}
async function waitForStableCheckboxState(checkbox, targetState, activate, options = {}) {
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 4_000;
const acknowledgedStableMs = Number.isFinite(options.acknowledgedStableMs)
? options.acknowledgedStableMs : 300;
const unacknowledgedStableMs = Number.isFinite(options.unacknowledgedStableMs)
? options.unacknowledgedStableMs : 2_100;
const row = checkbox?.closest('tr,[role="row"]') || checkbox?.parentElement || null;
const observedRoot = row || checkbox;
const deadline = Date.now() + timeoutMs;
let lastState = Boolean(checkbox?.checked);
let lastPending = checkboxLifecyclePending(checkbox, row);
let sawPendingLifecycle = lastPending;
let sawTargetState = lastState === targetState;
let lastObservedChangeAt = Date.now();
const noteActivity = () => { lastObservedChangeAt = Date.now(); };
const observer = typeof MutationObserver === 'function' && observedRoot
? new MutationObserver(noteActivity)
: null;
observer?.observe(observedRoot, {
attributes: true,
childList: true,
subtree: true,
});
checkbox?.addEventListener('input', noteActivity);
checkbox?.addEventListener('change', noteActivity);
try {
activate();
while (Date.now() < deadline) {
if (!checkbox.isConnected) return false;
const state = Boolean(checkbox.checked);
const pending = checkboxLifecyclePending(checkbox, row);
if (state !== lastState || pending !== lastPending) {
lastObservedChangeAt = Date.now();
lastState = state;
lastPending = pending;
}
if (pending) sawPendingLifecycle = true;
if (state === targetState) sawTargetState = true;
else if (sawTargetState && !pending) return false;
if (state === targetState && !pending) {
const requiredStableMs = sawPendingLifecycle
? acknowledgedStableMs
: unacknowledgedStableMs;
if (Date.now() - lastObservedChangeAt >= requiredStableMs) return true;
}
await sleep(50);
}
return false;
} finally {
observer?.disconnect();
checkbox?.removeEventListener('input', noteActivity);
checkbox?.removeEventListener('change', noteActivity);
}
}
async function toggleCurrentPage(targetState) {
const filteredRows = await getFilteredRows();
const stateLabel = t(targetState ? 'enabled' : 'disabled');
let count = 0;
for (const row of filteredRows) {
const checkbox = row.querySelector('input[type=checkbox]');
if (!checkbox) {
void trackTelemetryError('selector_ads_control');
throw adsOperationError(t('keywordChangeFailed', {
keyword: getRowWord(row) || t('keywordFallback'),
}), 'selector_ads_control');
}
if (checkbox.disabled || checkbox.checked === targetState) continue;
const changed = await waitForStableCheckboxState(
checkbox,
targetState,
() => checkbox.click()
);
if (!changed) {
throw adsOperationError(t('keywordChangeFailed', {
keyword: getRowWord(row) || t('keywordFallback'),
}), 'selector_ads_verification');
}
void trackTelemetry('ads_keyword_change_applied');
notify('success', t('keywordChanged', {
keyword: getRowWord(row) || t('keywordFallback'),
state: stateLabel,
}));
count++;
}
return count;
}
function capturePageState() {
const nav = document.querySelector(`${KEYWORD_ACCORDION_SELECTOR} nav`);
const currentPage = nav?.querySelector('button[aria-current="true"]')?.textContent?.trim() || '';
const rows = getAllRows();
const words = rows.map(getRowWord).filter(Boolean);
return {
currentPage,
rows,
wordsSignature: words.join('\u001f'),
signature: `${currentPage}|${words.length}|${words.join('\u001f')}`,
};
}
function getNextButton() {
const nav = document.querySelector(`${KEYWORD_ACCORDION_SELECTOR} nav`);
if (!nav) return null;
const buttons = Array.from(nav.querySelectorAll('button'));
const namedNext = buttons.find(
(button) => normalizeText(button.querySelector('.wt-screen-reader-only')?.textContent) === 'next'
);
const lastButton = buttons.at(-1) || null;
return namedNext || (lastButton?.querySelector('.wt-screen-reader-only') ? lastButton : null);
}
function isButtonDisabled(button) {
return !button || button.disabled || button.getAttribute('aria-disabled') === 'true';
}
async function goToNextPage(previousState) {
const nextButton = getNextButton();
if (!nextButton) {
throw adsOperationError(t('nextButtonMissing'), 'selector_ads_pagination');
}
if (isButtonDisabled(nextButton)) return 'end';
nextButton.click();
const changed = await waitUntil(
() => {
const currentState = capturePageState();
const pageChanged = previousState.currentPage
? Boolean(currentState.currentPage && currentState.currentPage !== previousState.currentPage)
: true;
const rowsReplaced = previousState.rows.every((row) => !row.isConnected);
const wordsChanged = currentState.wordsSignature !== previousState.wordsSignature;
return currentState.rows.length > 0 && pageChanged && (rowsReplaced || wordsChanged);
},
PAGE_CHANGE_TIMEOUT_MS
);
if (!changed) {
throw adsOperationError(t('nextPageTimeout'), 'selector_ads_pagination');
}
await sleep(250);
return 'moved';
}
/**
* Toggle filtered rows on/off.
* @param {boolean} targetState true = enable, false = disable
* @param {boolean} autoPaginate navigate to next page when done
*/
async function toggleRows(targetState, autoPaginate = false) {
const stateLabel = t(targetState ? 'enabled' : 'disabled');
const visitedPages = new Set();
let totalCount = 0;
let pageCount = 0;
while (true) {
const rowsReady = await waitForKeywordRows();
if (!rowsReady) throw adsOperationError(t('keywordRowsFailed'), 'selector_ads_table');
const pageState = capturePageState();
if (visitedPages.has(pageState.signature)) {
throw adsOperationError(t('repeatedPage'), 'selector_ads_pagination');
}
visitedPages.add(pageState.signature);
pageCount++;
setPanelStatus(
'processingPage',
'running',
'processingKeywords',
{ page: pageState.currentPage || pageCount }
);
totalCount += await toggleCurrentPage(targetState);
await decorateRows();
if (!autoPaginate) break;
if (pageCount >= MAX_AUTO_PAGES && !isButtonDisabled(getNextButton())) {
throw adsExpectedOperationError(t('maxPagesReached', { count: MAX_AUTO_PAGES }));
}
const navigation = await goToNextPage(pageState);
if (navigation === 'end') {
break;
}
}
const resultParams = { keywords: quantity(totalCount, 'keyword'), state: stateLabel };
notify('success', t('totalChanged', resultParams));
setPanelStatus('totalChanged', 'success', '', resultParams);
}
function runToggleRows(targetState, autoPaginate = false) {
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return activeOperation;
}
if (document.getElementById(EDITOR_MODAL_ID)) {
notify('warning', t('editorOperationBlocked'));
return null;
}
setPanelBusy(true);
setPanelStatus(autoPaginate ? 'allPagesWillProcess' : 'currentPageProcessing', 'running');
activeOperation = toggleRows(targetState, autoPaginate)
.catch((error) => {
if (!error?.telemetryExpected) {
const telemetryCode = TELEMETRY_ALLOWED_ERROR_CODES.has(error?.telemetryCode) ? error.telemetryCode : 'runtime_keyword_change';
void trackTelemetryError(telemetryCode);
}
console.error('[Makaytron Etsy Ads Keyword Manager]', t('consoleOperationFailed'), error);
notify('error', t('operationStopped', { error: error?.message || error }));
setPanelStatus(error?.message || t('operationStoppedFallback'), 'error', '', {}, true);
})
.finally(() => {
activeOperation = null;
setPanelBusy(false);
});
return activeOperation;
}
function requestDisableAllPages() {
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return activeOperation;
}
if (document.getElementById(EDITOR_MODAL_ID)) {
notify('warning', t('editorOperationBlocked'));
return null;
}
if (!window.confirm(t('confirmAllPages'))) return null;
return runToggleRows(false, true);
}
function requestShortcutDisableCurrentPage() {
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return activeOperation;
}
if (document.getElementById(EDITOR_MODAL_ID)) {
notify('warning', t('editorOperationBlocked'));
return null;
}
if (!window.confirm(t('confirmCurrentPageShortcut'))) return null;
return runToggleRows(false, false);
}
// ─── Remote wordlist update ───────────────────────────────────────────────
let wordlistUpdateRunning = false;
async function ensureWord() {
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return;
}
if (document.getElementById(EDITOR_MODAL_ID)) {
notify('warning', t('editorOperationBlocked'));
return;
}
if (!window.confirm(t('confirmListUpdate'))) return;
wordlistUpdateRunning = true;
setPanelBusy(true);
setPanelStatus('remoteChecking', 'running', 'listUpdating');
try {
let text;
let words;
try {
text = await xmlGet(`${WORD_LIST_URL}?t=${Date.now()}`);
const byteLength = new TextEncoder().encode(text).length;
words = parseWordlist(text);
if (!words.length) throw new Error(t('remoteListEmpty'));
if (byteLength > MAX_WORDLIST_BYTES) throw new Error(t('remoteListTooLarge'));
} catch (error) {
void trackTelemetryError('network_rules_update');
throw error;
}
notify('success', t('fetchingKeywords'));
try {
const currentValue = await GM.getValue('adWordlist', DEFAULT_WORDLIST);
await GM.setValue(WORDLIST_BACKUP_KEY, typeof currentValue === 'string' ? currentValue : DEFAULT_WORDLIST);
await GM.setValue('adWordlist', text);
} catch (error) {
void trackTelemetryError('storage_rules_state');
throw error;
}
await decorateRows();
const updateParams = { keywords: quantity(words.length, 'keyword') };
notify('success', t('keywordsUpdated', updateParams));
setPanelStatus('keywordsUpdatedSuccess', 'success', '', updateParams);
} catch (error) {
console.error('[Makaytron Etsy Ads Keyword Manager]', t('consoleListFailed'), error);
notify('error', t('listFetchFailed', { error: error?.message || error }));
setPanelStatus(error?.message || t('listFetchFailedFallback'), 'error', '', {}, true);
} finally {
wordlistUpdateRunning = false;
setPanelBusy(false);
}
}
async function restoreWordlistBackup() {
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return;
}
if (document.getElementById(EDITOR_MODAL_ID)) {
notify('warning', t('editorOperationBlocked'));
return;
}
const backup = await getRulesValue(WORDLIST_BACKUP_KEY, '');
if (typeof backup !== 'string' || !backup.trim()) {
notify('warning', t('backupListMissing'));
return;
}
if (!window.confirm(t('confirmListRestore'))) return;
wordlistUpdateRunning = true;
setPanelBusy(true);
setPanelStatus('listUpdating', 'running');
try {
const byteLength = new TextEncoder().encode(backup).length;
const rules = parseWordlist(backup);
if (!rules.length || byteLength > MAX_WORDLIST_BYTES) throw new Error(t('localListTooLarge'));
await setRulesValue('adWordlist', backup);
const filteredRows = await decorateRows();
const restoreParams = {
rules: quantity(rules.length, 'rule'),
matches: quantity(filteredRows.length, 'match'),
};
notify('success', t('backupRestored'));
setPanelStatus('backupRestoredStatus', 'success', '', restoreParams);
} catch (error) {
notify('error', t('backupRestoreFailed', { error: error?.message || error }));
setPanelStatus('backupRestoreFailed', 'error', '', { error: error?.message || error });
} finally {
wordlistUpdateRunning = false;
setPanelBusy(false);
}
}
// ─── Script version update ───────────────────────────────────────────────
let scriptUpdateCheckRunning = false;
let scriptUpdateAutoScheduled = false;
let scriptUpdateStatusModel = {
state: 'idle',
messageKey: '',
params: {},
latestVersion: '',
};
function parseSemVer(version) {
const match = String(version || '').trim().match(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
);
if (!match) return null;
return {
core: match.slice(1, 4).map(Number),
prerelease: match[4] ? match[4].split('.') : [],
};
}
function compareSemVer(leftVersion, rightVersion) {
const left = parseSemVer(leftVersion);
const right = parseSemVer(rightVersion);
if (!left || !right) return null;
for (let index = 0; index < 3; index += 1) {
if (left.core[index] !== right.core[index]) {
return left.core[index] > right.core[index] ? 1 : -1;
}
}
if (!left.prerelease.length && !right.prerelease.length) return 0;
if (!left.prerelease.length) return 1;
if (!right.prerelease.length) return -1;
const length = Math.max(left.prerelease.length, right.prerelease.length);
for (let index = 0; index < length; index += 1) {
const leftPart = left.prerelease[index];
const rightPart = right.prerelease[index];
if (leftPart === undefined) return -1;
if (rightPart === undefined) return 1;
if (leftPart === rightPart) continue;
const leftNumeric = /^\d+$/.test(leftPart);
const rightNumeric = /^\d+$/.test(rightPart);
if (leftNumeric && rightNumeric) return Number(leftPart) > Number(rightPart) ? 1 : -1;
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
return leftPart > rightPart ? 1 : -1;
}
return 0;
}
function extractRemoteScriptVersion(source) {
if (typeof source !== 'string') return '';
const match = source.match(/^\/\/\s*@version\s+([^\s]+)\s*$/m);
return match && parseSemVer(match[1]) ? match[1] : '';
}
function getScriptSourceKind() {
let info = null;
try {
info = typeof GM_info === 'object' && GM_info ? GM_info : null;
} catch {
info = null;
}
const scriptInfo = info?.script || {};
const sourceUrls = [
scriptInfo.downloadURL,
scriptInfo.updateURL,
].filter((value) => typeof value === 'string' && value.trim());
if (!sourceUrls.length) return 'unknown';
if (sourceUrls.some((url) => /(?:greasyfork|sleazyfork)\.org/i.test(url))) return 'external';
try {
const canonical = new URL(SCRIPT_SOURCE_URL);
const isCanonicalSource = sourceUrls.every((value) => {
const candidate = new URL(value);
return candidate.protocol === 'https:'
&& candidate.origin === canonical.origin
&& candidate.pathname === canonical.pathname
&& !candidate.username
&& !candidate.password
&& !candidate.search
&& !candidate.hash;
});
return isCanonicalSource ? 'github' : 'external';
} catch {
return 'external';
}
}
function isCriticalKeywordWorkActive() {
return Boolean(
activeOperation
|| wordlistUpdateRunning
|| document.getElementById(EDITOR_MODAL_ID)
);
}
function renderScriptUpdateStatus() {
if (!panelEl) return;
const banner = panelEl.querySelector('[data-script-update-banner]');
const message = panelEl.querySelector('[data-script-update-message]');
const installButton = panelEl.querySelector('[data-script-update-install]');
const checkButton = panelEl.querySelector('[data-script-update-check]');
if (!banner || !message || !installButton) return;
const visible = scriptUpdateStatusModel.state !== 'idle';
banner.hidden = !visible;
banner.dataset.state = scriptUpdateStatusModel.state;
message.textContent = visible
? t(scriptUpdateStatusModel.messageKey, scriptUpdateStatusModel.params)
: '';
installButton.hidden = scriptUpdateStatusModel.state !== 'available';
installButton.textContent = t('scriptUpdateInstall');
if (checkButton) checkButton.disabled = scriptUpdateCheckRunning;
}
function setScriptUpdateStatus(state, messageKey, params = {}, latestVersion = '') {
scriptUpdateStatusModel = { state, messageKey, params, latestVersion };
renderScriptUpdateStatus();
}
async function readScriptUpdateState() {
try {
const value = await GM.getValue(SCRIPT_UPDATE_STATE_KEY, {});
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
} catch {
return {};
}
}
async function writeScriptUpdateState(value) {
try {
await GM.setValue(SCRIPT_UPDATE_STATE_KEY, value);
} catch {
// Update-check persistence is optional and must never block the main tool.
}
}
async function checkScriptUpdate({ manual = false } = {}) {
if (scriptUpdateCheckRunning) return null;
if (isCriticalKeywordWorkActive()) {
if (manual) {
setScriptUpdateStatus('blocked', 'scriptUpdateBlocked');
notify('warning', t('scriptUpdateBlocked'));
}
return null;
}
const sourceKind = getScriptSourceKind();
if (sourceKind !== 'github') {
if (manual) {
const messageKey = sourceKind === 'unknown'
? 'scriptUpdateUnknownSource'
: 'scriptUpdateExternalSource';
setScriptUpdateStatus('external', messageKey);
notify('warning', t(messageKey));
}
return null;
}
const previousState = await readScriptUpdateState();
const now = Date.now();
const lastCheckedAt = Number(previousState.lastCheckedAt) || 0;
if (!manual && now - lastCheckedAt < SCRIPT_UPDATE_INTERVAL_MS) {
const cachedVersion = typeof previousState.latestVersion === 'string'
? previousState.latestVersion
: '';
if (compareSemVer(cachedVersion, APP_VERSION) === 1) {
setScriptUpdateStatus(
'available',
'scriptUpdateAvailable',
{ version: cachedVersion },
cachedVersion
);
}
return previousState;
}
scriptUpdateCheckRunning = true;
setScriptUpdateStatus('checking', 'scriptUpdateChecking');
const checkingState = { ...previousState, lastCheckedAt: now, lastStatus: 'checking' };
await writeScriptUpdateState(checkingState);
try {
const source = await xmlGet(`${SCRIPT_SOURCE_URL}?update_check=${now}`);
if (new TextEncoder().encode(String(source)).length > MAX_REMOTE_SCRIPT_BYTES) {
throw new Error(t('scriptUpdateInvalid'));
}
const latestVersion = extractRemoteScriptVersion(source);
const comparison = compareSemVer(latestVersion, APP_VERSION);
if (!latestVersion || comparison === null) throw new Error(t('scriptUpdateInvalid'));
const available = comparison === 1;
const nextState = {
lastCheckedAt: now,
lastStatus: available ? 'available' : 'current',
latestVersion,
};
await writeScriptUpdateState(nextState);
if (available) {
setScriptUpdateStatus(
'available',
'scriptUpdateAvailable',
{ version: latestVersion },
latestVersion
);
if (manual) notify('warning', t('scriptUpdateAvailable', { version: latestVersion }));
} else {
setScriptUpdateStatus(
'current',
'scriptUpdateCurrent',
{ version: APP_VERSION },
latestVersion
);
if (manual) notify('success', t('scriptUpdateCurrent', { version: APP_VERSION }));
}
return nextState;
} catch (error) {
const errorMessage = error?.message || String(error);
await writeScriptUpdateState({
...checkingState,
lastStatus: 'error',
lastError: errorMessage,
});
setScriptUpdateStatus('error', 'scriptUpdateFailed', { error: errorMessage });
if (manual) notify('warning', t('scriptUpdateFailed', { error: errorMessage }));
else console.warn('[Makaytron Etsy Ads Keyword Manager] Update check failed:', error);
return null;
} finally {
scriptUpdateCheckRunning = false;
renderScriptUpdateStatus();
}
}
function openScriptInstallPage() {
if (isCriticalKeywordWorkActive()) {
notify('warning', t('scriptInstallBlocked'));
return;
}
const sourceKind = getScriptSourceKind();
if (sourceKind !== 'github') {
const messageKey = sourceKind === 'unknown'
? 'scriptUpdateUnknownSource'
: 'scriptUpdateExternalSource';
setScriptUpdateStatus('external', messageKey);
notify('warning', t(messageKey));
return;
}
const latestVersion = scriptUpdateStatusModel.latestVersion;
if (!latestVersion || compareSemVer(latestVersion, APP_VERSION) !== 1) return;
if (!window.confirm(t('confirmScriptInstall', { version: latestVersion }))) return;
window.open(SCRIPT_SOURCE_URL, '_blank', 'noopener,noreferrer');
notify('success', t('scriptInstallOpening', { version: latestVersion }));
}
function scheduleAutomaticScriptUpdateCheck() {
if (scriptUpdateAutoScheduled) return;
scriptUpdateAutoScheduled = true;
window.setTimeout(() => { void checkScriptUpdate(); }, 1_500);
}
// ─── Wordlist editor popup ────────────────────────────────────────────────
let editorOpenSequence = 0;
let editorCleanup = null;
let editorRelocalize = null;
function applyEditorLanguage() {
editorRelocalize?.();
}
function decodeEditorRule(rawRule, id) {
const decoded = decodeHtmlEntities(rawRule).trim();
if (decoded.startsWith('=') && decoded.length > 1) {
return { id, type: 'exact', value: decoded.slice(1).trim(), sourceRaw: rawRule, edited: false };
}
if (decoded.startsWith('/') && decoded.endsWith('/') && decoded.length > 2) {
return { id, type: 'regex', value: decoded.slice(1, -1), sourceRaw: rawRule, edited: false };
}
return { id, type: 'contains', value: decoded, sourceRaw: rawRule, edited: false };
}
function normalizeEditorRuleValue(value, type) {
const rawValue = String(value ?? '');
if (type === 'regex') {
const wrappedValue = rawValue.trim();
if (wrappedValue.startsWith('/') && wrappedValue.endsWith('/') && wrappedValue.length > 1) {
return wrappedValue.slice(1, -1);
}
return rawValue;
}
let normalized = rawValue.trim();
if (type === 'exact' && normalized.startsWith('=')) normalized = normalized.slice(1).trim();
if (type === 'contains' && normalized.startsWith('=')) normalized = normalized.slice(1).trim();
if (type === 'contains' && normalized.startsWith('/') && normalized.endsWith('/') && normalized.length > 1) {
normalized = normalized.slice(1, -1).trim();
}
return normalized;
}
function editorRuleDuplicateKey(rule) {
const type = ['contains', 'exact', 'regex'].includes(rule?.type) ? rule.type : 'contains';
const value = normalizeEditorRuleValue(rule?.value, type);
if (type === 'regex') {
return `${type}:${value.normalize('NFKC').replace(APOSTROPHE_PATTERN, "'")}`;
}
return `${type}:${normalizeText(value)}`;
}
function encodeEditorRule(rule) {
if (!rule?.edited && typeof rule?.sourceRaw === 'string' && rule.sourceRaw.trim()) return rule.sourceRaw.trim();
const value = normalizeEditorRuleValue(rule?.value, rule?.type);
if (!value) return '';
if (rule.type === 'exact') return `=${value}`;
if (rule.type === 'regex') return `/${value}/`;
return value;
}
async function openWordlistEditor() {
if (activeOperation || wordlistUpdateRunning) {
notify('warning', t('operationBusy'));
return;
}
const existingEditor = document.getElementById(EDITOR_MODAL_ID);
if (existingEditor) {
existingEditor.querySelector('[data-rule-input]')?.focus();
return;
}
const previouslyFocused = document.activeElement;
const openSequence = ++editorOpenSequence;
editorCleanup?.();
document.getElementById(EDITOR_MODAL_ID)?.remove();
const storedValue = await getRulesValue('adWordlist', DEFAULT_WORDLIST);
if (openSequence !== editorOpenSequence) return;
const initialValue = typeof storedValue === 'string' ? storedValue : DEFAULT_WORDLIST;
let nextRuleId = 1;
let rules = parseWordlist(initialValue)
.map((rawRule) => decodeEditorRule(rawRule, nextRuleId++))
.filter((rule) => rule.value);
const serializeRules = () => rules.map(encodeEditorRule).filter(Boolean).join('\n');
let savedValue = serializeRules();
let editingId = null;
let saveInProgress = false;
const backdrop = document.createElement('div');
backdrop.id = EDITOR_MODAL_ID;
backdrop.className = 'maw-modal-backdrop';
backdrop.innerHTML = `
${t('editorTitle')}
Makaytron · Etsy Ads Keyword Manager · v${APP_VERSION}
`;
document.documentElement.appendChild(backdrop);
const countEl = backdrop.querySelector('[data-editor-count]');
const statusEl = backdrop.querySelector('[data-editor-status]');
const saveButton = backdrop.querySelector('[data-save]');
const ruleForm = backdrop.querySelector('[data-rule-form]');
const ruleInput = backdrop.querySelector('[data-rule-input]');
const ruleType = backdrop.querySelector('[data-rule-type]');
const ruleValueLabel = backdrop.querySelector('[data-rule-value-label]');
const ruleSubmitLabel = backdrop.querySelector('[data-rule-submit-label]');
const ruleFormTitle = backdrop.querySelector('[data-rule-form-title]');
const matchHelp = backdrop.querySelector('[data-match-help]');
const formFeedback = backdrop.querySelector('[data-form-feedback]');
const cancelEditButton = backdrop.querySelector('[data-cancel-edit]');
const searchInput = backdrop.querySelector('[data-rule-search]');
const ruleList = backdrop.querySelector('[data-rule-list]');
const typeLabelKey = (type) => ({ contains: 'matchContains', exact: 'matchExact', regex: 'matchRegex' }[type] || 'matchContains');
const typeHelpKey = (type) => ({ contains: 'matchContainsHelp', exact: 'matchExactHelp', regex: 'matchRegexHelp' }[type] || 'matchContainsHelp');
let formFeedbackState = { key: '', tone: '' };
const setFormFeedback = (key = '', tone = '') => {
formFeedbackState = { key, tone };
formFeedback.textContent = key ? t(key) : '';
formFeedback.className = `maw-form-feedback${tone ? ` is-${tone}` : ''}`;
formFeedback.setAttribute('role', tone === 'error' ? 'alert' : 'status');
ruleInput.setAttribute('aria-invalid', tone === 'error' ? 'true' : 'false');
};
const getPreview = () => {
const encodedRules = rules.map(encodeEditorRule).filter(Boolean);
const matches = getAllRows().filter((row) => {
const rowWord = getRowWord(row);
return encodedRules.some((rule) => wordMatchesRow(rule, rowWord));
}).length;
return { matches };
};
const getDraftState = () => {
const type = ['contains', 'exact', 'regex'].includes(ruleType.value) ? ruleType.value : 'contains';
const normalizedValue = normalizeEditorRuleValue(ruleInput.value, type);
const hasInput = type === 'regex' ? ruleInput.value.length > 0 : Boolean(ruleInput.value.trim());
if (editingId === null) return { hasPendingDraft: hasInput, hasEditChanges: false };
const editingRule = rules.find((rule) => rule.id === editingId);
const hasEditChanges = !editingRule || editingRule.type !== type || editingRule.value !== normalizedValue;
return { hasPendingDraft: hasEditChanges, hasEditChanges };
};
const refreshEditorMeta = () => {
const { matches } = getPreview();
const { hasPendingDraft } = getDraftState();
const dirty = serializeRules() !== savedValue || hasPendingDraft;
countEl.textContent = quantity(rules.length, 'rule');
const statusParams = { matches: quantity(matches, 'match') };
statusEl.textContent = t(dirty ? 'editorDirty' : 'editorSaved', statusParams);
statusEl.className = `maw-modal-status ${dirty ? 'is-dirty' : 'is-saved'}`;
return { rules, matches, dirty };
};
const renderFormMode = () => {
const isEditing = editingId !== null;
const isCustomRule = ruleType.value === 'regex';
ruleFormTitle.textContent = t(isEditing ? 'editRuleTitle' : 'addRuleTitle');
ruleSubmitLabel.textContent = t(isEditing ? 'updateRule' : 'addRule');
cancelEditButton.hidden = !isEditing;
matchHelp.textContent = t(typeHelpKey(ruleType.value));
ruleValueLabel.textContent = t(isCustomRule ? 'customRuleValueLabel' : 'ruleValueLabel');
ruleInput.placeholder = t(isCustomRule ? 'customRulePlaceholder' : 'ruleValuePlaceholder');
};
const resetForm = ({ keepFeedback = false } = {}) => {
editingId = null;
ruleInput.value = '';
ruleType.value = 'contains';
if (!keepFeedback) setFormFeedback();
renderFormMode();
refreshEditorMeta();
};
const renderRuleList = () => {
const search = normalizeText(searchInput.value);
const visibleRules = rules.filter((rule) => !search || normalizeText(`${rule.value} ${t(typeLabelKey(rule.type))}`).includes(search));
ruleList.replaceChildren();
if (!visibleRules.length) {
const empty = document.createElement('div');
empty.className = 'maw-rule-empty';
empty.textContent = rules.length ? t('noSearchResults') : t('noRules');
ruleList.appendChild(empty);
refreshEditorMeta();
return;
}
visibleRules.forEach((rule) => {
const row = document.createElement('div');
row.className = 'maw-rule-row';
row.dataset.ruleId = String(rule.id);
const badge = document.createElement('span');
badge.className = 'maw-rule-badge';
badge.dataset.type = rule.type;
badge.textContent = t(typeLabelKey(rule.type));
const value = document.createElement('div');
value.className = 'maw-rule-value';
value.textContent = rule.value;
value.title = rule.value;
const actions = document.createElement('div');
actions.className = 'maw-rule-actions';
const editButton = document.createElement('button');
editButton.type = 'button';
editButton.className = 'maw-row-btn';
editButton.dataset.editRule = String(rule.id);
editButton.setAttribute('aria-label', `${t('editRule')}: ${rule.value}`);
editButton.title = t('editRule');
editButton.innerHTML = uiIcon('edit');
const removeButton = document.createElement('button');
removeButton.type = 'button';
removeButton.className = 'maw-row-btn is-danger';
removeButton.dataset.removeRule = String(rule.id);
removeButton.setAttribute('aria-label', `${t('removeRule')}: ${rule.value}`);
removeButton.title = t('removeRule');
removeButton.innerHTML = uiIcon('close');
actions.append(editButton, removeButton);
row.append(badge, value, actions);
ruleList.appendChild(row);
});
refreshEditorMeta();
};
const startEditing = (id) => {
const rule = rules.find((item) => item.id === id);
if (!rule) return;
editingId = id;
ruleInput.value = rule.value;
ruleType.value = rule.type;
setFormFeedback();
renderFormMode();
refreshEditorMeta();
ruleInput.focus();
ruleInput.select();
};
const removeRule = (id) => {
if (!rules.some((rule) => rule.id === id)) return;
rules = rules.filter((rule) => rule.id !== id);
if (editingId === id) resetForm();
setFormFeedback('ruleRemoved', 'success');
renderRuleList();
};
const submitRule = () => {
const type = ['contains', 'exact', 'regex'].includes(ruleType.value) ? ruleType.value : 'contains';
const value = normalizeEditorRuleValue(ruleInput.value, type);
if (!value) {
setFormFeedback('ruleRequired', 'error');
ruleInput.focus();
return;
}
if (type === 'regex') {
try { new RegExp(value, 'iu'); }
catch {
setFormFeedback('invalidRegex', 'error');
ruleInput.focus();
return;
}
}
const candidateKey = editorRuleDuplicateKey({ type, value });
const duplicate = rules.some((rule) => rule.id !== editingId && editorRuleDuplicateKey(rule) === candidateKey);
if (duplicate) {
setFormFeedback('duplicateRule', 'error');
ruleInput.focus();
return;
}
const wasEditing = editingId !== null;
if (wasEditing) {
rules = rules.map((rule) => rule.id === editingId ? { ...rule, type, value, sourceRaw: null, edited: true } : rule);
} else {
rules.unshift({ id: nextRuleId++, type, value, sourceRaw: null, edited: true });
}
searchInput.value = '';
resetForm({ keepFeedback: true });
setFormFeedback(wasEditing ? 'ruleUpdated' : 'ruleAdded', 'success');
renderRuleList();
ruleInput.focus();
};
const relocalize = () => {
backdrop.querySelector('.maw-modal-logo-shell')?.setAttribute('aria-label', t('logoOpen'));
const titleEl = backdrop.querySelector('[data-editor-title]');
const noteEl = backdrop.querySelector('[data-editor-note]');
const closeButton = backdrop.querySelector('[data-editor-close-icon]');
const cancelButton = backdrop.querySelector('[data-editor-cancel]');
const saveLabel = backdrop.querySelector('[data-editor-save-label]');
if (titleEl) titleEl.textContent = t('editorTitle');
if (noteEl) noteEl.textContent = t('editorNote');
if (closeButton) closeButton.setAttribute('aria-label', t('close'));
if (cancelButton) cancelButton.textContent = t('cancel');
if (saveLabel) saveLabel.textContent = t('save');
backdrop.querySelector('[data-rule-type-label]').textContent = t('matchTypeLabel');
ruleType.querySelector('option[value="contains"]').textContent = t('matchContainsOption');
ruleType.querySelector('option[value="exact"]').textContent = t('matchExactOption');
ruleType.querySelector('option[value="regex"]').textContent = t('matchRegexOption');
searchInput.placeholder = t('searchRulesPlaceholder');
searchInput.setAttribute('aria-label', t('searchRulesAria'));
backdrop.querySelector('[data-rule-list-title]').textContent = t('ruleListTitle');
cancelEditButton.textContent = t('cancelEdit');
setFormFeedback(formFeedbackState.key, formFeedbackState.tone);
renderFormMode();
renderRuleList();
};
const onKeyDown = (event) => {
if (event.key === 'Escape' && !saveInProgress) close();
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
event.preventDefault();
if (!saveInProgress) saveButton.click();
}
};
const close = (force = false) => {
if (saveInProgress && !force) return;
const hasUnsavedRules = serializeRules() !== savedValue;
const { hasPendingDraft } = getDraftState();
if (!force && (hasUnsavedRules || hasPendingDraft) && !window.confirm(t('discardChanges'))) return;
document.removeEventListener('keydown', onKeyDown, true);
backdrop.remove();
if (editorCleanup === forceClose) editorCleanup = null;
if (editorRelocalize === relocalize) editorRelocalize = null;
if (previouslyFocused instanceof HTMLElement && previouslyFocused.isConnected) previouslyFocused.focus();
};
const forceClose = () => close(true);
const setEditorSaving = (busy) => {
saveInProgress = busy;
backdrop.querySelector('.maw-modal')?.setAttribute('aria-busy', String(busy));
backdrop.querySelectorAll('button,input,select').forEach((control) => { control.disabled = busy; });
};
editorCleanup = forceClose;
editorRelocalize = relocalize;
ruleForm.addEventListener('submit', (event) => {
event.preventDefault();
submitRule();
});
ruleInput.addEventListener('input', () => {
if (formFeedback.classList.contains('is-error')) setFormFeedback();
refreshEditorMeta();
});
ruleType.addEventListener('change', () => {
setFormFeedback();
renderFormMode();
refreshEditorMeta();
});
cancelEditButton.addEventListener('click', () => {
resetForm();
ruleInput.focus();
});
searchInput.addEventListener('input', renderRuleList);
ruleList.addEventListener('click', (event) => {
const editButton = event.target.closest('[data-edit-rule]');
const removeButton = event.target.closest('[data-remove-rule]');
if (editButton) startEditing(Number(editButton.dataset.editRule));
if (removeButton) removeRule(Number(removeButton.dataset.removeRule));
});
backdrop.querySelectorAll('[data-close]').forEach((button) => button.addEventListener('click', () => close()));
backdrop.addEventListener('click', (event) => { if (event.target === backdrop) close(); });
document.addEventListener('keydown', onKeyDown, true);
saveButton.addEventListener('click', async () => {
if (saveInProgress) return;
const draftState = getDraftState();
if (editingId !== null && draftState.hasEditChanges) {
setFormFeedback('pendingEdit', 'error');
ruleInput.focus();
return;
}
if (editingId !== null) resetForm();
if (editingId === null && draftState.hasPendingDraft) {
setFormFeedback('pendingRule', 'error');
ruleInput.focus();
return;
}
if (!rules.length) {
statusEl.textContent = t('atLeastOneRule');
statusEl.className = 'maw-modal-status is-dirty';
notify('warning', t('emptyListCannotSave'));
return;
}
const serializedValue = serializeRules();
if (new TextEncoder().encode(serializedValue).length > MAX_WORDLIST_BYTES) {
statusEl.textContent = t('editorSaveFailed', { error: t('localListTooLarge') });
statusEl.className = 'maw-modal-status is-dirty';
notify('error', t('editorSaveFailed', { error: t('localListTooLarge') }));
return;
}
setEditorSaving(true);
try {
await setRulesValue('adWordlist', serializedValue);
rules = rules.map((rule) => ({ ...rule, sourceRaw: encodeEditorRule(rule), edited: false }));
savedValue = serializedValue;
refreshEditorMeta();
try {
const filteredRows = await decorateRows();
const saveParams = {
rules: quantity(rules.length, 'rule'),
matches: quantity(filteredRows.length, 'match'),
};
notify('success', t('rulesSaved', saveParams));
setPanelStatus('rulesSavedStatus', 'success', '', saveParams);
} catch (previewError) {
const previewParams = {
rules: quantity(rules.length, 'rule'),
error: previewError?.message || previewError,
};
notify('warning', t('rulesSavedPreviewFailed', previewParams));
setPanelStatus('rulesSavedPreviewFailed', 'warning', '', previewParams);
}
} catch (error) {
statusEl.textContent = t('editorSaveFailed', { error: error?.message || error });
statusEl.className = 'maw-modal-status is-dirty';
notify('error', t('editorSaveFailed', { error: error?.message || error }));
} finally {
setEditorSaving(false);
}
});
relocalize();
requestAnimationFrame(() => ruleInput.focus());
}
// ─── Menu commands ────────────────────────────────────────────────────────
let menuCommandIds = [];
let menuCommandsRegistered = false;
function registerMenuCommands() {
if (menuCommandsRegistered) {
if (typeof GM_unregisterMenuCommand !== 'function') return;
menuCommandIds.forEach((id) => {
try { GM_unregisterMenuCommand(id); }
catch { /* The userscript manager may already have removed it. */ }
});
}
const commands = [
['menuCloseCurrent', () => runToggleRows(false)],
['menuOpenCurrent', () => runToggleRows(true)],
['menuCloseAll', () => requestDisableAllPages()],
['menuUpdateList', () => ensureWord()],
['menuRestoreList', () => restoreWordlistBackup()],
['menuEditList', () => openWordlistEditor()],
['menuTogglePanel', () => setPanelCollapsed(!panelCollapsed)],
['menuSwitchLanguage', () => switchLanguage()],
['menuCheckScriptUpdate', () => checkScriptUpdate({ manual: true })],
];
menuCommandIds = commands.map(([key, callback]) => (
GM_registerMenuCommand(`Makaytron · ${t(key)}`, callback)
)).filter((id) => id !== undefined && id !== null);
menuCommandsRegistered = true;
}
// ─── Keyboard shortcuts ───────────────────────────────────────────────────
function isEditableTarget(target) {
return target instanceof Element && Boolean(
target.closest('input, textarea, select, button, a[href], [contenteditable="true"], [role="textbox"], [role="button"]')
);
}
document.addEventListener('keydown', (e) => {
if (e.repeat || e.isComposing || document.getElementById(EDITOR_MODAL_ID) || isEditableTarget(e.target)) return;
if (e.getModifierState?.('AltGraph')) return;
const autoShortcut = e.ctrlKey && e.altKey && !e.shiftKey && !e.metaKey && e.code === 'KeyK';
const pageShortcut = e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && e.code === 'Space';
if (!autoShortcut && !pageShortcut) return;
e.preventDefault();
if (autoShortcut) requestDisableAllPages();
else requestShortcutDisableCurrentPage();
});
// ─── Page load ────────────────────────────────────────────────────────────
async function initializePage() {
await loadLanguage();
registerMenuCommands();
registerTelemetryMenuCommand();
await mountMakaytronPanel();
scheduleAutomaticScriptUpdateCheck();
setPanelStatus('loadingKeywordTable', 'running', 'preparing');
const rowsReady = await waitForKeywordRows();
if (!rowsReady) {
void trackTelemetryError('selector_ads_table');
throw new Error(t('keywordTableFailed'));
}
const filteredRows = await decorateRows();
setPanelStatus(
'rowsReady',
filteredRows.length ? 'warning' : 'ready',
'',
{
rows: quantity(getAllRows().length, 'row'),
matches: quantity(filteredRows.length, 'match'),
}
);
notify('success', t('appReady'));
}
function startInitialization() {
initializePage().catch((error) => {
console.error('[Makaytron Etsy Ads Keyword Manager]', t('consoleInitializationFailed'), error);
notify('error', t('appStartFailed', { error: error?.message || error }));
setPanelStatus(error?.message || t('appStartFailedFallback'), 'error', '', {}, true);
});
}
if (document.readyState === 'complete') {
startInitialization();
} else {
window.addEventListener('load', startInitialization, { once: true });
}
})();