Greasy Fork is available in English.
Assistente completo para Popmundo — melhorias visuais, ferramentas de inventário e comércio, música e atalhos de cidade. Compatível com desktop, Android e iPhone.
// ==UserScript==
// @name 🎨 Helper
// @name:en 🎨 Helper
// @name:pt-BR 🎨 Helper
// @namespace popmundo.helper
// @version 4.9
// @description Popmundo için kapsamlı oyun yardımcısı — görsel iyileştirmeler, envanter & ticaret araçları, müzik ve şehir kısayolları. Masaüstü, Android ve iPhone ile tam uyumlu.
// @description:en Comprehensive Popmundo game assistant — UI enhancements, inventory & trading tools, music and city shortcuts. Fully compatible with desktop, Android and iPhone.
// @description:pt-BR Assistente completo para Popmundo — melhorias visuais, ferramentas de inventário e comércio, música e atalhos de cidade. Compatível com desktop, Android e iPhone.
// @author luke-james-gibson
// @license MIT
// @id 9g1a6x
// @match https://*.popmundo.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-end
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
// ─── POPCONTROL DISABLE CHECK ─────────────────────────────────────────────────
try { const _ppc = JSON.parse(localStorage.getItem('ppc_enabled')||'{}'); if (_ppc['helper'] === false) return; } catch {}
// ─── MOBILE DETECTION ────────────────────────────────────────────────────────
const isMobile = window.innerWidth < 768 || 'ontouchstart' in window;
// ─── CSS ──────────────────────────────────────────────────────────────────────
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: `
.tvip-bar{position:fixed;top:0;right:0;z-index:9990;background:#fff;border:1px solid #ddd;border-radius:0 0 0 6px;padding:3px 10px;font-size:11px;display:flex;gap:10px;align-items:center;box-shadow:0 1px 6px rgba(0,0,0,.15)}
.tvip-bar a{font-weight:bold;text-decoration:none;color:inherit;cursor:pointer}
.tvip-hpanel{display:none;position:fixed;top:26px;right:0;z-index:9989;background:#fff;border:1px solid #ddd;border-radius:0 0 0 6px;padding:12px;min-width:240px;max-width:290px;box-shadow:0 4px 12px rgba(0,0,0,.15);max-height:82vh;overflow-y:auto}
.tvip-ov{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:center;justify-content:center}
.tvip-box{background:#fff;border-radius:8px;padding:18px;min-width:300px;max-width:500px;width:90%;box-shadow:0 4px 24px rgba(0,0,0,.35);max-height:80vh;overflow-y:auto}
.tvip-title{font-weight:bold;font-size:14px;margin-bottom:12px}
.tvip-chk{display:flex;align-items:flex-start;gap:6px;margin-bottom:8px;cursor:pointer;font-size:12px;line-height:1.4}
.tvip-chk input{margin-top:2px;flex-shrink:0;cursor:pointer}
.tvip-hr{border:none;border-top:1px solid #e0e0e0;margin:6px 0}
.tvip-sec{font-size:10px;font-weight:bold;color:#888;margin:8px 0 3px;text-transform:uppercase;letter-spacing:.5px}
.tvip-pct{float:left;font-size:9px;pointer-events:none}
.tvip-badge{padding:0 5px;border-radius:10px;font-weight:bold;margin-left:4px;font-size:inherit;display:inline-block}
.tvip-search-wrap{margin-bottom:6px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.tvip-search-done{display:none}
.tvip-ticket-price{margin-left:8px;color:#d6021e;font-weight:bold}
.tvip-item-id,.tvip-item-page-id{margin-left:6px;color:#777;font-size:11px}
.tvip-tbl-avg{font-weight:bold;background:#f5f5f5}
.tvip-heist-avg{font-weight:bold;background:#f5f5f5}
.tvip-lang-row{display:flex;gap:4px;margin:6px 0}
.tvip-lang-btn{flex:1;padding:3px 4px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:11px;background:#f8f9fa;text-align:center}
.tvip-lang-btn.active{background:#17a2b8;color:#fff;border-color:#17a2b8;font-weight:bold}
.tvip-bulk-panel{background:#f0f0f0;border:1px solid #dcdcdc;border-radius:6px;padding:12px;margin-bottom:16px;font-size:13px}
.tvip-bulk-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;margin-bottom:10px}
.tvip-bulk-field label{display:block;font-weight:bold;font-size:11px;color:#555;margin-bottom:3px}
.tvip-bulk-field input{width:100%;padding:5px 6px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;font-size:12px}
.tvip-bulk-actions{display:flex;gap:6px;margin-bottom:8px}
.tvip-bulk-actions button{flex:1;padding:5px 10px;border-radius:4px;font-weight:bold;font-size:12px;cursor:pointer;border:1px solid #555}
.tvip-bulk-actions button:disabled{opacity:.5;cursor:not-allowed}
.tvip-status{font-size:12px;text-align:center;background:#e9ecef;padding:5px;border-radius:4px;margin-bottom:4px;color:#495057;font-weight:500}
.tvip-spent{font-size:12px;text-align:center;background:#d1fae5;padding:5px;border-radius:4px;color:#065f46;font-weight:500}
.tvip-btn-start{background:linear-gradient(to bottom,#d4edda,#c3e6cb);border-color:#28a745!important}
.tvip-btn-stop{background:linear-gradient(to bottom,#f8d7da,#f5c6cb);border-color:#dc3545!important}
.tvip-send-wrap{margin-top:10px;padding:10px;border-radius:5px}
.tvip-send-running{background:#d4edda;border:2px solid #28a745}
.tvip-send-setup{background:#fff3cd;border:2px solid #ffc107}
.btn-g{padding:5px 14px;background:#218838;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}
.btn-b{padding:5px 14px;background:#17a2b8;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}
.btn-r{padding:5px 14px;background:#e74c3c;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}
.btn-grey{padding:5px 14px;background:#6c757d;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:12px}
.btn-sm{padding:2px 8px!important;font-size:11px!important}
` }));
if (isMobile) document.head.appendChild(Object.assign(document.createElement('style'), { textContent: `
/* ── Touch targets ── */
.tvip-chk{padding:10px 4px;font-size:14px;gap:10px}
.tvip-chk input{width:20px;height:20px;flex-shrink:0}
.tvip-lang-btn{padding:10px 4px;font-size:13px}
.btn-g,.btn-b,.btn-r,.btn-grey{padding:10px 18px;font-size:14px}
.btn-sm{padding:8px 12px!important;font-size:13px!important}
/* ── Sections ── */
.tvip-sec{font-size:12px;margin:12px 0 6px}
.tvip-hr{margin:10px 0}
/* ── Modal ── */
.tvip-box{width:96%!important;max-width:96%!important;padding:16px;max-height:88vh}
.tvip-title{font-size:16px;margin-bottom:14px}
/* ── Bulk panels ── */
.tvip-bulk-grid{grid-template-columns:1fr!important}
.tvip-bulk-field input,.tvip-bulk-field select{font-size:14px;padding:8px 10px}
.tvip-bulk-field label{font-size:13px}
.tvip-bulk-actions button{padding:10px;font-size:14px}
.tvip-status,.tvip-spent{font-size:13px;padding:8px}
/* ── Table search ── */
.tvip-search-wrap input{font-size:14px;padding:7px 10px;width:100%;box-sizing:border-box}
.tvip-search-wrap{flex-wrap:wrap}
/* ── Send wrap ── */
.tvip-send-wrap{padding:14px}
.tvip-send-wrap input[type=number]{font-size:14px;padding:6px 8px}
/* ── City manager ── */
#tvip-bulk-offer-ui h3,#tvip-bulk-accept-ui h3,#tvip-bulk-cancel-ui h3{font-size:15px}
` }));
// ─── UTILS ────────────────────────────────────────────────────────────────────
const CK = {
get: k => { const m = document.cookie.match(new RegExp('(?:^|; )' + k + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : null; },
set: (k, v) => { document.cookie = `${k}=${encodeURIComponent(v)};domain=.popmundo.com;path=/;max-age=31536000`; }
};
const mk = (tag, cls, txt) => { const e = document.createElement(tag); if (cls) e.className = cls; if (txt !== undefined) e.textContent = txt; return e; };
const mkB = (txt, cls, fn) => Object.assign(mk('button', cls, txt), { onclick: fn, type: 'button' });
const isOn = k => CK.get(k) !== '0';
const guard = (key, ...urls) => isOn(key) && (!urls.length || urls.some(u => location.href.includes(u)));
const mkModal = (title, renderFn) => {
document.getElementById('tvip-modal')?.remove();
const ov = mk('div', 'tvip-ov'); ov.id = 'tvip-modal';
const box = mk('div', 'tvip-box');
box.appendChild(mk('div', 'tvip-title', title));
const cont = mk('div'); box.appendChild(cont);
const close = () => ov.remove();
renderFn(cont, close);
const cb = mkB('✕ ' + s('close'), 'btn-grey', close);
cb.style.marginTop = '12px';
box.appendChild(cb);
ov.onclick = e => { if (e.target === ov) close(); };
ov.appendChild(box); document.body.appendChild(ov);
};
// ─── LANG ─────────────────────────────────────────────────────────────────────
const LANG = CK.get('ppm_lang') || 'TR';
const _D = (tr, en, pt) => ({ TR: tr, EN: en, PT: pt });
const STR = {
menuTitle: _D('🎨 Helper', '🎨 Helper', '🎨 Helper'),
save: _D('✔ Tamam', '✔ Save', '✔ Salvar'),
readMe: _D('📖 Beni Oku', '📖 Read Me', '📖 Leia-me'),
langLabel: _D('Dil', 'Language', 'Idioma'),
backup: _D('📤 Yedekle', '📤 Backup', '📤 Exportar'),
restore: _D('📥 Geri Yükle', '📥 Restore', '📥 Importar'),
backupOk: _D('Yedek alındı.', 'Backup created.', 'Backup criado.'),
restoreOk: _D('Geri yüklendi.', 'Restored.', 'Restaurado.'),
restoreErr: _D('Geçersiz dosya.', 'Invalid file.', 'Arquivo inválido.'),
restoreQ: _D('Mevcut veriler ne olsun?', 'What to do with current data?', 'O que fazer com os dados atuais?'),
mergeLbl: _D('Birleştir', 'Merge', 'Mesclar'),
replaceLbl: _D('Üzerine Yaz', 'Replace', 'Substituir'),
cancelLbl: _D('İptal', 'Cancel', 'Cancelar'),
close: _D('Kapat', 'Close', 'Fechar'),
// sections
secGeneral: _D('GENEL GÖRÜNÜM', 'GENERAL', 'GERAL'),
secTable: _D('TABLO', 'TABLE', 'TABELA'),
secItems: _D('EŞYA', 'ITEMS', 'ITENS'),
secMusic: _D('MÜZİK', 'MUSIC', 'MÚSICA'),
secNav: _D('GEZİNME', 'NAVIGATION', 'NAVEGAÇÃO'),
// features
pb: _D('📊 Barlarda yüzde göster', '📊 Show % on bars', '📊 Mostrar % nas barras'),
alignLeft: _D('⬅️ Sayfayı sola yasla', '⬅️ Align page to left', '⬅️ Alinhar página à esquerda'),
tableTools: _D('🔍 Tablolarda sıralama ve arama', '🔍 Table sort & search', '🔍 Ordenar e buscar tabelas'),
itemId: _D('🏷️ Eşya ID — Eşya linklerinin yanına ID numarasını ekler',
'🏷️ Item ID — Appends ID number next to item links',
'🏷️ ID do Item — Adiciona o número de ID ao lado dos links de item'),
colorScoring: _D('🌈 Renkli Puanlama Sistemi — Şöhret linklerinin yanına renkli rozet ekler (0-26)',
'🌈 Coloured Scoring System — Adds a coloured badge next to fame links (0-26)',
'🌈 Sistema de Pontuação Colorida — Adiciona emblema colorido ao lado dos links de fama'),
autoDelivery: _D('✅ Otomatik Teslimat — Teslim onay kutularını otomatik işaretler',
'✅ Auto Delivery — Automatically checks delivery confirmation checkboxes',
'✅ Entrega Automática — Marca automaticamente as caixas de confirmação de entrega'),
ticketPricer: _D('🎟️ Bilet Fiyatlandırıcı — Sanatçı davet sayfasında önerilen bilet fiyatını gösterir',
'🎟️ Ticket Pricer — Shows recommended ticket price on artist invite page',
'🎟️ Precificador de Ingressos — Mostra o preço recomendado na página de convite de artista'),
imageCtrl: _D('🖼️ Yavaş/Yüklenmez Resim Kontrolü — 5sn timeout; placeholder + tıklayınca yeniden dene',
'🖼️ Slow/Broken Image Control — 5s timeout; placeholder + click to retry',
'🖼️ Controle de Imagens Lentas — Timeout 5s; placeholder + clique para tentar novamente'),
tableAvg: _D('📈 Tablolara ortalama satırı ekle', '📈 Add average row to tables', '📈 Adicionar linha de média às tabelas'),
itemFilters: _D('🎒 Sadece Alınabilecek Eşyaları Göster', '🎒 Show Only Takeable Items', '🎒 Mostrar Apenas Itens Disponíveis'),
jamHelper: _D('🎸 Jam için eksik şarkıları seç', '🎸 Select incomplete songs for Jam', '🎸 Selecionar músicas incompletas para Jam'),
repertoireF: _D('🎵 Repertuarı kategoriye göre filtrele', '🎵 Filter repertoire by category', '🎵 Filtrar repertório por categoria'),
sendItems: _D('📦 Aynı eşyayı sırayla otomatik gönder', '📦 Auto-send same item multiple times', '📦 Enviar o mesmo item várias vezes'),
bulkOffer: _D('🏷️ Eşyaları toplu fiyatla teklif et', '🏷️ Offer items in bulk at set price', '🏷️ Ofertar itens em massa com preço fixo'),
bulkAccept: _D('🛒 Gelen teklifleri otomatik kabul et', '🛒 Auto-accept incoming offers', '🛒 Aceitar ofertas recebidas automaticamente'),
cityShortcuts: _D('🏙️ Şehirde duş evi ve patikaları göster','🏙️ Show shower house & paths in city', '🏙️ Mostrar casa de banho e trilhas na cidade'),
bbMinimize: _D('—', '—', '—'),
bbRestore: _D('🎮', '🎮', '🎮'),
// table search
psPlh: _D('Tabloda ara...', 'Search table...', 'Buscar na tabela...'),
psCount: _D('sonuç', 'results', 'resultados'),
tdConfirm: _D('Bu tablonun arama kutusunu gizlemek istiyor musun?', 'Hide the search box for this table?', 'Ocultar a caixa de busca desta tabela?'),
tdHide: _D('Aramayı gizle', 'Hide search', 'Ocultar busca'),
tdClear: _D('Gizli Tabloları Sıfırla', 'Reset Hidden Tables', 'Redefinir Tabelas Ocultas'),
// item filters
btnShowAll: _D('👁 Tüm Eşyaları Göster', '👁 Show All Items', '👁 Mostrar Todos os Itens'),
btnOnlyTake: _D('🔒 Sadece Alınabilecekler', '🔒 Only Takeable', '🔒 Só os Pegáveis'),
resetOffered: _D('🗑️ Teklif Listesini Sıfırla', '🗑️ Reset Offer List', '🗑️ Redefinir Lista'),
confirmReset: _D('Teklif listesi sıfırlansın mı?', 'Reset offered items list?', 'Redefinir lista de itens ofertados?'),
resetDone: _D('Sıfırlandı.', 'Done.', 'Redefinido.'),
hideOfferedChk: _D('Teklif edilenleri gizle', 'Hide offered items', 'Ocultar itens ofertados'),
// jam
jamBtn: _D('⏱ %100 Olmayan Jam', '⏱ Jam <100%', '⏱ Jam <100%'),
// repertoire
repAll: _D('Tüm Şarkılar', 'All Songs', 'Todas as Músicas'),
repMarket: _D('Pazar Şarkıları', 'Market Songs', 'Músicas do Mercado'),
repNoMarket: _D('Pazar Dışı', 'Non-Market', 'Fora do Mercado'),
repJam: _D('Jam Şarkıları', 'Jam Songs', 'Músicas de Jam'),
repSetlist: _D('Setlist Şarkıları', 'Setlist Songs', 'Músicas do Setlist'),
repSecret: _D('Gizli Şarkılar', 'Secret Songs', 'Músicas Secretas'),
repTitle: _D('🎵 Repertuar Filtresi', '🎵 Repertoire Filter', '🎵 Filtro de Repertório'),
// quick send
siWarning: _D('⚠️ Birden fazla aynı eşya seçin', '⚠️ Select an item with multiple copies', '⚠️ Selecione um item com múltiplas cópias'),
siCount: _D('Kaç adet?', 'How many?', 'Quantas?'),
siSend: _D('🔄 Gönder', '🔄 Send', '🔄 Enviar'),
siCancel: _D('❌ İptal', '❌ Cancel', '❌ Cancelar'),
siStop: _D('⏹ Durdur', '⏹ Stop', '⏹ Parar'),
siStopping: _D('Durduruluyor...', 'Stopping...', 'Parando...'),
siDone: _D('✅ Tamamlandı!', '✅ Done!', '✅ Concluído!'),
// bulk offer
boTitle: _D('Toplu Teklif', 'Bulk Offer', 'Oferta em Massa'),
boItemName: _D('Eşya adı (başlangıç):', 'Item name (prefix):', 'Nome do item (prefixo):'),
boQty: _D('Adet:', 'Quantity:', 'Quantidade:'),
boPrice: _D('Fiyat (M$):', 'Price (M$):', 'Preço (M$):'),
boStart: _D('▶ Teklif Et', '▶ Offer', '▶ Ofertar'),
boStop: _D('■ Durdur', '■ Stop', '■ Parar'),
boReady: _D('Hazır.', 'Ready.', 'Pronto.'),
boDone: _D('Tüm teklifler tamamlandı!', 'All offers completed!', 'Todas as ofertas concluídas!'),
boStopped: _D('Kullanıcı tarafından durduruldu.', 'Stopped by user.', 'Parado pelo usuário.'),
boResumed: _D('Yeniden yüklendi, devam ediliyor...', 'Reloaded, resuming...', 'Recarregado, retomando...'),
boCritErr: _D('Kritik hata: Sayfa öğeleri kayboldu.', 'Critical error: Page elements gone.', 'Erro crítico: Elementos da página desapareceram.'),
boErrName: _D('Hata: Eşya adı girin.', 'Error: Enter item name.', 'Erro: Digite o nome do item.'),
boErrQty: _D('Hata: Geçersiz adet.', 'Error: Invalid quantity.', 'Erro: Quantidade inválida.'),
boErrPrice: _D('Hata: Geçersiz fiyat.', 'Error: Invalid price.', 'Erro: Preço inválido.'),
// bulk accept
baTitle: _D('Toplu Kabul', 'Bulk Accept', 'Aceitar em Massa'),
baItemName: _D('Eşya Adı (Opsiyonel):', 'Item Name (Optional):', 'Nome do Item (Opcional):'),
baItemPlh: _D('Tümü için boş bırakın', 'Leave blank for all', 'Deixe em branco para todos'),
baMaxPrice: _D('Maks. Fiyat (M$):', 'Max Price (M$):', 'Preço Máximo (M$):'),
baStart: _D('▶ Kabul Et', '▶ Accept', '▶ Aceitar'),
baStop: _D('■ Durdur', '■ Stop', '■ Parar'),
baReady: _D('Hazır.', 'Ready.', 'Pronto.'),
baNoSection: _D('Teklif bölümü bulunamadı.', 'Offer section not found.', 'Seção de ofertas não encontrada.'),
baResumed: _D('Yeniden yüklendi, devam ediliyor...', 'Reloaded, resuming...', 'Recarregado, retomando...'),
baErrPrice: _D('Hata: Geçersiz fiyat.', 'Error: Invalid price.', 'Erro: Preço inválido.'),
baFree: _D('Bedava', 'Free', 'Grátis'),
// bulk cancel
bcTitle: _D('Toplu İptal', 'Bulk Cancel', 'Cancelar em Massa'),
bcItemName: _D('Eşya Adı (Opsiyonel):', 'Item Name (Optional):', 'Nome do Item (Opcional):'),
bcFilter: _D('Filtre:', 'Filter:', 'Filtro:'),
bcFilterAll: _D('Tümü', 'All', 'Todos'),
bcFilterFree: _D('Sadece Bedava', 'Free Only', 'Só Grátis'),
bcFilterPaid: _D('Sadece Ücretli', 'Paid Only', 'Só Pagos'),
bcStart: _D('▶ İptal Et', '▶ Cancel', '▶ Cancelar'),
bcStop: _D('■ Durdur', '■ Stop', '■ Parar'),
bcReady: _D('Hazır.', 'Ready.', 'Pronto.'),
bcNoSection: _D('Teklif bölümü bulunamadı.', 'Offer section not found.', 'Seção não encontrada.'),
// city shortcuts
csShower: _D('Duş Evi', 'Shower House', 'Casa de Banho'),
csPath: _D('Patika', 'Path', 'Trilha'),
csGoShower: _D('Duş evine git', 'Go to shower house', 'Ir para casa de banho'),
csGoPath: _D('Patikaya git', 'Go to path', 'Ir para trilha'),
csMin: _D('Dakika', 'Minutes', 'Minutos'),
csOther: _D('Diğer Mekan', 'Other Location', 'Outro Local'),
cityMgr: _D('Şehir Kısayolları Yönet','Manage City Shortcuts','Gerenciar Atalhos'),
cityMgrReset: _D('↩ Sıfırla', '↩ Reset', '↩ Redefinir'),
cityMgrResetQ: _D('Bu şehir varsayılana dönsün mü?','Reset this city to default?','Redefinir esta cidade?'),
cityMgrAdd: _D('Mekan Ekle', 'Add Location', 'Adicionar Local'),
cityMgrAddBtn: _D('Ekle', 'Add', 'Adicionar'),
cityMgrName: _D('Mekan Adı', 'Location Name', 'Nome do Local'),
cityMgrCustom: _D('Özelleştirilmiş', 'Customized', 'Personalizado'),
cityMgrDelQ: _D('Bu mekan silinsin mi?','Delete this location?','Excluir este local?'),
cityMgrEmpty: _D('Mekan yok.', 'No locations.', 'Sem locais.'),
// table avg
taAvg: _D('🌍 Ortalama', '🌍 Average', '🌍 Média'),
taDiscAvg: _D('🎯 Keşif Ortalaması:', '🎯 Heist Avg:', '🎯 Média de Descoberta:'),
};
const _clHelper = (() => { try { const v = localStorage.getItem('ppc_lc_helper'); return v ? JSON.parse(v) : null; } catch { return null; } })();
const s = k => {
if (_clHelper && _clHelper[k]) return _clHelper[k];
const v = STR[k];
if (!v) return k;
return v[LANG] ?? v['TR'] ?? k;
};
// Dynamic string helpers
const sf = {
boNoItem: n => ({ TR:`"${n}" ile başlayan eşya bulunamadı.`, EN:`No items starting with "${n}" found.`, PT:`Nenhum item começando com "${n}" encontrado.` }[LANG]),
boFound: (t,q,p) => ({ TR:`${t} eşya bulundu. ${q} adet ${p}M$ tekliflenecek...`, EN:`Found ${t}. Offering ${q} at ${p}M$...`, PT:`${t} itens. Ofertando ${q} por ${p}M$...` }[LANG]),
boOfferring: (c,t,n) => ({ TR:`Teklif ${c}/${t}: '${n}'...`, EN:`Offering ${c}/${t}: '${n}'...`, PT:`Ofertando ${c}/${t}: '${n}'...` }[LANG]),
boSkip: n => ({ TR:`Hata: '${n}' seçilemedi, atlandı.`, EN:`Error: Could not select '${n}', skipping.`, PT:`Erro: Não foi possível selecionar '${n}', pulando.` }[LANG]),
siStopped: c => ({ TR:`⏹ Durduruldu. Gönderilen: ${c}`, EN:`⏹ Stopped. Sent: ${c}`, PT:`⏹ Parado. Enviados: ${c}` }[LANG]),
siConfirm: (n,c) => ({ TR:`"${n}" ${c} kez gönderilecek. Onayla?`, EN:`Send "${n}" ${c} times. Confirm?`, PT:`Enviar "${n}" ${c} vezes. Confirmar?` }[LANG]),
siSending: (c,t,n) => ({ TR:`🔄 Gönderiliyor: ${c}/${t} — ${n}`, EN:`🔄 Sending: ${c}/${t} — ${n}`, PT:`🔄 Enviando: ${c}/${t} — ${n}` }[LANG]),
baSpent: n => ({ TR:`Toplam Harcama: ${n} M$`, EN:`Total Spent: ${n} M$`, PT:`Total Gasto: ${n} M$` }[LANG]),
baInit: (n,p) => ({ TR:`Başlatılıyor... "${n||'tümü'}" maks. ${p}M$`, EN:`Starting... "${n||'all'}" up to ${p}M$`, PT:`Iniciando... "${n||'todos'}" até ${p}M$` }[LANG]),
baAccepting: (i,n,p) => ({ TR:`Kabul ediliyor #${i}: '${n}' (${p})...`, EN:`Accepting #${i}: '${n}' (${p})...`, PT:`Aceitando #${i}: '${n}' (${p})...` }[LANG]),
baDone: (c,sp) => ({ TR:`Tamamlandı! Kabul: ${c}. Harcama: ${sp}M$.`, EN:`Done! Accepted: ${c}. Spent: ${sp}M$.`, PT:`Concluído! Aceitos: ${c}. Gasto: ${sp}M$.` }[LANG]),
baStopped: (c,sp) => ({ TR:`Durduruldu. Kabul: ${c}. Harcama: ${sp}M$.`, EN:`Stopped. Accepted: ${c}. Spent: ${sp}M$.`, PT:`Parado. Aceitos: ${c}. Gasto: ${sp}M$.` }[LANG]),
baNoMatch: (c,sp) => ({ TR:`Eşleşen teklif yok. Kabul: ${c}. Harcama: ${sp}M$.`, EN:`No more matches. Accepted: ${c}. Spent: ${sp}M$.`, PT:`Sem correspondências. Aceitos: ${c}. Gasto: ${sp}M$.` }[LANG]),
bcCancelling:(i,n) => ({ TR:`İptal ediliyor #${i}: '${n}'...`, EN:`Cancelling #${i}: '${n}'...`, PT:`Cancelando #${i}: '${n}'...` }[LANG]),
bcDone: c => ({ TR:`Tamamlandı! İptal edilen: ${c}.`, EN:`Done! Cancelled: ${c}.`, PT:`Concluído! Cancelados: ${c}.` }[LANG]),
bcStopped: c => ({ TR:`Durduruldu. İptal edilen: ${c}.`, EN:`Stopped. Cancelled: ${c}.`, PT:`Parado. Cancelados: ${c}.` }[LANG]),
bcNoMatch: c => ({ TR:`Eşleşen teklif yok. İptal edilen: ${c}.`, EN:`No more matches. Cancelled: ${c}.`, PT:`Sem correspondências. Cancelados: ${c}.` }[LANG]),
};
// ─── SETTINGS KEYS ────────────────────────────────────────────────────────────
const K = {
pb: 'tvip_feat_pb',
alignLeft: 'tvip_align_left',
imageCtrl: 'tvip_feat_imgctrl',
tableTools: 'tvip_feat_ttls',
itemId: 'tvip_feat_itemid',
colorScoring: 'tvip_feat_colorsc',
autoDelivery: 'tvip_feat_autodlv',
ticketPricer: 'tvip_feat_ticket',
tableAvg: 'tvip_feat_tavg',
itemFilters: 'tvip_feat_itmf',
jamHelper: 'tvip_feat_jam',
repertoireF: 'tvip_feat_repf',
sendItems: 'tvip_feat_snd',
bulkOffer: 'tvip_feat_bkoffer',
bulkAccept: 'tvip_feat_bkaccept',
cityShortcuts:'tvip_feat_citysc',
popControl: 'tvip_feat_popcontrol',
onlyYoursSt: 'tvip_oy_state',
hideOffBox: 'tvip_hideoff_chk',
};
// ─── DATA KEYS (GM_setValue) ──────────────────────────────────────────────────
const DK = {
TD: 'tvip_table_deny',
OFFERED: 'tvip_offered_list',
BO_ITEMS: 'tvip_bo_items',
BO_RUNNING: 'tvip_bo_running',
BO_PRICE: 'tvip_bo_price',
BO_COUNT: 'tvip_bo_count',
BO_TOTAL: 'tvip_bo_total',
BA_RUNNING: 'tvip_ba_running',
BA_MAX_PRICE: 'tvip_ba_max_price',
BA_ITEM_NAME: 'tvip_ba_item_name',
BA_COUNT: 'tvip_ba_count',
BA_SPENT: 'tvip_ba_spent',
SI_QUEUE: 'tvip_si_queue',
SI_NAME: 'tvip_si_name',
SI_TOTAL: 'tvip_si_total',
SI_STOP: 'tvip_si_stop',
CITY_CUSTOM: 'tvip_city_custom',
BC_RUNNING: 'tvip_bc_running',
BC_ITEM_NAME: 'tvip_bc_item_name',
BC_FILTER: 'tvip_bc_filter',
BC_COUNT: 'tvip_bc_count',
};
// ─── RAINBOW (scoring badges) ─────────────────────────────────────────────────
const RAINBOW = [
['#ff0000','#fff'],['#ff0036','#fff'],['#ff006c','#fff'],['#ff00a2','#fff'],
['#ff00d8','#fff'],['#f000ff','#fff'],['#ba00ff','#fff'],['#8400ff','#fff'],
['#4e00ff','#fff'],['#1900ff','#fff'],['#001dff','#fff'],['#0053ff','#fff'],
['#0089ff','#fff'],['#00bfff','#fff'],['#00f5ff','#000'],['#00ffd3','#000'],
['#00ff9d','#000'],['#00ff67','#000'],['#00ff31','#000'],['#05ff00','#000'],
['#3bff00','#000'],['#71ff00','#000'],['#a7ff00','#000'],['#ddff00','#000'],
['#ffeb00','#000'],['#ffb500','#000'],['#ff8000','#000']
];
// ─── TABLE DENY ───────────────────────────────────────────────────────────────
const getTableDeny = () => { try { return JSON.parse(GM_getValue(DK.TD, '[]')); } catch { return []; } };
const addTableDeny = (path, i) => { const l = getTableDeny(), k = `${path}::${i}`; if (!l.includes(k)) { l.push(k); GM_setValue(DK.TD, JSON.stringify(l)); } };
const clearTableDeny = () => GM_setValue(DK.TD, '[]');
// ─── BACKUP & RESTORE ─────────────────────────────────────────────────────────
const DB_KEYS_GM = [DK.TD, DK.OFFERED, DK.CITY_CUSTOM, DK.BA_RUNNING, DK.BA_MAX_PRICE, DK.BA_ITEM_NAME, DK.BA_COUNT, DK.BA_SPENT, DK.BC_RUNNING, DK.BC_ITEM_NAME, DK.BC_FILTER, DK.BC_COUNT];
const DB_KEYS_CK = Object.values(K);
const dbExport = () => {
const data = { v: 1, script: 'helper', cookies: {}, gm: {} };
DB_KEYS_CK.forEach(k => { const v = CK.get(k); if (v !== null) data.cookies[k] = v; });
DB_KEYS_GM.forEach(k => { const v = GM_getValue(k, null); if (v !== null) data.gm[k] = v; });
data.cookies['ppm_lang'] = CK.get('ppm_lang') || 'TR';
const d = new Date(), p = n => String(n).padStart(2,'0');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
a.download = `ppm-helper-${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}.json`;
a.click();
};
const dbImport = () => {
const inp = Object.assign(document.createElement('input'), { type: 'file', accept: '.json' });
inp.onchange = () => {
const f = inp.files[0]; if (!f) return;
const reader = new FileReader();
reader.onload = ev => {
let data; try { data = JSON.parse(ev.target.result); } catch { alert(s('restoreErr')); return; }
mkModal(s('restore'), (cont, close) => {
cont.appendChild(mk('div', '', s('restoreQ'))).style.cssText = 'font-size:13px;margin-bottom:12px';
const applyMerge = () => {
close();
if (data.cookies) Object.entries(data.cookies).forEach(([k, v]) => { if (CK.get(k) === null) CK.set(k, v); });
if (data.gm) Object.entries(data.gm).forEach(([k, v]) => { if (GM_getValue(k, null) === null) GM_setValue(k, v); });
location.reload();
};
const applyReplace = () => {
close();
DB_KEYS_CK.forEach(k => CK.set(k, ''));
DB_KEYS_GM.forEach(k => GM_deleteValue(k));
if (data.cookies) Object.entries(data.cookies).forEach(([k, v]) => CK.set(k, v));
if (data.gm) Object.entries(data.gm).forEach(([k, v]) => GM_setValue(k, v));
location.reload();
};
const row = mk('div'); row.style.cssText = 'display:flex;gap:8px';
row.append(mkB(s('mergeLbl'), 'btn-b', applyMerge), mkB(s('replaceLbl'), 'btn-r', applyReplace), mkB(s('cancelLbl'), 'btn-grey', close));
cont.appendChild(row);
});
};
reader.readAsText(f);
};
inp.click();
};
// ─── ALIGN LEFT ───────────────────────────────────────────────────────────────
const applyAlignLeft = () => {
if (!isOn(K.alignLeft)) return;
document.head.appendChild(Object.assign(document.createElement('style'), {
textContent: '#aspnetForm>div{margin-left:20px!important}body{justify-content:flex-start!important}'
}));
};
// ─── PROGRESS BAR ─────────────────────────────────────────────────────────────
const applyProgressBar = () => {
if (!isOn(K.pb)) return;
document.querySelectorAll('.progressBar,.greenProgressBar,.blueProgressBar').forEach(e => {
let pct = e.getAttribute('title') || ''; if (pct.includes(' ')) pct = pct.split(' ').pop();
if (pct && !e.querySelector('.tvip-pct')) e.prepend(mk('div', 'tvip-pct', pct));
});
document.querySelectorAll('.plusMinusBar').forEach(e => {
let pct = e.getAttribute('title') || ''; if (pct.includes(' ')) pct = pct.split(' ').pop();
const tgt = e.querySelector('.negholder');
if (tgt && pct && !tgt.querySelector('.tvip-pct')) tgt.prepend(mk('div', 'tvip-pct', pct));
});
};
// ─── ITEM ID ──────────────────────────────────────────────────────────────────
const applyItemId = () => {
if (!isOn(K.itemId)) return;
document.querySelectorAll("a[href*='/ItemDetails/']").forEach(link => {
if (link.parentElement.querySelector('.tvip-item-id')) return;
const m = link.href.match(/\/ItemDetails\/(\d+)/); if (!m) return;
link.parentElement.appendChild(mk('span', 'tvip-item-id', `(#${m[1]})`));
});
const pm = location.href.match(/\/ItemDetails\/(\d+)/);
if (pm && !document.querySelector('.tvip-item-page-id')) {
const h2 = document.querySelector('h2');
if (h2) h2.appendChild(mk('span', 'tvip-item-page-id', `ID: ${pm[1]}`));
}
};
// ─── COLOUR SCORING ───────────────────────────────────────────────────────────
const applyColorScoring = () => {
if (!isOn(K.colorScoring)) return;
document.querySelectorAll('a[href*="Help/Scoring/"]:not(.tvip-ng)').forEach(a => {
const m = a.href.match(/\/Help\/Scoring\/(\d+)/i); if (!m) return;
const idx = parseInt(m[1]) - 1; if (idx < 0 || idx > 26) return;
const [bg, fg] = RAINBOW[idx];
const badge = mk('span', 'tvip-badge', String(idx));
badge.style.cssText = `background:${bg};color:${fg}`;
a.classList.add('tvip-ng'); a.parentNode.insertBefore(badge, a.nextSibling);
});
};
// ─── AUTO DELIVERY ────────────────────────────────────────────────────────────
const applyAutoDelivery = () => {
if (!isOn(K.autoDelivery)) return;
document.querySelectorAll("input[type=checkbox][id$='chkDelivery']").forEach(cb => cb.checked = true);
};
// ─── TICKET PRICER ────────────────────────────────────────────────────────────
const applyTicketPricer = () => {
if (!isOn(K.ticketPricer)) return;
if (!location.href.includes('/InviteArtist/')) return;
const pm = {0:'5$',1:'5$',2:'5$',3:'7$',4:'9$',5:'12$',6:'15$',7:'18$',8:'20$',9:'25$',
10:'30$',11:'35$',12:'40$',13:'45$',14:'50$',15:'65$',16:'70$',17:'75$',18:'80$',19:'85$',20:'90$'};
document.querySelectorAll("a[href^='/World/Popmundo.aspx/Help/Scoring/']").forEach(link => {
const raw = link.getAttribute('title'); if (!raw) return;
const score = parseInt(raw.replace('/26','').trim()); if (isNaN(score)) return;
if (pm[score] && !link.parentElement.querySelector('.tvip-ticket-price'))
link.parentElement.appendChild(mk('span', 'tvip-ticket-price', pm[score]));
});
};
// ─── IMAGE CONTROL ────────────────────────────────────────────────────────────
const applyImageCtrl = () => {
if (!isOn(K.imageCtrl)) return;
const TIMEOUT_MS = 5000;
const POPMUNDO_RE = /^https?:\/\/([^/]*\.)?popmundo\.com\//i;
const placeholder = (img) => {
if (img.dataset.tvipImgHandled) return;
img.dataset.tvipImgHandled = '1';
const src = img.src;
const wrap = img.parentElement;
const ph = mk('span', 'tvip-img-ph');
ph.style.cssText = 'display:inline-flex;align-items:center;justify-content:center;width:'+(img.width||48)+'px;height:'+(img.height||48)+'px;min-width:24px;min-height:24px;background:#eee;border:1px dashed #ccc;border-radius:4px;font-size:13px;cursor:pointer;color:#aaa;box-sizing:border-box;';
ph.textContent = '🖼️';
ph.title = 'Yüklenemedi — yeniden denemek için tıkla';
ph.onclick = () => {
ph.remove();
const ni = mk('img');
ni.src = src + (src.includes('?') ? '&' : '?') + '_r=' + Date.now();
ni.className = img.className;
ni.style.cssText = img.style.cssText;
if (wrap) wrap.insertBefore(ni, img.nextSibling);
};
img.style.display = 'none';
if (wrap) wrap.insertBefore(ph, img.nextSibling);
};
const observe = (img) => {
if (img.dataset.tvipImgHandled) return;
if (!img.src || POPMUNDO_RE.test(img.src)) return;
if (img.complete && img.naturalWidth > 0) return;
let timer = setTimeout(() => placeholder(img), TIMEOUT_MS);
img.addEventListener('load', () => clearTimeout(timer), { once: true });
img.addEventListener('error', () => { clearTimeout(timer); placeholder(img); }, { once: true });
};
document.querySelectorAll('img').forEach(observe);
const mo = new MutationObserver(muts => {
muts.forEach(m => m.addedNodes.forEach(n => {
if (n.tagName === 'IMG') observe(n);
else if (n.querySelectorAll) n.querySelectorAll('img').forEach(observe);
}));
});
mo.observe(document.body, { childList: true, subtree: true });
};
// ─── ITEM FILTERS ─────────────────────────────────────────────────────────────
const applyOnlyYours = () => {
if (!isOn(K.itemFilters)) return;
const list = document.getElementById('checkedlist'); if (!list) return;
const filter = on => list.querySelectorAll('tr:not(:first-child)').forEach(row => {
const inputs = row.querySelector('td:first-child')?.querySelectorAll('input') || [];
row.style.display = (on && inputs.length < 2 && row.className !== 'group') ? 'none' : '';
});
const active = CK.get(K.onlyYoursSt) === '1';
filter(active);
const btn = mk('a', '', active ? s('btnShowAll') : s('btnOnlyTake'));
btn.href = '#';
btn.style.cssText = 'display:inline-block;margin-bottom:12px;padding:5px 10px;background:#17a2b8;color:#fff;text-decoration:none;border-radius:4px;font-size:12px';
btn.onclick = e => {
e.preventDefault();
const cur = CK.get(K.onlyYoursSt) === '1';
CK.set(K.onlyYoursSt, cur ? '0' : '1');
btn.textContent = cur ? s('btnOnlyTake') : s('btnShowAll');
filter(!cur);
};
list.before(btn);
};
const getOfferedList = () => { try { return JSON.parse(GM_getValue(DK.OFFERED, '[]')); } catch { return []; } };
const saveOfferedList = (l) => GM_setValue(DK.OFFERED, JSON.stringify(l));
const applyHideOffered = () => {
if (!isOn(K.itemFilters)) return;
if (isOn(K.bulkOffer)) return; // cleanHideOfferedCheckbox handles it
const sel = document.querySelector('#ctl00_cphLeftColumn_ctl00_ddlItem'); if (!sel) return;
getOfferedList().forEach(id => sel.querySelector(`option[value="${id}"]`)?.remove());
document.querySelector('#ctl00_cphLeftColumn_ctl00_btnGive')?.addEventListener('click', () => {
const id = sel.value; if (!id || id === '-1') return;
const l = getOfferedList(); if (!l.includes(id)) { l.push(id); saveOfferedList(l); }
});
};
// ─── TABLE SORT ───────────────────────────────────────────────────────────────
const applyTableSort = () => {
if (!guard(K.tableTools)) return;
const deny = getTableDeny();
document.querySelectorAll('table').forEach((table, ti) => {
if (deny.includes(`${location.pathname}::${ti}`)) return;
const ths = table.querySelectorAll('tr:first-child th'); if (!ths.length) return;
ths.forEach((th, ci) => {
th.style.cursor = 'pointer'; let asc = true;
th.addEventListener('click', () => {
const body = table.querySelector('tbody') || table;
const rows = [...body.querySelectorAll('tr')].filter(r => r.cells.length && !r.classList.contains('tvip-tbl-avg') && !r.classList.contains('tvip-heist-avg'));
rows.sort((a, b) => {
const av = a.cells[ci]?.textContent.trim() || '', bv = b.cells[ci]?.textContent.trim() || '';
const an = parseFloat(av.replace(/[^\d.-]/g, '')), bn = parseFloat(bv.replace(/[^\d.-]/g, ''));
if (!isNaN(an) && !isNaN(bn)) return asc ? an - bn : bn - an;
return asc ? av.localeCompare(bv, LANG === 'PT' ? 'pt' : LANG === 'EN' ? 'en' : 'tr') : bv.localeCompare(av, LANG === 'PT' ? 'pt' : LANG === 'EN' ? 'en' : 'tr');
});
rows.forEach(r => body.appendChild(r)); asc = !asc;
// avg satırı her zaman en altta kalsın
body.querySelectorAll('.tvip-tbl-avg,.tvip-heist-avg').forEach(r => body.appendChild(r));
});
});
});
};
// ─── TABLE SEARCH ─────────────────────────────────────────────────────────────
const applyPageSearch = () => {
if (!guard(K.tableTools)) return;
const deny = getTableDeny();
document.querySelectorAll('table').forEach((table, ti) => {
if (table.closest('#tvip-bar,.tvip-ov') || table.querySelector('.tvip-search-done')) return;
if (deny.includes(`${location.pathname}::${ti}`)) return;
const rows = table.querySelectorAll('tbody tr'); if (rows.length < 8) return;
const wrap = mk('div', 'tvip-search-wrap');
const inp = mk('input');
inp.style.cssText = 'padding:4px 8px;border:1px solid #ccc;border-radius:4px;font-size:12px;width:180px';
inp.placeholder = s('psPlh');
const info = mk('span'); info.style.cssText = 'font-size:11px;color:#666';
inp.addEventListener('input', () => {
const q = inp.value.trim().toLowerCase(); let count = 0;
rows.forEach(r => { const m = !q || r.textContent.toLowerCase().includes(q); r.style.display = m ? '' : 'none'; if (m) count++; });
info.textContent = q ? `${count} ${s('psCount')}` : '';
});
const hideBtn = mkB('🚫', 'btn-sm btn-grey', () => {
if (!confirm(s('tdConfirm'))) return;
addTableDeny(location.pathname, ti); wrap.remove();
});
hideBtn.title = s('tdHide');
wrap.append(inp, info, hideBtn); table.parentNode.insertBefore(wrap, table);
table.appendChild(mk('span', 'tvip-search-done'));
});
};
// ─── QUICK TOOLS ──────────────────────────────────────────────────────────────
// ─── TABLE AVG ────────────────────────────────────────────────────────────────
const applyTableAvg = () => {
if (!isOn(K.tableAvg)) return;
const table = document.querySelector('#tablefame');
if (table && !table.querySelector('.tvip-tbl-avg')) {
const rows = table.querySelectorAll('tbody tr');
let fameSum = 0, mcSum = 0, count = 0;
rows.forEach(row => {
const sl = row.querySelector("a[href*='/Help/Scoring/']");
if (sl) { const v = parseInt((sl.getAttribute('title')||'').replace('/26','').trim()); if (!isNaN(v)) fameSum += v; }
const bar = row.querySelector("div[class$='ProgressBar']");
if (bar) { const mv = bar.getAttribute('title')?.match(/(\d+)%/); if (mv) mcSum += parseInt(mv[1]); }
count++;
});
if (count) {
const row = mk('tr', 'even tvip-tbl-avg');
row.innerHTML = `<td>${s('taAvg')}</td><td>💫 ${(fameSum/count).toFixed(2)}</td><td>🔛 ${(mcSum/count).toFixed(2)}%</td>`;
table.querySelector('tbody').prepend(row);
}
}
if (!location.href.includes('CrewReconnaissance')) return;
const rows = document.querySelectorAll("tr[id*='repReconnaissance'][id*='trCrewMember']"); if (!rows.length) return;
let sum = 0, cnt = 0;
rows.forEach(r => { const sk = r.querySelector('span.sortkey'); if (sk) { const v = parseInt(sk.textContent); if (!isNaN(v)) { sum += v; cnt++; } } });
if (!cnt) return;
const tbody = rows[0].parentElement;
if (!tbody.querySelector('.tvip-heist-avg')) {
const row = mk('tr', 'tvip-heist-avg');
row.innerHTML = `<td colspan="2">${s('taDiscAvg')}</td><td class="width20">${(sum/cnt).toFixed(0)}%</td><td class="width10"></td>`;
tbody.prepend(row);
}
};
// ─── JAM HELPER ───────────────────────────────────────────────────────────────
const applyJamHelper = () => {
if (!guard(K.jamHelper, '/Artist/Repertoire/')) return;
if (!document.querySelector('a[href*="/Artist/BookingAssistant"]')) return;
const sel = "tr[id*='ctl00_cphLeftColumn_ctl01_repArtistRepertoire']";
const btn = mk('input'); btn.type = 'button'; btn.value = s('jamBtn');
btn.style.cssText = 'margin:4px 2px;padding:4px 10px;cursor:pointer;font-size:12px';
btn.onclick = () => {
document.querySelectorAll(sel).forEach(tr => {
const bar = tr.querySelector('td:nth-child(5) div[title]');
if (!bar) return;
const m = bar.getAttribute('title')?.match(/(\d{1,3})%/);
const cb = tr.querySelector('td:first-child input[type="checkbox"]');
if (cb) cb.checked = (m ? parseInt(m[1]) : 100) < 100;
});
};
document.querySelector('p.actionbuttons')?.prepend(btn);
};
// ─── REPERTOIRE FILTER ────────────────────────────────────────────────────────
const applyRepertoireFilter = () => {
if (!guard(K.repertoireF, '/Artist/Repertoire/') || document.querySelector('#tvip-rep-filter')) return;
const div = mk('div'); div.id = 'tvip-rep-filter';
const labels = [s('repAll'), s('repMarket'), s('repNoMarket'), s('repJam'), s('repSetlist'), s('repSecret')];
div.innerHTML = `<h3 style="margin-top:10px">${s('repTitle')}</h3>` +
labels.map((l, i) => `<label><input type="radio" name="tvipRepF" value="${i}"> ${l}</label><br>`).join('');
document.querySelector('div.box')?.insertAdjacentElement('beforebegin', div);
const checks = ['_', 'imgSongMarket', 'imgSongMarket', '_Rep_PracOn', '_imgDefaultSetlist', '_imgSecret'];
const invert = [true, true, false, true, true, true];
const rowSel = "tr[id^='ctl00_cphLeftColumn_ctl01_repArtistRepertoire_ct']";
const update = id => document.querySelectorAll(rowSel).forEach(tr => {
tr.style.display = invert[id]
? (tr.innerHTML.includes(checks[id]) ? '' : 'none')
: (tr.innerHTML.includes(checks[id]) ? 'none' : '');
});
div.querySelectorAll('input[name=tvipRepF]').forEach(r => r.addEventListener('change', () => update(parseInt(r.value))));
};
// ─── QUICK SEND ITEMS ─────────────────────────────────────────────────────────
const applyQuickSend = () => {
if (!guard(K.sendItems, '/Character/OfferItem/')) return;
const waitFor = (cb, tries = 0) => {
const dd = document.getElementById('ctl00_cphLeftColumn_ctl00_ddlItem');
const bn = document.getElementById('ctl00_cphLeftColumn_ctl00_btnGive');
if (dd && bn) cb(dd, bn); else if (tries < 20) setTimeout(() => waitFor(cb, tries + 1), 500);
};
waitFor((dropdown, btnGive) => {
// Resume if running
if (GM_getValue(DK.SI_STOP, '') === '1') {
const cnt = parseInt(GM_getValue(DK.SI_QUEUE, '0'));
GM_deleteValue(DK.SI_QUEUE); GM_deleteValue(DK.SI_NAME); GM_deleteValue(DK.SI_TOTAL); GM_deleteValue(DK.SI_STOP);
alert(sf.siStopped(cnt));
location.reload(); return;
}
const name = GM_getValue(DK.SI_NAME, '');
const total = parseInt(GM_getValue(DK.SI_TOTAL, '0'));
let counter = parseInt(GM_getValue(DK.SI_QUEUE, '0'));
if (name && counter < total) {
const opt = [...dropdown.options].find(o => o.text === name);
if (!opt) {
GM_deleteValue(DK.SI_QUEUE); GM_deleteValue(DK.SI_NAME); GM_deleteValue(DK.SI_TOTAL);
alert(`✅ ${counter}/${total}`); return;
}
const wrap = mk('div', 'tvip-send-wrap tvip-send-running');
const info = mk('div'); info.style.cssText = 'font-size:12px;margin-bottom:8px';
info.innerHTML = sf.siSending(counter, total, name);
const stopBtn = mkB(s('siStop'), 'btn-r btn-sm', () => {
GM_setValue(DK.SI_STOP, '1'); stopBtn.disabled = true; stopBtn.textContent = s('siStopping');
});
wrap.append(info, stopBtn);
btnGive.parentElement?.appendChild(wrap);
dropdown.value = opt.value; counter++;
GM_setValue(DK.SI_QUEUE, String(counter));
setTimeout(() => btnGive.click(), 500); return;
}
if (name && counter >= total) {
GM_deleteValue(DK.SI_QUEUE); GM_deleteValue(DK.SI_NAME); GM_deleteValue(DK.SI_TOTAL);
alert(s('siDone')); location.reload(); return;
}
// Setup UI
const wrap = mk('div', 'tvip-send-wrap tvip-send-setup');
btnGive.parentElement?.appendChild(wrap);
const update = () => {
const sel = dropdown.options[dropdown.selectedIndex]; if (!sel) return;
const same = [...dropdown.options].filter(o => o.text === sel.text);
wrap.innerHTML = '';
if (same.length <= 1) { wrap.appendChild(mk('em', '', s('siWarning'))).style.cssText = 'color:#999;font-size:12px'; return; }
const info = mk('div', '', `"${sel.text}" — ${same.length}`); info.style.cssText = 'font-size:12px;margin-bottom:8px';
const cRow = mk('div'); cRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:8px';
const lbl = mk('span', '', s('siCount') + ' '); lbl.style.fontSize = '12px';
const num = mk('input'); num.type = 'number'; num.min = 1; num.max = same.length; num.value = same.length;
num.style.cssText = 'width:60px;padding:3px 5px;border:1px solid #ccc;border-radius:3px;font-size:12px';
cRow.append(lbl, num);
const bRow = mk('div'); bRow.style.cssText = 'display:flex;gap:6px';
const sendB = mkB(s('siSend'), 'btn-b btn-sm', () => {
const cnt = Math.max(1, Math.min(parseInt(num.value) || 1, same.length));
if (!confirm(sf.siConfirm(sel.text, cnt))) return;
GM_setValue(DK.SI_NAME, sel.text); GM_setValue(DK.SI_TOTAL, String(cnt)); GM_setValue(DK.SI_QUEUE, '0');
location.reload();
});
const canB = mkB(s('siCancel'), 'btn-grey btn-sm', () => wrap.remove());
bRow.append(sendB, canB);
wrap.append(info, cRow, bRow);
};
update(); dropdown.addEventListener('change', () => setTimeout(update, 100));
});
};
// ─── BULK OFFER ───────────────────────────────────────────────────────────────
const SEL_DD = '#ctl00_cphLeftColumn_ctl00_ddlItem';
const SEL_PRICE = '#ctl00_cphLeftColumn_ctl00_txtPriceTag';
const SEL_BTN = '#ctl00_cphLeftColumn_ctl00_btnGive';
const SEL_FORM = '#ctl00_cphLeftColumn_ctl00_updMain';
const removeWarningBox = () => {
const h2 = [...document.querySelectorAll('.box h2')].find(h => h.textContent.includes('Quem avisa') || h.textContent.includes('Uyarı') || h.textContent.includes('Warning'));
h2?.closest('.box')?.remove();
};
const cleanHideOfferedCheckbox = () => {
const hide = CK.get(K.hideOffBox) === '1';
const deliveryParent = document.querySelector('#ctl00_cphLeftColumn_ctl00_chkDelivery')?.parentElement;
if (!deliveryParent) return;
const p = document.createElement('p');
const chk = document.createElement('input'); chk.type = 'checkbox'; chk.id = 'tvip-hideoff-chk'; chk.checked = hide;
const lbl = document.createElement('label'); lbl.htmlFor = 'tvip-hideoff-chk';
lbl.style.cssText = 'font-weight:normal;color:#333;font-size:12px;margin-left:4px';
lbl.textContent = s('hideOfferedChk');
chk.addEventListener('change', () => { CK.set(K.hideOffBox, chk.checked ? '1' : '0'); location.reload(); });
p.append(chk, lbl); deliveryParent.before(p);
const sel = document.querySelector(SEL_DD);
if (hide && sel) getOfferedList().forEach(id => sel.querySelector(`option[value="${id}"]`)?.remove());
// Track manual offers so the list stays accurate
document.querySelector(SEL_BTN)?.addEventListener('click', () => {
if (!chk.checked || !sel) return;
const id = sel.value; if (!id || id === '-1') return;
const l = getOfferedList(); if (!l.includes(id)) { l.push(id); saveOfferedList(l); }
});
};
const cleanOfferedOnItemsPage = () => {
const offered = getOfferedList(); if (!offered.length) return;
const box = [...document.querySelectorAll('.box h2')]
.find(h => h.textContent.includes('Itens que você está ofertando') || h.textContent.includes('Items you are offering') || h.textContent.includes('Teklif ettiğiniz'))
?.closest('.box');
if (!box) { saveOfferedList([]); return; }
const currentIDs = [...box.querySelectorAll('a[href*="/Character/ItemDetails/"]')]
.map(a => { const m = a.href.match(/\/ItemDetails\/(\d+)/); return m ? m[1] : null; }).filter(Boolean);
if (!currentIDs.length) { saveOfferedList([]); return; }
saveOfferedList(offered.filter(id => currentIDs.includes(id)));
};
const setupBulkOfferUI = () => {
if (!guard(K.bulkOffer, '/Character/OfferItem/')) return;
const formBox = document.querySelector(SEL_FORM)?.closest('.box');
if (!formBox || document.getElementById('tvip-bulk-offer-ui')) return;
removeWarningBox();
cleanHideOfferedCheckbox();
const ui = mk('div'); ui.id = 'tvip-bulk-offer-ui';
ui.innerHTML = `
<h3 style="margin:0 0 8px;font-size:13px">${s('boTitle')}</h3>
<div class="tvip-bulk-panel">
<div class="tvip-bulk-grid">
<div class="tvip-bulk-field"><label>${s('boItemName')}</label><input type="text" id="tvip-bo-name" placeholder="Analgés..."></div>
<div class="tvip-bulk-field"><label>${s('boQty')}</label><input type="number" id="tvip-bo-qty" min="1" value="1"></div>
<div class="tvip-bulk-field" style="grid-column:1/-1"><label>${s('boPrice')}</label><input type="number" id="tvip-bo-price" min="0" step="1" value="0"></div>
</div>
<div class="tvip-bulk-actions">
<button id="tvip-bo-start" class="tvip-btn-start">${s('boStart')}</button>
<button id="tvip-bo-stop" class="tvip-btn-stop">${s('boStop')}</button>
</div>
<div id="tvip-bo-status" class="tvip-status">${s('boReady')}</div>
</div>`;
formBox.insertBefore(ui, formBox.firstChild);
const setDisabled = on => {
document.getElementById('tvip-bo-start').disabled = on;
document.getElementById('tvip-bo-stop').disabled = !on;
document.getElementById('tvip-bo-name').disabled = on;
document.getElementById('tvip-bo-qty').disabled = on;
document.getElementById('tvip-bo-price').disabled = on;
};
const processNext = () => {
if (!GM_getValue(DK.BO_RUNNING, false)) { setDisabled(false); return; }
let items = [];
try { items = JSON.parse(GM_getValue(DK.BO_ITEMS, '[]')); } catch {}
const status = document.getElementById('tvip-bo-status');
if (!items.length) { status.textContent = s('boDone'); stopOffer(); return; }
const dropdown = document.querySelector(SEL_DD);
const offerBtn = document.querySelector(SEL_BTN);
const priceInp = document.querySelector(SEL_PRICE);
if (!dropdown || !offerBtn) { status.textContent = s('boCritErr'); stopOffer(); return; }
const item = items.shift();
const price = GM_getValue(DK.BO_PRICE, 0);
const total = parseInt(GM_getValue(DK.BO_TOTAL, '0'));
const count = total - items.length;
if (priceInp) priceInp.value = String(price);
GM_setValue(DK.BO_ITEMS, JSON.stringify(items));
status.textContent = sf.boOfferring(count, total, item.text);
dropdown.value = item.value;
if (dropdown.value !== item.value) {
status.textContent = sf.boSkip(item.text);
setTimeout(processNext, 1000); return;
}
// Track offered item
const l = getOfferedList(); if (!l.includes(item.value)) { l.push(item.value); saveOfferedList(l); }
setTimeout(() => {
if (!GM_getValue(DK.BO_RUNNING, false)) { setDisabled(false); return; }
offerBtn.click();
}, 2000);
};
const stopOffer = () => {
GM_deleteValue(DK.BO_ITEMS); GM_deleteValue(DK.BO_RUNNING);
GM_deleteValue(DK.BO_PRICE); GM_deleteValue(DK.BO_TOTAL);
setDisabled(false);
};
document.getElementById('tvip-bo-start').onclick = () => {
const nameVal = document.getElementById('tvip-bo-name').value.trim();
const qtyVal = parseInt(document.getElementById('tvip-bo-qty').value, 10);
const priceVal = parseInt(document.getElementById('tvip-bo-price').value, 10);
const status = document.getElementById('tvip-bo-status');
if (!nameVal) { status.textContent = s('boErrName'); return; }
if (isNaN(qtyVal) || qtyVal < 1) { status.textContent = s('boErrQty'); return; }
if (isNaN(priceVal) || priceVal < 0) { status.textContent = s('boErrPrice'); return; }
const found = [...document.querySelectorAll(`${SEL_DD} option`)]
.filter(o => o.value && o.value !== '-1' && o.text.trim().toLowerCase().startsWith(nameVal.toLowerCase()))
.map(o => ({ value: o.value, text: o.text.trim() }));
if (!found.length) { status.textContent = sf.boNoItem(nameVal); return; }
const toOffer = found.slice(0, qtyVal);
status.textContent = sf.boFound(found.length, toOffer.length, priceVal);
GM_setValue(DK.BO_PRICE, priceVal);
GM_setValue(DK.BO_ITEMS, JSON.stringify(toOffer));
GM_setValue(DK.BO_TOTAL, String(toOffer.length));
GM_setValue(DK.BO_RUNNING, 'true');
setDisabled(true);
setTimeout(processNext, 300);
};
document.getElementById('tvip-bo-stop').onclick = () => {
const status = document.getElementById('tvip-bo-status');
status.textContent = s('boStopped'); stopOffer();
};
// Resume on page reload
if (GM_getValue(DK.BO_RUNNING, false)) {
setDisabled(true);
document.getElementById('tvip-bo-status').textContent = s('boResumed');
setTimeout(processNext, 500);
} else { setDisabled(false); }
};
// ─── BULK ACCEPT + BULK CANCEL ────────────────────────────────────────────────
const setupBulkAcceptUI = () => {
if (!guard(K.bulkAccept, '/Character/ItemsOffered')) return;
if (document.getElementById('tvip-bulk-accept-ui')) return;
cleanOfferedOnItemsPage();
const findBox = txt => [...document.querySelectorAll('.box')]
.find(b => b.querySelector('h2')?.textContent.includes(txt));
const incomingBox = findBox('Teklif edilen eşyalar') || findBox('Items you are being offered') || findBox('Itens sendo ofertados');
const outgoingBox = findBox('Teklif ettiğiniz') || findBox('Items you are offering') || findBox('Itens que você está ofertando');
const parseCurrency = str => parseInt((str || '').replace(/\./g, '').split(',')[0].replace(/[^\d]/g, ''), 10) || 0;
const getPrice = p => {
const m = p.textContent.match(/Tutar:\s*([\d.,]+)\s*M\$|cost:\s*([\d.,]+)\s*M\$|custo:\s*([\d.,]+)\s*M\$/i);
return m ? parseCurrency(m[1] || m[2] || m[3]) : 0;
};
const isFree = p => /Tutar:\s*Bedava|cost:\s*Free|custo:\s*Grátis/i.test(p.textContent);
// ── ACCEPT UI ──
if (incomingBox) {
const ui = mk('div'); ui.id = 'tvip-bulk-accept-ui';
ui.innerHTML = `
<h3 style="margin:0 0 8px;font-size:13px">${s('baTitle')}</h3>
<div class="tvip-bulk-panel">
<div class="tvip-bulk-grid">
<div class="tvip-bulk-field"><label>${s('baItemName')}</label><input type="text" id="tvip-ba-name" placeholder="${s('baItemPlh')}"></div>
<div class="tvip-bulk-field"><label>${s('baMaxPrice')}</label><input type="number" id="tvip-ba-price" min="0" step="1" value="55000"></div>
</div>
<div class="tvip-bulk-actions">
<button id="tvip-ba-start" class="tvip-btn-start">${s('baStart')}</button>
<button id="tvip-ba-stop" class="tvip-btn-stop">${s('baStop')}</button>
</div>
<div id="tvip-ba-status" class="tvip-status">${s('baReady')}</div>
<div id="tvip-ba-spent" class="tvip-spent">${sf.baSpent(0)}</div>
</div>`;
incomingBox.insertBefore(ui, incomingBox.firstChild);
const setDisabledA = on => {
document.getElementById('tvip-ba-start').disabled = on;
document.getElementById('tvip-ba-stop').disabled = !on;
document.getElementById('tvip-ba-name').disabled = on;
document.getElementById('tvip-ba-price').disabled = on;
};
const processNextAccept = () => {
if (!GM_getValue(DK.BA_RUNNING, false)) { setDisabledA(false); return; }
const maxP = parseInt(GM_getValue(DK.BA_MAX_PRICE, '0'));
const tgtName = GM_getValue(DK.BA_ITEM_NAME, '');
const count = parseInt(GM_getValue(DK.BA_COUNT, '0'));
const spent = parseInt(GM_getValue(DK.BA_SPENT, '0'));
const status = document.getElementById('tvip-ba-status');
const spentEl = document.getElementById('tvip-ba-spent');
const box = findBox('Teklif edilen eşyalar') || findBox('Items you are being offered') || findBox('Itens sendo ofertados');
if (!box) { status.textContent = s('baNoSection'); stopAccept(); return; }
const paragraphs = [...box.querySelectorAll('p.nobmargin')].filter(p => p.querySelector('a[id*="lnkItem"]'));
if (!paragraphs.length) { status.textContent = sf.baDone(count, spent); stopAccept(); return; }
let found = false;
for (const p of paragraphs) {
const itemName = p.querySelector('a[id*="lnkItem"]')?.textContent.trim() || ''; if (!itemName) continue;
const price = isFree(p) ? 0 : getPrice(p);
const priceLabel = price ? `${price} M$` : s('baFree');
if ((tgtName === '' || itemName.toLowerCase().includes(tgtName)) && price <= maxP) {
const newCount = count + 1, newSpent = spent + price;
GM_setValue(DK.BA_COUNT, String(newCount)); GM_setValue(DK.BA_SPENT, String(newSpent));
status.textContent = sf.baAccepting(newCount, itemName, priceLabel);
if (spentEl) spentEl.textContent = sf.baSpent(newSpent);
found = true;
const btn = p.querySelector('input[id*="btnAcceptAndPay"], input[name*="btnAcceptAndPay"]');
if (!btn) continue;
setTimeout(() => {
if (!GM_getValue(DK.BA_RUNNING, false)) { setDisabledA(false); return; }
btn.click();
}, 2000);
break;
}
}
if (!found) { status.textContent = sf.baNoMatch(count, spent); stopAccept(); }
};
const stopAccept = () => {
GM_deleteValue(DK.BA_RUNNING); GM_deleteValue(DK.BA_MAX_PRICE);
GM_deleteValue(DK.BA_ITEM_NAME); GM_deleteValue(DK.BA_COUNT); GM_deleteValue(DK.BA_SPENT);
setDisabledA(false);
};
document.getElementById('tvip-ba-start').onclick = () => {
const price = parseInt(document.getElementById('tvip-ba-price').value.replace(/[.,]/g,''), 10);
const name = document.getElementById('tvip-ba-name').value.trim().toLowerCase();
if (isNaN(price) || price < 0) { document.getElementById('tvip-ba-status').textContent = s('baErrPrice'); return; }
document.getElementById('tvip-ba-status').textContent = sf.baInit(name, price);
document.getElementById('tvip-ba-spent').textContent = sf.baSpent(0);
GM_setValue(DK.BA_RUNNING, 'true'); GM_setValue(DK.BA_MAX_PRICE, price);
GM_setValue(DK.BA_ITEM_NAME, name); GM_setValue(DK.BA_COUNT, '0'); GM_setValue(DK.BA_SPENT, '0');
setDisabledA(true); setTimeout(processNextAccept, 300);
};
document.getElementById('tvip-ba-stop').onclick = () => {
const c = parseInt(GM_getValue(DK.BA_COUNT, '0')), sp = parseInt(GM_getValue(DK.BA_SPENT, '0'));
document.getElementById('tvip-ba-status').textContent = sf.baStopped(c, sp);
stopAccept();
};
if (GM_getValue(DK.BA_RUNNING, false)) {
setDisabledA(true);
document.getElementById('tvip-ba-status').textContent = s('baResumed');
document.getElementById('tvip-ba-spent').textContent = sf.baSpent(parseInt(GM_getValue(DK.BA_SPENT, '0')));
setTimeout(processNextAccept, 500);
} else { setDisabledA(false); }
}
// ── CANCEL UI ──
if (outgoingBox) {
const uic = mk('div'); uic.id = 'tvip-bulk-cancel-ui';
uic.innerHTML = `
<h3 style="margin:0 0 8px;font-size:13px">${s('bcTitle')}</h3>
<div class="tvip-bulk-panel">
<div class="tvip-bulk-grid">
<div class="tvip-bulk-field"><label>${s('bcItemName')}</label><input type="text" id="tvip-bc-name" placeholder="${s('baItemPlh')}"></div>
<div class="tvip-bulk-field">
<label>${s('bcFilter')}</label>
<select id="tvip-bc-filter" style="width:100%;padding:5px 6px;border:1px solid #ccc;border-radius:4px;font-size:12px">
<option value="all">${s('bcFilterAll')}</option>
<option value="free">${s('bcFilterFree')}</option>
<option value="paid">${s('bcFilterPaid')}</option>
</select>
</div>
</div>
<div class="tvip-bulk-actions">
<button id="tvip-bc-start" class="tvip-btn-start">${s('bcStart')}</button>
<button id="tvip-bc-stop" class="tvip-btn-stop">${s('bcStop')}</button>
</div>
<div id="tvip-bc-status" class="tvip-status">${s('bcReady')}</div>
</div>`;
outgoingBox.insertBefore(uic, outgoingBox.firstChild);
const setDisabledC = on => {
document.getElementById('tvip-bc-start').disabled = on;
document.getElementById('tvip-bc-stop').disabled = !on;
document.getElementById('tvip-bc-name').disabled = on;
document.getElementById('tvip-bc-filter').disabled = on;
};
const processNextCancel = () => {
if (!GM_getValue(DK.BC_RUNNING, false)) { setDisabledC(false); return; }
const tgtName = GM_getValue(DK.BC_ITEM_NAME, '');
const filter = GM_getValue(DK.BC_FILTER, 'all');
const count = parseInt(GM_getValue(DK.BC_COUNT, '0'));
const status = document.getElementById('tvip-bc-status');
const box = findBox('Teklif ettiğiniz') || findBox('Items you are offering') || findBox('Itens que você está ofertando');
if (!box) { status.textContent = s('bcNoSection'); stopCancel(); return; }
const paragraphs = [...box.querySelectorAll('p.nobmargin')].filter(p => p.querySelector('a[id*="lnkItem"]'));
if (!paragraphs.length) { status.textContent = sf.bcDone(count); stopCancel(); return; }
let found = false;
for (const p of paragraphs) {
const itemName = p.querySelector('a[id*="lnkItem"]')?.textContent.trim() || ''; if (!itemName) continue;
const free = isFree(p);
const matchesFilter = filter === 'all' || (filter === 'free' && free) || (filter === 'paid' && !free);
const matchesName = tgtName === '' || itemName.toLowerCase().includes(tgtName);
if (matchesFilter && matchesName) {
const newCount = count + 1;
GM_setValue(DK.BC_COUNT, String(newCount));
status.textContent = sf.bcCancelling(newCount, itemName);
found = true;
const btn = p.querySelector('input[id*="btnStop"], input[name*="btnStop"]');
if (!btn) continue;
setTimeout(() => {
if (!GM_getValue(DK.BC_RUNNING, false)) { setDisabledC(false); return; }
btn.click();
}, 2000);
break;
}
}
if (!found) { status.textContent = sf.bcNoMatch(count); stopCancel(); }
};
const stopCancel = () => {
GM_deleteValue(DK.BC_RUNNING); GM_deleteValue(DK.BC_ITEM_NAME);
GM_deleteValue(DK.BC_FILTER); GM_deleteValue(DK.BC_COUNT);
setDisabledC(false);
};
document.getElementById('tvip-bc-start').onclick = () => {
const name = document.getElementById('tvip-bc-name').value.trim().toLowerCase();
const filter = document.getElementById('tvip-bc-filter').value;
document.getElementById('tvip-bc-status').textContent = s('bcReady');
GM_setValue(DK.BC_RUNNING, 'true'); GM_setValue(DK.BC_ITEM_NAME, name);
GM_setValue(DK.BC_FILTER, filter); GM_setValue(DK.BC_COUNT, '0');
setDisabledC(true); setTimeout(processNextCancel, 300);
};
document.getElementById('tvip-bc-stop').onclick = () => {
const c = parseInt(GM_getValue(DK.BC_COUNT, '0'));
document.getElementById('tvip-bc-status').textContent = sf.bcStopped(c);
stopCancel();
};
if (GM_getValue(DK.BC_RUNNING, false)) {
setDisabledC(true);
setTimeout(processNextCancel, 500);
} else { setDisabledC(false); }
}
};
// ─── CITY SHORTCUTS ───────────────────────────────────────────────────────────
// CITY NAMES
const CITY_NAMES = {
"1":"Stockholm","5":"Londra","6":"New York","7":"Berlin","8":"Amsterdam",
"9":"Barselona","10":"Melbourne","11":"Nashville","14":"Los Angeles",
"16":"Toronto","17":"Buenos Aires","18":"Moskova","19":"Helsinki",
"20":"Paris","21":"Sao Paulo","22":"Kopenhag","23":"Roma","24":"Madrid",
"25":"Rio de Janeiro","26":"Tromsø","27":"Glasgow","28":"Vilnius",
"29":"Dubrovnik","30":"İstanbul","31":"Porto","32":"Mexico City",
"33":"Brüksel","34":"Tallinn","35":"Ankara","36":"Belgrad","38":"Montreal",
"39":"Singapur","42":"Budapeşte","45":"Şangay","46":"Bükreş","47":"İzmir",
"48":"Varşova","49":"Saraybosna","50":"Seattle","51":"Johannesburg",
"52":"Milano","53":"Sofya","54":"Manila","55":"Cakarta","56":"Kyiv",
"58":"Bakü","60":"Şikago","61":"Antalya","62":"Tokyo"
};
// CITY DATA
const CITY_DATA = {
"1": { locations: [ {id:"s",type:"shower",placeId:3160405,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49044, name:"Årsta Havsbad", dur:90 } ]},
"5": { locations: [ {id:"s",type:"shower",placeId:3161774,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:234234,name:"Herman's Palace", dur:5 } ]},
"6": { locations: [ {id:"s",type:"shower",placeId:2986433,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49087, name:"Cape Cod", dur:95 } ]},
"7": { locations: [ {id:"s",type:"shower",placeId:3231072,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:233224,name:"Schliemann's Zimmer",dur:10} ]},
"8": { locations: [ {id:"s",type:"shower",placeId:3161145,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49089, name:"Breskens", dur:90 } ]},
"9": { locations: [ {id:"s",type:"shower",placeId:3159414,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49090, name:"Costa Brava", dur:20 } ]},
"10": { locations: [ {id:"s",type:"shower",placeId:3198312,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49091, name:"Niney Mile Beach", dur:50 } ]},
"11": { locations: [ {id:"s",type:"shower",placeId:3245177,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:76469, name:"Little house on the Prairie",dur:10} ]},
"14": { locations: [ {id:"s",type:"shower",placeId:3196672,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49198, name:"Santa Monica Beach",dur:20 } ]},
"16": { locations: [ {id:"s",type:"shower",placeId:3268245,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49094, name:"Sunnyside", dur:15 } ]},
"17": { locations: [ {id:"s",type:"shower",placeId:3161537,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49095, name:"La Pampa", dur:90 } ]},
"18": { locations: [ {id:"s",type:"shower",placeId:3204377,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49096, name:"Волга", dur:120} ]},
"19": { locations: [ {id:"s",type:"shower",placeId:3237480,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49097, name:"Pyhäjärvi", dur:95 } ]},
"20": { locations: [ {id:"s",type:"shower",placeId:3162065,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:49098, name:"Charente", dur:65 } ]},
"21": { locations: [ {id:"s",type:"shower",placeId:3262551,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:53596, name:"Guarujá", dur:90 } ]},
"22": { locations: [ {id:"s",type:"shower",placeId:3204935,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:67582, name:"Gilleleje", dur:95 } ]},
"23": { locations: [ {id:"s",type:"shower",placeId:3181531,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:72404, name:"Ostia Lido", dur:50 } ]},
"24": { locations: [ {id:"s",type:"shower",placeId:3162492,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:103128,name:"La Rioja", dur:50 } ]},
"25": { locations: [ {id:"s",type:"shower",placeId:3199641,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:104742,name:"Ipanema", dur:20 } ]},
"26": { locations: [ {id:"s",type:"shower",placeId:3203190,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:106202,name:"Telegrafbukta", dur:15 } ]},
"27": { locations: [ {id:"s",type:"shower",placeId:3205233,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:182793,name:"Öğretmenin evi", dur:5 } ]},
"28": { locations: [ {id:"s",type:"shower",placeId:3220498,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:122919,name:"Merkys", dur:90 } ]},
"29": { locations: [ {id:"s",type:"shower",placeId:3220722,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:131991,name:"Korcula", dur:95 } ]},
"30": { locations: [ {id:"s",type:"shower",placeId:3160535,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:137942,name:"Gala Gölü", dur:90 } ]},
"31": { locations: [ {id:"s",type:"shower",placeId:2986566,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:140964,name:"Costa Verde", dur:20 } ]},
"32": { locations: [ {id:"s",type:"shower",placeId:3218344,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:170268,name:"Acapulco", dur:90 } ]},
"33": { locations: [ {id:"s",type:"shower",placeId:2965425,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:188643,name:"Blankenberge", dur:95 } ]},
"34": { locations: [ {id:"s",type:"shower",placeId:3222559,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:195084,name:"Pirita", dur:15 } ]},
"35": { locations: [ {id:"s",type:"shower",placeId:3198434,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:249590,name:"Hatay", dur:65 } ]},
"36": { locations: [ {id:"s",type:"shower",placeId:3218479,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:282985,name:"Srebrno", dur:95 } ]},
"38": { locations: [ {id:"s",type:"shower",placeId:3198981,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:358359,name:"St Lawrence River",dur:95 } ]},
"39": { locations: [ {id:"s",type:"shower",placeId:3222154,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:473018,name:"Sentosa", dur:36 } ]},
"42": { locations: [ {id:"s",type:"shower",placeId:3231282,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:653963,name:"Tisza", dur:90 } ]},
"45": { locations: [ {id:"s",type:"shower",placeId:3255714,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:670043,name:"Putuo Shan", dur:90 } ]},
"46": { locations: [ {id:"s",type:"shower",placeId:3198948,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:773546,name:"Constanţa", dur:95 } ]},
"47": { locations: [ {id:"s",type:"shower",placeId:3204448,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:782567,name:"Urla", dur:90 } ]},
"48": { locations: [ {id:"s",type:"shower",placeId:3231449,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:847919,name:"Wielkopolskie", dur:100} ]},
"49": { locations: [ {id:"s",type:"shower",placeId:3177850,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:1174002,name:"Pliva", dur:95 } ]},
"50": { locations: [ {id:"s",type:"shower",placeId:3023079,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:1349118,name:"Elliott Bay Park", dur:20 } ]},
"51": { locations: [ {id:"s",type:"shower",placeId:3202857,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:1845324,name:"St Lucia", dur:90 } ]},
"52": { locations: [ {id:"s",type:"shower",placeId:3218697,name:"Duş Evi",dur:0}, {id:"p",type:"path",placeId:1886305,name:"Lago di Garda", dur:95 } ]},
"53": { locations: [ {id:"s",type:"shower",placeId:3201807,name:"Duş Evi",dur:0} ]},
"54": { locations: [ {id:"s",type:"shower",placeId:3188092,name:"Duş Evi",dur:0} ]},
"55": { locations: [ {id:"s",type:"shower",placeId:3222289,name:"Duş Evi",dur:0} ]},
"56": { locations: [ {id:"s",type:"shower",placeId:3187003,name:"Duş Evi",dur:0} ]},
"58": { locations: [ {id:"s",type:"shower",placeId:3230603,name:"Duş Evi",dur:0} ]},
"60": { locations: [ {id:"s",type:"shower",placeId:3200260,name:"Duş Evi",dur:0} ]},
"61": { locations: [ {id:"s",type:"shower",placeId:3263617,name:"Duş Evi",dur:0} ]},
"62": { locations: [ {id:"s",type:"shower",placeId:3227018,name:"Duş Evi",dur:0} ]},
};
// CITY SHORTCUTS — helper
const getCityData = () => {
const custom = (() => { try { return JSON.parse(GM_getValue('tvip_city_custom','null')); } catch { return null; } })();
if (!custom) return CITY_DATA;
const merged = {};
Object.keys({...CITY_DATA,...custom}).forEach(id => {
merged[id] = custom[id] ?? CITY_DATA[id];
});
return merged;
};
const typeIcon = t => t==='shower'?'🚿':t==='path'?'🌿':'📍';
const typeLabel = t => t==='shower'?s('csShower'):t==='path'?s('csPath'):s('csOther');
const PM = '/World/Popmundo.aspx';
const applyCityShortcuts = () => {
if (!guard(K.cityShortcuts)) return;
const airport = document.getElementById('ctl00_cphLeftColumn_ctl00_lnkAirport'); if (!airport) return;
const cityDd = document.getElementById('ctl00_cphRightColumn_ctl01_ddlCities'); if (!cityDd) return;
const props = getCityData()[cityDd.value]; if (!props) return;
const tbody = airport.closest('tr')?.parentElement; if (!tbody) return;
props.locations.forEach(loc => {
const tr = document.createElement('tr');
const label = `${typeIcon(loc.type)} ${loc.name}`;
const durStr = loc.type==='path' && loc.dur ? ` — ${loc.dur} ${s('csMin')}` : '';
tr.innerHTML = `
<td>${typeLabel(loc.type)}:</td>
<td><a href="${PM}/Locale/${loc.placeId}">${label}</a>${durStr}</td>
<td class="right"><a class="icon" href="${PM}/Locale/MoveToLocale/${loc.placeId}"><img src="../../../Static/Icons/movetolocale.png" alt="" style="border:0"></a></td>`;
tbody.appendChild(tr);
});
};
// CITY MANAGER MODAL
const openCityManager = () => {
const cityDd = document.getElementById('ctl00_cphRightColumn_ctl01_ddlCities');
const activeCityId = cityDd?.value || Object.keys(CITY_DATA)[0];
mkModal('🏙️ ' + s('cityMgr'), (cont) => {
// Şehir dropdown
const topRow = mk('div'); topRow.style.cssText='display:flex;gap:6px;align-items:center;margin-bottom:10px';
const citySelect = mk('select'); citySelect.style.cssText='flex:1;padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:12px';
const allCityIds = [...new Set([...Object.keys(CITY_DATA), ...Object.keys((() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })())])].sort((a,b)=>+a-+b);
allCityIds.forEach(id => {
const o = mk('option','', CITY_NAMES[id] ? `${CITY_NAMES[id]} (ID ${id})` : `ID ${id}`); o.value=id;
if (id===activeCityId) o.selected=true;
citySelect.appendChild(o);
});
const resetBtn = mkB(s('cityMgrReset'),'btn-sm btn-r',() => {
if (!confirm(s('cityMgrResetQ'))) return;
const custom = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
delete custom[citySelect.value];
GM_setValue('tvip_city_custom', JSON.stringify(custom));
renderList();
});
topRow.append(citySelect, resetBtn);
cont.appendChild(topRow);
// Liste
const listWrap = mk('div'); cont.appendChild(listWrap);
// Ekle formu
const formWrap = mk('div'); formWrap.style.cssText='border-top:1px solid #eee;margin-top:8px;padding-top:8px';
formWrap.appendChild(mk('div','tvip-sec', s('cityMgrAdd')));
const fRow1 = mk('div'); fRow1.style.cssText='display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:6px';
const typeSel = mk('select'); typeSel.style.cssText='padding:4px;border:1px solid #ccc;border-radius:4px;font-size:11px';
[['shower',s('csShower')],['path',s('csPath')],['other',s('csOther')]].forEach(([v,l])=>{ const o=mk('option','',`${typeIcon(v)} ${l}`);o.value=v;typeSel.appendChild(o); });
const placeIdInp = mk('input'); placeIdInp.type='number'; placeIdInp.placeholder='Mekan ID'; placeIdInp.style.cssText='padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:11px;width:100%;box-sizing:border-box';
fRow1.append(typeSel, placeIdInp);
const fRow2 = mk('div'); fRow2.style.cssText='display:grid;grid-template-columns:1fr auto auto;gap:6px;margin-bottom:6px;align-items:end';
const nameInp = mk('input'); nameInp.type='text'; nameInp.placeholder=s('cityMgrName'); nameInp.style.cssText='padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:11px;width:100%;box-sizing:border-box';
const durInp = mk('input'); durInp.type='number'; durInp.placeholder='dk'; durInp.min='0'; durInp.style.cssText='padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:11px;width:52px';
const durLbl = mk('span','','dk'); durLbl.style.cssText='font-size:11px;color:#888;white-space:nowrap';
// dur satırı sadece path seçilince göster
const durWrap = mk('div'); durWrap.style.cssText='display:flex;align-items:center;gap:4px';
durWrap.append(durInp, durLbl);
const toggleDur = () => { durWrap.style.display = typeSel.value==='path'?'flex':'none'; };
typeSel.addEventListener('change', toggleDur); toggleDur();
fRow2.append(nameInp, durWrap);
const addBtn = mkB('+ '+s('cityMgrAddBtn'),'btn-g btn-sm',() => {
const cityId = citySelect.value;
const pid = parseInt(placeIdInp.value);
const nm = nameInp.value.trim();
if (!pid || !nm) return;
const custom = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
if (!custom[cityId]) custom[cityId] = { locations: JSON.parse(JSON.stringify((CITY_DATA[cityId]?.locations)||[])) };
custom[cityId].locations.push({ id:'c'+Date.now(), type:typeSel.value, placeId:pid, name:nm, dur: typeSel.value==='path'?parseInt(durInp.value)||0:0 });
GM_setValue('tvip_city_custom', JSON.stringify(custom));
placeIdInp.value=''; nameInp.value=''; durInp.value='';
renderList();
});
formWrap.append(fRow1, fRow2, addBtn);
cont.appendChild(formWrap);
const renderList = () => {
listWrap.innerHTML='';
const cityId = citySelect.value;
const custom = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
const base = CITY_DATA[cityId]?.locations || [];
const locs = custom[cityId]?.locations ?? base;
const isCustom= !!custom[cityId];
if (isCustom) {
const tag = mk('span','', '✏️ ' + s('cityMgrCustom')); tag.style.cssText='font-size:10px;color:#6f42c1;display:block;margin-bottom:6px';
listWrap.appendChild(tag);
}
locs.forEach((loc, i) => {
const row = mk('div'); row.style.cssText='display:flex;align-items:center;gap:6px;padding:5px 6px;background:#f8f9fa;border-radius:4px;margin-bottom:3px;border:1px solid #e8e8e8;font-size:12px';
const ico = mk('span','', typeIcon(loc.type)); ico.style.flexShrink='0';
const info = mk('span','', `${loc.name} (${loc.placeId})${loc.type==='path'&&loc.dur?' — '+loc.dur+' dk':''}`); info.style.flex='1';
const editB = mkB('✏','btn-sm btn-grey',() => {
const nName = prompt(s('cityMgrName')+':',loc.name); if(nName===null) return;
const nId = prompt('Mekan ID:',String(loc.placeId)); if(nId===null) return;
const nDur = loc.type==='path' ? (prompt('Dakika:',String(loc.dur))||'0') : '0';
const c2 = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
if (!c2[cityId]) c2[cityId]={ locations: JSON.parse(JSON.stringify(base)) };
c2[cityId].locations[i] = {...loc, name:nName.trim()||loc.name, placeId:parseInt(nId)||loc.placeId, dur:parseInt(nDur)||0};
GM_setValue('tvip_city_custom', JSON.stringify(c2)); renderList();
});
const delB = mkB('🗑','btn-sm btn-r',() => {
if (!confirm(s('cityMgrDelQ'))) return;
const c2 = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
if (!c2[cityId]) c2[cityId]={ locations: JSON.parse(JSON.stringify(base)) };
c2[cityId].locations.splice(i,1);
GM_setValue('tvip_city_custom', JSON.stringify(c2)); renderList();
});
row.append(ico, info, editB, delB); listWrap.appendChild(row);
});
if (!locs.length) { const e=mk('div','',s('cityMgrEmpty')); e.style.cssText='color:#999;font-size:12px'; listWrap.appendChild(e); }
};
citySelect.addEventListener('change', renderList);
renderList();
});
};
// EN strings for PopControl export
window.__ppcStrHelper = {"menuTitle": "🎨 Helper", "save": "✔ Save", "readMe": "📖 Read Me", "langLabel": "Language", "backup": "📤 Backup", "restore": "📥 Restore", "backupOk": "Backup created.", "restoreOk": "Restored.", "restoreErr": "Invalid file.", "restoreQ": "What to do with current data?", "mergeLbl": "Merge", "replaceLbl": "Replace", "cancelLbl": "Cancel", "close": "Close", "secGeneral": "GENERAL", "secTable": "TABLE", "secItems": "ITEMS", "secMusic": "MUSIC", "secNav": "NAVIGATION", "pb": "📊 Show % on bars", "alignLeft": "⬅️ Align page to left", "tableTools": "🔍 Table sort & search", "itemId": "🏷️ Item ID — Appends ID number next to item links", "colorScoring": "🌈 Coloured Scoring System — Adds a coloured badge next to fame links (0-26)", "autoDelivery": "✅ Auto Delivery — Automatically checks delivery confirmation checkboxes", "ticketPricer": "🎟️ Ticket Pricer — Shows recommended ticket price on artist invite page", "imageCtrl": "🖼️ Slow/Broken Image Control — 5s timeout; placeholder + click to retry", "tableAvg": "📈 Add average row to tables", "itemFilters": "🎒 Show Only Takeable Items", "jamHelper": "🎸 Select incomplete songs for Jam", "repertoireF": "🎵 Filter repertoire by category", "sendItems": "📦 Auto-send same item multiple times", "bulkOffer": "🏷️ Offer items in bulk at set price", "bulkAccept": "🛒 Auto-accept incoming offers", "cityShortcuts": "🏙️ Show shower house & paths in city", "bbMinimize": "—", "bbRestore": "🎮", "psPlh": "Search table...", "psCount": "results", "tdConfirm": "Hide the search box for this table?", "tdHide": "Hide search", "tdClear": "Reset Hidden Tables", "btnShowAll": "👁 Show All Items", "btnOnlyTake": "🔒 Only Takeable", "resetOffered": "🗑️ Reset Offer List", "confirmReset": "Reset offered items list?", "resetDone": "Done.", "hideOfferedChk": "Hide offered items", "jamBtn": "⏱ Jam <100%", "repAll": "All Songs", "repMarket": "Market Songs", "repNoMarket": "Non-Market", "repJam": "Jam Songs", "repSetlist": "Setlist Songs", "repSecret": "Secret Songs", "repTitle": "🎵 Repertoire Filter", "siWarning": "⚠️ Select an item with multiple copies", "siCount": "How many?", "siSend": "🔄 Send", "siCancel": "❌ Cancel", "siStop": "⏹ Stop", "siStopping": "Stopping...", "siDone": "✅ Done!", "boTitle": "Bulk Offer", "boItemName": "Item name (prefix):", "boQty": "Quantity:", "boPrice": "Price (M$):", "boStart": "▶ Offer", "boStop": "■ Stop", "boReady": "Ready.", "boDone": "All offers completed!", "boStopped": "Stopped by user.", "boResumed": "Reloaded, resuming...", "boCritErr": "Critical error: Page elements gone.", "boErrName": "Error: Enter item name.", "boErrQty": "Error: Invalid quantity.", "boErrPrice": "Error: Invalid price.", "baTitle": "Bulk Accept", "baItemName": "Item Name (Optional):", "baItemPlh": "Leave blank for all", "baMaxPrice": "Max Price (M$):", "baStart": "▶ Accept", "baStop": "■ Stop", "baReady": "Ready.", "baNoSection": "Offer section not found.", "baResumed": "Reloaded, resuming...", "baErrPrice": "Error: Invalid price.", "baFree": "Free", "bcTitle": "Bulk Cancel", "bcItemName": "Item Name (Optional):", "bcFilter": "Filter:", "bcFilterAll": "All", "bcFilterFree": "Free Only", "bcFilterPaid": "Paid Only", "bcStart": "▶ Cancel", "bcStop": "■ Stop", "bcReady": "Ready.", "bcNoSection": "Offer section not found.", "csShower": "Shower House", "csPath": "Path", "csGoShower": "Go to shower house", "csGoPath": "Go to path", "csMin": "Minutes", "csOther": "Other Location", "cityMgr": "Manage City Shortcuts", "cityMgrReset": "↩ Reset", "cityMgrResetQ": "Reset this city to default?", "cityMgrAdd": "Add Location", "cityMgrAddBtn": "Add", "cityMgrName": "Location Name", "cityMgrCustom": "Customized", "cityMgrDelQ": "Delete this location?", "cityMgrEmpty": "No locations.", "taAvg": "🌍 Average", "taDiscAvg": "🎯 Heist Avg:"};
function _waitPC(cb,n){n=n||0;if(unsafeWindow.PopControl){cb();return;}if(n<20)setTimeout(function(){_waitPC(cb,n+1);},300);}
// ─── MENU ─────────────────────────────────────────────────────────────────────
const injectMenu = () => {
if (document.getElementById('tvip-bar')) return;
// ── PANEL ──
const hpanel = mk('div', 'tvip-hpanel');
hpanel.id = 'tvip-hpanel'; hpanel.style.display = 'none';
const checks = {};
const mkCheck = (ck, lk) => {
const lbl = mk('label', 'tvip-chk');
const chk = Object.assign(mk('input'), { type: 'checkbox', checked: isOn(ck) });
checks[ck] = chk; lbl.append(chk, mk('span', '', s(lk))); hpanel.appendChild(lbl);
};
const mkHr = () => hpanel.appendChild(mk('hr', 'tvip-hr'));
const mkSec = txt => hpanel.appendChild(mk('div', 'tvip-sec', txt));
mkSec(s('secGeneral'));
mkCheck(K.pb, 'pb');
mkCheck(K.alignLeft, 'alignLeft');
mkCheck(K.imageCtrl, 'imageCtrl');
mkHr();
mkSec(s('secTable'));
mkCheck(K.tableTools, 'tableTools');
mkCheck(K.itemId, 'itemId');
mkCheck(K.colorScoring, 'colorScoring');
mkCheck(K.tableAvg, 'tableAvg');
mkHr();
mkSec(s('secItems'));
mkCheck(K.itemFilters, 'itemFilters');
mkCheck(K.hideOffBox, 'hideOfferedChk');
mkCheck(K.autoDelivery, 'autoDelivery');
mkCheck(K.ticketPricer, 'ticketPricer');
mkCheck(K.sendItems, 'sendItems');
mkCheck(K.bulkOffer, 'bulkOffer');
mkCheck(K.bulkAccept, 'bulkAccept');
mkHr();
mkSec(s('secMusic'));
mkCheck(K.jamHelper, 'jamHelper');
mkCheck(K.repertoireF, 'repertoireF');
mkHr();
mkSec(s('secNav'));
mkCheck(K.cityShortcuts,'cityShortcuts');
hpanel.appendChild(mkB('🏙️ ' + s('cityMgr'), 'btn-sm btn-grey', () => openCityManager()));
mkHr();
// LANGUAGE SELECTOR
mkSec(s('langLabel'));
const langRow = mk('div', 'tvip-lang-row');
[['TR','🇹🇷 Türkçe'],['EN','🇬🇧 English'],['PT','🇧🇷 Português']].forEach(([code, label]) => {
const b = mk('button', 'tvip-lang-btn' + (LANG === code ? ' active' : ''), label);
b.onclick = () => { CK.set('ppm_lang', code); location.reload(); };
langRow.appendChild(b);
});
hpanel.appendChild(langRow);
mkHr();
// ACTION BUTTONS
const row1 = mk('div'); row1.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-top:4px';
const row2 = mk('div'); row2.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-top:4px';
const saveBtn = mkB(s('save'), 'btn-g', () => {
Object.entries(checks).forEach(([k, c]) => CK.set(k, c.checked ? '1' : '0'));
location.reload();
});
const readBtn = mk('a', 'btn-grey btn-sm', s('readMe'));
readBtn.href = 'https://rentry.org/HelperOku';
readBtn.target = '_blank'; readBtn.style.textDecoration = 'none';
row1.append(
saveBtn, readBtn,
mkB(s('backup'), 'btn-b btn-sm', () => dbExport()),
mkB(s('restore'), 'btn-grey btn-sm', () => dbImport())
);
const tdClearBtn = mkB(s('tdClear'), 'btn-sm btn-grey', () => { clearTableDeny(); alert(s('resetDone')); });
const resetOfferBtn = mkB(s('resetOffered'), 'btn-sm', () => {
if (confirm(s('confirmReset'))) { saveOfferedList([]); alert(s('resetDone')); }
});
resetOfferBtn.style.cssText = 'padding:2px 8px;background:none;color:#c0392b;border:1px solid #c0392b;border-radius:3px;cursor:pointer;font-size:11px';
row2.append(tdClearBtn, resetOfferBtn);
hpanel.append(row1, row2);
const closePanel = () => {
hpanel.style.display = 'none';
document.getElementById('tvip-helper-btn')?.classList.remove('active');
};
const togglePanel = () => {
const open = hpanel.style.display !== 'none';
hpanel.style.display = open ? 'none' : 'block';
};
// Modal — hem standalone bar hem PopControl butonu kullanır
const _isModalOpen = () => !!document.getElementById('tvip-modal-ov');
const closeModal = () => {
const ov = document.getElementById('tvip-modal-ov');
if (!ov) return;
document.body.appendChild(hpanel);
hpanel.style.cssText = 'display:none';
ov.remove();
};
const openModal = () => {
if (_isModalOpen()) { closeModal(); return; }
const novEl = document.createElement('div'); novEl.id = 'tvip-modal-ov';
novEl.style.cssText = [
'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99997',
'display:flex;align-items:center;justify-content:center',
'padding:20px;box-sizing:border-box'
].join(';');
// Reset hpanel inline style so CSS class takes over inside the overlay
hpanel.removeAttribute('style');
hpanel.style.cssText = [
'display:block!important;position:relative!important',
'top:auto!important;right:auto!important;left:auto!important',
'max-height:85vh;overflow-y:auto;border-radius:10px',
'min-width:240px;max-width:500px;width:92%;box-sizing:border-box'
].join(';');
novEl.appendChild(hpanel);
novEl.addEventListener('click', function(e) { if (e.target === novEl) closeModal(); });
document.body.appendChild(novEl);
};
const togglePanelModal = () => _isModalOpen() ? closeModal() : openModal();
// ── KLASİK BAR ──
const bar = mk('div', 'tvip-bar'); bar.id = 'tvip-bar';
const lnk = (txt, fn) => { const a = mk('a', '', txt); a.href = '#'; a.onclick = e => { e.preventDefault(); fn(); }; return a; };
bar.appendChild(lnk(s('menuTitle'), togglePanelModal));
document.body.append(bar, hpanel);
// click-outside: sadece modal açık değilken hpanel'i kapat
document.addEventListener('click', e => {
if (_isModalOpen()) return;
if (!bar.contains(e.target) && !hpanel.contains(e.target)) closePanel();
});
// Auto-connect to PopControl if available
_waitPC(function() {
unsafeWindow.PopControl.register({
id: 'helper', icon: '🎨', label: 'Helper',
strings: window.__ppcStrHelper || {},
buttons: [{ icon: '🎨', label: 'Helper', onClick: togglePanelModal }],
onUndo: function() {
closeModal();
bar.style.display = '';
},
});
bar.style.display = 'none';
});
};
// ─── INIT ─────────────────────────────────────────────────────────────────────
applyAlignLeft();
applyProgressBar();
applyItemId();
applyColorScoring();
applyImageCtrl();
applyTableSort();
applyPageSearch();
applyAutoDelivery();
applyTicketPricer();
applyTableAvg();
applyJamHelper();
applyRepertoireFilter();
applyQuickSend();
setupBulkOfferUI();
setupBulkAcceptUI();
injectMenu();
if (location.href.includes('/Locale/ItemsEquipment/')) applyOnlyYours();
if (location.href.includes('/Character/OfferItem/')) applyHideOffered();
if (document.getElementById('ctl00_cphLeftColumn_ctl00_lnkAirport')) applyCityShortcuts();
})();