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.
// ==UserScript==
// @name 🎨 Helper
// @name:en 🎨 Helper
// @name:pt-BR 🎨 Helper
// @namespace popmundo.helper
// @version 6.0
// @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';
// 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,.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,.tvip-bulk-field select{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}
/* Remove spinners from number inputs */
input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
input[type=number]{-moz-appearance:textfield}
` }));
if (isMobile) document.head.appendChild(Object.assign(document.createElement('style'), { textContent: `
/* ── Touch targets ── */
.tvip-chk{padding:12px 6px;font-size:15px;gap:12px}
.tvip-chk input{width:22px;height:22px;flex-shrink:0}
.tvip-lang-btn{padding:12px 6px;font-size:14px;min-height:44px}
.btn-g,.btn-b,.btn-r,.btn-grey{padding:12px 20px;font-size:15px;min-height:44px}
.btn-sm{padding:10px 14px!important;font-size:14px!important;min-height:40px}
/* ── Sections ── */
.tvip-sec{font-size:13px;margin:14px 0 8px}
.tvip-hr{margin:12px 0}
/* ── Modal ── */
.tvip-box{width:98%!important;max-width:98%!important;padding:18px;max-height:90vh;min-height:auto}
.tvip-title{font-size:17px;margin-bottom:16px}
/* ── Bulk panels ── */
.tvip-bulk-grid{grid-template-columns:1fr!important}
.tvip-bulk-field input,.tvip-bulk-field select{font-size:15px;padding:10px 12px;min-height:44px}
.tvip-bulk-field label{font-size:14px;margin-bottom:6px}
.tvip-bulk-actions button{padding:12px;font-size:15px;min-height:44px}
.tvip-status,.tvip-spent{font-size:14px;padding:10px}
/* ── Table search ── */
.tvip-search-wrap input{font-size:15px;padding:10px 12px;width:100%;box-sizing:border-box;min-height:44px}
.tvip-search-wrap{flex-wrap:wrap;gap:8px}
/* ── Send wrap ── */
.tvip-send-wrap{padding:16px}
.tvip-send-wrap input[type=number]{font-size:15px;padding:8px 12px;min-height:44px}
/* ── City manager ── */
#tvip-bulk-offer-ui h3,#tvip-bulk-accept-ui h3,#tvip-bulk-cancel-ui h3{font-size:16px}
/* ── Mobile Header Fix ── */
.tvip-bar{top:50px!important;z-index:999999!important;font-size:13px;padding:4px 12px}
.tvip-bar a{padding:4px 8px}
/* ── Modal improvements ── */
.tvip-ov{padding:10px;box-sizing:border-box}
.tvip-hpanel{max-height:85vh;overflow-y:scroll}
/* ── Touch improvements ── */
.tvip-chk:hover,.tvip-lang-btn:hover,.btn-g:hover,.btn-b:hover,.btn-r:hover,.btn-grey:hover{background-color:rgba(0,0,0,0.05)}
/* ── Input focus for mobile ── */
input:focus,select:focus,textarea:focus{outline:2px solid #17a2b8;outline-offset:2px}
` }));
// 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 showGlobalStop = (onStop) => {
if (document.getElementById('tvip-global-stop')) return;
const bar = mk('div'); bar.id = 'tvip-global-stop';
bar.style.cssText = 'position:fixed;top:0;left:0;width:100%;background:#dc3545;color:#fff;text-align:center;padding:12px;z-index:999999;font-weight:bold;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.3);font-size:14px;';
bar.textContent = '⏹ ' + s('globalStop');
bar.onclick = () => { bar.remove(); if(onStop) onStop(); };
document.body.appendChild(bar);
};
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 DISPLAY', 'APARÊNCIA GERAL'),
secItems: _D('EŞYA', 'ITEMS', 'ITENS'),
// features
pb: _D('📊 Bar yüzdeleri gösterilir', '📊 Show % on bars', '📊 Mostrar % nas barras'),
alignLeft: _D('⬅️ Sayfa sola yaslanır','⬅️ Page aligned to left','⬅️ Página alinhada à esquerda'),
tableTools: _D('🔍 Tablolarda Arama & Sıralama & Ortalama','🔍 Table Search & Sort & Average','🔍 Busca & Ordenação & Média em tabelas'),
itemId: _D('🏷️ Eşya adlarına ID numarası ekle','🏷️ Add ID number next to item names','🏷️ Adiciona número de ID ao lado dos nomes'),
colorScoring: _D('🌈 Renkli Puanlama — Şöhret linklerine 0–26 rozet','🌈 Colour Scoring — 0–26 badge on fame links','🌈 Pontuação Colorida — emblema 0–26 nos links de fama'),
autoDelivery: _D('✅ Teslim onay kutularını otomatik işaretler','✅ Auto-checks delivery confirmation boxes','✅ Marca automaticamente caixas de entrega'),
ticketPricer: _D('🎟️ Sanatçı davet sayfasında önerilen bilet fiyatını göster','🎟️ Show suggested ticket price on invite page','🎟️ Mostra preço sugerido na página de convite'),
imageCtrl: _D('🖼️ Yavaş/Yüklenmeyen Resim — 5 sn, tıkla yeniden dene','🖼️ Slow/Broken Images — 5s, click to retry','🖼️ Imagens Lentas/Quebradas — 5s, clique para tentar novamente'),
itemFilters: _D('🎒 Sadece alınabilir eşyalar gösterilir', '🎒 Only takeable items shown','🎒 Exibir apenas itens disponíveis'),
repertoireF: _D('🎵 Repertuarı kategoriye göre filtreler','🎵 Filters repertoire by category','🎵 Filtra repertório por categoria'),
bulkOffer: _D('🏷️ TradeHub - Toplu teklif','🏷️ TradeHub - Bulk offer','🏷️ TradeHub - Oferta em massa'),
bulkAccept: _D('🛒 Gelen teklifleri otomatik kabul','🛒 Auto-accept incoming offers','🛒 Aceitar ofertas recebidas automaticamente'),
cityShortcuts: _D('🏙️ Şehirde duş evi ve patikalar görünür', '🏙️ 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'),
deleteConfirmChk: _D('🗑️ Onay penceresine eşya adlarını ekler','🗑️ Add item names to confirm dialog','🗑️ Adiciona nomes dos itens à confirmação'),
// 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'),
// bulk offer
boTitle: _D('Toplu Teklif', 'Bulk Offer', 'Oferta em Massa'),
globalStop: _D('TÜM İŞLEMLERİ DURDUR', 'STOP ALL PROCESSES', 'PARAR TODOS OS PROCESSOS'),
boNamePlh: _D('Örn: Ağrı kesici...', 'e.g., Painkiller...', 'Ex: Analgésico...'),
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'),
boFavorites: _D('⭐ Favoriler', '⭐ Favorites', '⭐ Favoritos'),
boCustomers: _D('👥 Müşteriler', '👥 Customers', '👥 Clientes'),
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.'),
// favorites modal
favTitle: _D('⭐ Favoriler', '⭐ Favorites', '⭐ Favoritos'),
favEmpty: _D('Henüz favori yok.', 'No favorites yet.', 'Nenhum favorito ainda.'),
favNameLbl: _D('Favori Adı:', 'Favorite Name:', 'Nome do Favorito:'),
favItemLbl: _D('Eşya Adı (prefix):', 'Item Name (prefix):', 'Nome do Item (prefixo):'),
favQtyLbl: _D('Adet:', 'Qty:', 'Qtd:'),
favPriceLbl: _D('Fiyat (M$):', 'Price (M$):', 'Preço (M$):'),
favAdd: _D('Ekle', 'Add', 'Adicionar'),
favUse: _D('Kullan', 'Use', 'Usar'),
favDel: _D('Sil', 'Delete', 'Excluir'),
favDelQ: _D('Bu favori silinsin mi?', 'Delete this favorite?', 'Excluir este favorito?'),
favFillErr: _D('Tüm alanları doldurun.', 'Fill in all fields.', 'Preencha todos os campos.'),
// customers modal
custTitle: _D('👥 Müşteri Kartları', '👥 Customer Cards', '👥 Cartões de Clientes'),
custEmpty: _D('Henüz müşteri yok.', 'No customers yet.', 'Nenhum cliente ainda.'),
custSearch: _D('Müşteri ara...', 'Search customer...', 'Buscar cliente...'),
custAdd: _D('+ Yeni Müşteri', '+ New Customer', '+ Novo Cliente'),
custNameLbl: _D('Ad:', 'Name:', 'Nome:'),
custUrlLbl: _D('Karakter URL:', 'Character URL:', 'URL do Personagem:'),
custNoteLbl: _D('Not:', 'Note:', 'Nota:'),
custSave: _D('Kaydet', 'Save', 'Salvar'),
custEdit: _D('Düzenle', 'Edit', 'Editar'),
custDel: _D('Sil', 'Delete', 'Excluir'),
custDelQ: _D('Bu müşteri silinsin mi?', 'Delete this customer?', 'Excluir este cliente?'),
custOffers: _D('Son Gönderimler', 'Recent Offers', 'Ofertas Recentes'),
custCharLink: _D('👤 Karakter', '👤 Character', '👤 Personagem'),
custNoNote: _D('Not yok.', 'No note.', 'Sem nota.'),
custNoOffers: _D('Henüz gönderim yok.', 'No offers yet.', 'Nenhuma oferta ainda.'),
custSaved: _D('Müşteri kaydedildi.', 'Customer saved.', 'Cliente salvo.'),
// log modal
boLogTitle: _D('📋 Toplu Teklif Logu', '📋 Bulk Offer Log', '📋 Log de Ofertas em Massa'),
boLogEmpty: _D('Henüz gönderim kaydı yok.', 'No offer records yet.', 'Nenhum registro ainda.'),
boLogClear: _D('Temizle', 'Clear', 'Limpar'),
boLogClearQ: _D('Tüm log kayıtları silinsin mi?', 'Clear all log records?', 'Limpar todos os registros?'),
// 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]),
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]),
deleteConfirmItems: names => ({ TR:`Silinecek eşyalar:\n${names.map(n=>'• '+n).join('\n')}\n\nDevam edilsin mi?`, EN:`Items to delete:\n${names.map(n=>'• '+n).join('\n')}\n\nProceed?`, PT:`Itens a excluir:\n${names.map(n=>'• '+n).join('\n')}\n\nContinuar?` }[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',
itemFilters: 'tvip_feat_itmf',
repertoireF: 'tvip_feat_repf',
bulkOffer: 'tvip_feat_bkoffer',
bulkAccept: 'tvip_feat_bkaccept',
cityShortcuts:'tvip_feat_citysc',
onlyYoursSt: 'tvip_oy_state',
hideOffBox: 'tvip_hideoff_chk',
deleteConfirm:'tvip_delete_confirm',
};
// 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',
BO_LOG: 'tvip_bo_log',
BO_CUSTOMERS: 'tvip_bo_customers',
BO_FAVORITES: 'tvip_bo_favorites',
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',
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',
BO_BATCH_ID: 'tvip_bo_batch_id',
BO_CUST_LOGS: 'tvip_bo_customer_logs',
};
// 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,
DK.BO_LOG, DK.BO_CUSTOMERS, DK.BO_FAVORITES, DK.BO_CUST_LOGS];
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
// FIX: Popmundo'nun disabled yaptığı kutucuğu zorla etkinleştirip işaretliyoruz.
// MutationObserver ile UpdatePanel yenilendiğinde de otomatik tekrar uygulanır.
const applyAutoDelivery = () => {
if (!isOn(K.autoDelivery)) return;
const tryCheck = () => {
document.querySelectorAll("input[type=checkbox][id$='chkDelivery']").forEach(cb => {
cb.removeAttribute('disabled');
cb.checked = true;
});
};
tryCheck();
if (location.href.includes('/Character/OfferItem/')) {
// MutationObserver: UpdatePanel DOM değiştiğinde re-check (en güvenilir yöntem)
const observeTarget = document.getElementById('ctl00_cphLeftColumn_ctl00_updMain')
|| document.querySelector('form')
|| document.body;
const mo = new MutationObserver(() => tryCheck());
mo.observe(observeTarget, { childList: true, subtree: true });
// Ek güvence: dropdown change olayında da çalıştır
const dd = document.getElementById('ctl00_cphLeftColumn_ctl00_ddlItem');
if (dd) dd.addEventListener('change', () => setTimeout(tryCheck, 300));
}
};
// 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));
// ── Ortak Fiyat Geçmişi okuma/yazma (pop_shared_prices — Depot ile paylaşılır) ──
const _spDateH = () => { const d=new Date(); return `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth()+1).padStart(2,'0')}.${String(d.getFullYear()).slice(2)}`; };
// Depot ile aynı key formatı: Sözlükten EN adını çöz (pop_cat_data), yoksa raw adı kullan
// ── Dropdown option text → {name, variant} ayırıcı ──
// "Fıkra (hoş) (3 kullanımlık)" → { name:"Fıkra (hoş)", variant:"3 kullanımlık" }
// "Painkiller (5 uses left.)" → { name:"Painkiller", variant:"5 uses left." }
// "Snowball" → { name:"Snowball", variant:"" }
const _splitItemVariant = (text) => {
if (!text) return { name: '', variant: '' };
// Son parantezdeki içerik: "N kullanımlık", "N kullanım", "N uses left.", "N uses"
const usageRe = /\s*\((\d+\s*(?:kullanımlık|kullanım|uses?\s*(?:left\.?)?|кратного|раза|uso[s]?))\)\s*$/i;
const m = text.match(usageRe);
if (m) {
return { name: text.slice(0, text.length - m[0].length).trim(), variant: m[1].trim() };
}
return { name: text.trim(), variant: '' };
};
let _catDictCache = null;
const _getCatDict = () => {
if (_catDictCache) return _catDictCache;
try {
const raw = GM_getValue('pop_cat_data', null);
if (!raw) return null;
_catDictCache = JSON.parse(raw).items || [];
return _catDictCache;
} catch { return null; }
};
const _spResolveKey = (name, variant) => {
const cleanV = (variant||'').replace(/\.\s*$/,'').trim();
try {
const dict = _getCatDict();
if (dict) {
const normN = _normItemName(name);
for (const d of dict) {
const langs = [d.en, d.tr, d.pt_br, d.it, d.es].filter(Boolean);
if (langs.some(l => _normItemName(l) === normN)) {
const enName = d.en || d.tr || name;
return `${enName}|||${cleanV}`;
}
}
}
} catch {}
return `${name}|||${cleanV}`;
};
const addSharedPriceH = (rawText, variantHint, priceVal) => {
if (!priceVal || priceVal < 10000) return;
try {
// Dropdown option text'ten isim ve varyantı ayır
const split = _splitItemVariant(rawText);
const itemName = split.name || rawText;
const variant = split.variant || variantHint || '';
const k = _spResolveKey(itemName, variant);
const dStr = _spDateH();
// pop_shared_prices'a yaz
const spRaw = GM_getValue('pop_shared_prices', null);
const sp = spRaw ? JSON.parse(spRaw) : {};
if (!sp[k]) sp[k] = { prices: [], last: priceVal, lastD: dStr };
if (!sp[k].prices.some(e => e.p === priceVal)) {
sp[k].prices.push({ p: priceVal, d: dStr });
if (sp[k].prices.length > 20) sp[k].prices.splice(0, sp[k].prices.length - 20);
}
sp[k].last = priceVal; sp[k].lastD = dStr;
GM_setValue('pop_shared_prices', JSON.stringify(sp));
// pop_price_data'ya da yaz (Depot uyumluluğu: aynı key ile orada da görünsün)
const pdRaw = GM_getValue('pop_price_data', null);
const pd = pdRaw ? JSON.parse(pdRaw) : {};
const priceStr = priceVal >= 1000000 ? (priceVal/1000000)+'m'
: priceVal >= 1000 ? (priceVal/1000)+'k'
: String(priceVal);
pd[k] = priceStr;
GM_setValue('pop_price_data', JSON.stringify(pd));
} catch {}
};
const getSharedPriceH = (rawText, variantHint) => {
try {
// isim + varyant ayır
const split = _splitItemVariant(rawText);
const itemName = split.name || rawText;
const variant = split.variant || variantHint || '';
// Denenecek key kombinasyonları
const keys = [ _spResolveKey(itemName, variant) ];
if (variant) keys.push(_spResolveKey(itemName, ''));
// raw text fallback
const rawK = rawText.trim() + '|||';
if (!keys.includes(rawK)) keys.push(rawK);
// pop_shared_prices
const spRaw = GM_getValue('pop_shared_prices', null);
if (spRaw) {
const sp = JSON.parse(spRaw);
for (const k of keys) { if (sp[k]) return sp[k]; }
}
// Fallback: pop_price_data (Depot'un eski fiyatları + Helper'ın pop_price_data'ya yazdıkları)
const pdRaw = GM_getValue('pop_price_data', null);
if (pdRaw) {
const pd = JSON.parse(pdRaw);
for (const k of keys) {
const rawPrice = pd[k];
if (rawPrice) {
const v = String(rawPrice).trim().toLowerCase().replace(',','.');
let n = parseFloat(v);
if (isNaN(n)) continue;
if (v.endsWith('m')) n = Math.round(n * 1_000_000);
else if (v.endsWith('k')) n = Math.round(n * 1_000);
if (n >= 10000)
return { prices: [{ p: n, d: '—' }], last: n, lastD: '—' };
}
}
}
return null;
} catch { return null; }
};
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;
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'));
});
};
// TABLE AVG
const applyTableAvg = () => {
if (!isOn(K.tableTools)) 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);
}
};
// 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))));
};
// TRADEHUB CORE
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());
// Manuel teklif: btnGive click — log kaydı + delivery checkbox + offered list
document.querySelector(SEL_BTN)?.addEventListener('click', () => {
if (!sel) return;
const id = sel.value; if (!id || id === '-1') return;
// Offered list güncelle
if (chk.checked) {
const l = getOfferedList(); if (!l.includes(id)) { l.push(id); saveOfferedList(l); }
}
// Delivery checkbox — eğer AutoDelivery kapalıysa bile burada zorla uygula
setTimeout(() => {
document.querySelectorAll("input[type=checkbox][id$='chkDelivery']").forEach(cb => {
cb.removeAttribute('disabled');
cb.checked = true;
});
}, 100);
// Log kaydı — BulkOffer çalışıyorsa log yazma (processNext zaten yazıyor)
if (GM_getValue(DK.BO_RUNNING, false)) return;
const itemText = sel.options[sel.selectedIndex]?.textContent?.trim() || `ID:${id}`;
const priceEl = document.querySelector(SEL_PRICE);
// parsePriceStr kullan: "25.000" → 25000, parseInt ile 25 olarak yanlış parse ederdi
const price = parsePriceStr(priceEl?.value || '0');
// Ortak fiyat geçmişine yaz (10.000 M$ altı yoksay)
if (price >= 10000) addSharedPriceH(itemText, '', price);
const ci = getPageCharacterInfo();
const entry = {
timestamp: new Date().toISOString(),
itemName: itemText,
price,
qty: 1,
source: 'manual',
characterName: ci?.characterName || '',
characterId: ci?.characterId || '',
characterUrl: ci?.characterUrl || '',
};
const log = getLog();
log.push(entry);
if (log.length > 500) log.splice(0, log.length - 500);
saveLog(log);
// Müşteri logu — kayıtlı müşteriyse ekle
const customers = getCustomers();
const existingCustomer = customers.find(c =>
(ci?.characterId && c.characterId === ci.characterId) ||
(ci?.characterName && c.name.toLowerCase() === ci.characterName.toLowerCase())
);
if (existingCustomer) {
const custLogs = getCustomerLogs();
custLogs.push({
customerId: existingCustomer.id,
customerName: existingCustomer.name,
timestamp: entry.timestamp,
itemName: entry.itemName,
qty: 1, price, source: 'manual'
});
if (custLogs.length > 1000) custLogs.splice(0, custLogs.length - 1000);
saveCustomerLogs(custLogs);
}
});
};
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)));
};
// ── Fiyat parse yardımcıları ──
// "20k" → 20000, "1.5m" → 1500000, "2.000.000" → 2000000
const parsePriceStr = (v) => {
const norm2 = v.toString().trim().toLowerCase().replace(/\s/g, '').replace(/,/g, '.');
// Türkçe binlik nokta → kaldır (ama "1.5m" gibi ondalık noktayı koru)
// Strateji: k/m yoksa tüm noktaları kaldır; varsa sadece bin ayraçlarını kaldır
const hasSuffix = /[km]$/.test(norm2);
let clean;
if (hasSuffix) {
// "1.500k" → "1500k", "1.5m" → "1.5m" (ondalık olabilir)
const core = norm2.slice(0, -1);
const dots = (core.match(/\./g) || []).length;
// Birden fazla nokta → hepsi binlik ayraç → kaldır
// Tek nokta ve rakamlar → ondalık
const numeric = dots > 1 ? core.replace(/\./g, '') : core;
clean = numeric + norm2.slice(-1);
} else {
clean = norm2.replace(/\./g, '');
}
const m = clean.match(/^(\d+(?:\.\d+)?)([km]?)$/);
if (!m) return parseInt(clean.replace(/[^0-9]/g, '')) || 0;
const n = parseFloat(m[1]);
if (m[2] === 'k') return Math.round(n * 1_000);
if (m[2] === 'm') return Math.round(n * 1_000_000);
return Math.round(n);
};
const setupPriceInput = (inp) => {
inp.addEventListener('blur', () => {
const raw = parsePriceStr(inp.value);
inp.dataset.raw = String(raw);
inp.value = raw > 0 ? raw.toLocaleString('tr-TR') : '0';
});
inp.addEventListener('focus', () => {
const raw = parseInt(inp.dataset.raw || '0') || parsePriceStr(inp.value);
inp.value = raw > 0 ? String(raw) : '';
inp.select();
});
};
const getPriceVal = (inp) => {
return parseInt(inp.dataset.raw || '0') || parsePriceStr(inp.value);
};
// ── Sayfa karakter bilgisini çek ──
const getPageCharacterInfo = () => {
const urlM = location.href.match(/\/Character\/OfferItem\/(\d+)/);
if (!urlM) return null;
const characterId = urlM[1];
// PreviewReport linkini içeren h2 → ismi al
let characterName = '';
const reportLink = document.querySelector('a[href*="/PreviewReport/50/"]');
if (reportLink) {
const h2 = reportLink.closest('h2');
if (h2) characterName = h2.firstChild?.textContent?.trim() || '';
}
if (!characterName) {
const h2 = document.querySelector('.charPresBox h2, .ofauto h2');
if (h2) characterName = h2.firstChild?.textContent?.trim() || '';
}
const characterUrl = `/World/Popmundo.aspx/Character/OfferItem/${characterId}`;
return { characterName, characterId, characterUrl };
};
// TRADEHUB — MÜŞTERİ SİSTEMİ
// ── Müşteri sistemi ──
const getCustomers = () => { try { return JSON.parse(GM_getValue(DK.BO_CUSTOMERS, '[]')); } catch { return []; } };
const saveCustomers = cs => GM_setValue(DK.BO_CUSTOMERS, JSON.stringify(cs));
// Customer-specific logs (separate from main logs)
const getCustomerLogs = () => { try { return JSON.parse(GM_getValue(DK.BO_CUST_LOGS, '[]')); } catch { return []; } };
const saveCustomerLogs = logs => GM_setValue(DK.BO_CUST_LOGS, JSON.stringify(logs));
const saveCustomer = (name, characterId, characterUrl, note, logEntry = null) => {
const cs = getCustomers();
const existing = cs.find(c => c.name.toLowerCase() === name.toLowerCase() || (characterId && c.characterId === characterId));
const _appendCustLog = (custId, custName) => {
if (!logEntry) return;
const custLogs = getCustomerLogs();
custLogs.push({
customerId: custId,
customerName: custName,
timestamp: logEntry.timestamp,
itemName: logEntry.itemName,
qty: logEntry.qty || 1,
price: logEntry.price || 0,
source: logEntry.source || 'bulk',
batchNumber: logEntry.batchNumber,
totalInBatch: logEntry.totalInBatch,
batchId: logEntry.batchId
});
if (custLogs.length > 1000) custLogs.splice(0, custLogs.length - 1000);
saveCustomerLogs(custLogs);
};
if (existing) {
if (name) existing.name = name;
if (characterId) existing.characterId = characterId;
if (characterUrl) existing.characterUrl = characterUrl;
if (note !== undefined) existing.note = note;
existing.updatedAt = new Date().toISOString();
_appendCustLog(existing.id, existing.name);
} else {
const newCust = {
id: Date.now().toString(), name: name || '', characterId: characterId || '',
characterUrl: characterUrl || '', note: note || '',
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString()
};
cs.push(newCust);
_appendCustLog(newCust.id, newCust.name);
}
if (cs.length > 200) cs.splice(200);
saveCustomers(cs);
};
// ── Log yardımcıları ──
const getLog = () => { try { return JSON.parse(GM_getValue(DK.BO_LOG, '[]')); } catch { return []; } };
const saveLog = lg => GM_setValue(DK.BO_LOG, JSON.stringify(lg));
// Log'dan o müşteriye ait teklifleri bul (characterId veya isim eşleşmesi)
const getOffersForCustomer = (customer) => {
const log = getLog();
return log.filter(e =>
(customer.characterId && e.characterId === customer.characterId) ||
(customer.name && e.characterName && e.characterName.toLowerCase() === customer.name.toLowerCase())
);
};
// ── Müşteri arama: isim + ID + not + teklif edilen eşya ──
const searchCustomers = (query) => {
const cs = getCustomers();
if (!query) return cs;
const q = query.toLowerCase();
const log = getLog();
// Hangi characterId'lerde bu eşya geçiyor?
const matchingIds = new Set(
log.filter(e => (e.itemName || '').toLowerCase().includes(q)).map(e => e.characterId).filter(Boolean)
);
return cs.filter(c =>
c.name.toLowerCase().includes(q) ||
(c.characterId || '').includes(q) ||
(c.note || '').toLowerCase().includes(q) ||
(c.characterId && matchingIds.has(c.characterId))
);
};
// TRADEHUB — FAVORİLER
// ── Favori sistemi ──
const getFavorites = () => { try { return JSON.parse(GM_getValue(DK.BO_FAVORITES, '[]')); } catch { return []; } };
const saveFavorites = fs => GM_setValue(DK.BO_FAVORITES, JSON.stringify(fs));
const saveFavorite = (name, itemName, qty, price) => {
const fs = getFavorites();
const existing = fs.find(f => f.name.toLowerCase() === name.toLowerCase());
if (existing) {
Object.assign(existing, { itemName, qty, price, updatedAt: new Date().toISOString() });
} else {
fs.push({ id: Date.now().toString(), name, itemName, qty, price,
createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
}
if (fs.length > 100) fs.splice(100);
saveFavorites(fs);
};
// Favoriler boşsa 2 varsayılan şablonu yükle
const seedDefaultFavorites = () => {
if (getFavorites().length > 0) return;
const defaults = [
{ name: ({ TR:'💊 Ağrı Kesici ×10', EN:'💊 Painkiller ×10', PT:'💊 Analgésico ×10' }[LANG]),
itemName: ({ TR:'Ağrı kesici', EN:'Painkiller', PT:'Analgésico' }[LANG]),
qty: 10, price: 50000 },
{ name: ({ TR:'⚗️ Zehir & Afrodizyak ×10', EN:'⚗️ Poison & Aphrodisiac ×10', PT:'⚗️ Veneno & Afrodisíaco ×10' }[LANG]),
itemName: ({ TR:'Zehir, Afrodizyak', EN:'Poison, Aphrodisiac', PT:'Veneno, Afrodisíaco' }[LANG]),
qty: 10, price: 30000 },
];
defaults.forEach((d, i) => {
const fs = getFavorites();
fs.push({ id: (Date.now() + i).toString(), name: d.name, itemName: d.itemName,
qty: d.qty, price: d.price, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() });
saveFavorites(fs);
});
};
// ── Paylaşımlı form satırı yardımcısı ──
const _mkFormRow = (container, lbl, val, ph) => {
const w = mk('div'); w.style.cssText = 'margin-bottom:7px';
const l = mk('label', '', lbl); l.style.cssText = 'display:block;font-size:10px;font-weight:bold;color:#555;margin-bottom:2px';
const i = mk('input'); i.type = 'text'; i.value = val || ''; if (ph) i.placeholder = ph;
i.style.cssText = 'width:100%;padding:5px 8px;border:1px solid #ccc;border-radius:4px;font-size:12px;box-sizing:border-box';
w.append(l, i); container.appendChild(w); return i;
};
// ── Müşteri Ekleme Formu (log satırından) ──
const openAddFromLogModal = (logEntry, onSaved) => {
const modal = mk('div'); modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:999999;display:flex;align-items:center;justify-content:center;padding:12px;box-sizing:border-box';
const content = mk('div'); content.style.cssText = 'background:#fff;border-radius:8px;padding:18px;width:95vw;max-width:420px;box-sizing:border-box';
const title = mk('h3', '', ({ TR:'👤 Müşteri Olarak Ekle', EN:'👤 Add as Customer', PT:'👤 Adicionar como Cliente' }[LANG]));
title.style.cssText = 'margin:0 0 14px;font-size:14px;color:#333';
const nameInp = _mkFormRow(content, ({ TR:'Ad:', EN:'Name:', PT:'Nome:' }[LANG]), logEntry.characterName || '');
const urlInp = _mkFormRow(content, ({ TR:'Karakter Linki:', EN:'Character Link:', PT:'Link do Personagem:' }[LANG]), logEntry.characterUrl || '');
const offerInp = _mkFormRow(content, ({ TR:'Eşya & Fiyat:', EN:'Item & Price:', PT:'Item & Preço:' }[LANG]),
`${logEntry.itemName || ''} × ${logEntry.qty || 1} @ ${(logEntry.price || 0).toLocaleString('tr-TR')} M$`);
offerInp.readOnly = true; offerInp.style.background = '#f8f9fa';
const noteInp = _mkFormRow(content, ({ TR:'Not:', EN:'Note:', PT:'Nota:' }[LANG]), '');
const btns = mk('div'); btns.style.cssText = 'display:flex;gap:8px;margin-top:4px';
btns.append(
mkB(({ TR:'Kaydet', EN:'Save', PT:'Salvar' }[LANG]), 'btn-g', () => {
const name = nameInp.value.trim();
if (!name) { nameInp.style.borderColor = '#dc3545'; return; }
saveCustomer(name, logEntry.characterId || '', urlInp.value.trim(), noteInp.value.trim(), logEntry);
modal.remove();
if (onSaved) onSaved();
}),
mkB(({ TR:'İptal', EN:'Cancel', PT:'Cancelar' }[LANG]), 'btn-grey', () => modal.remove())
);
content.append(title, btns);
modal.appendChild(content);
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
};
// ── Customer History Modal ──
const openCustomerHistoryModal = (customer) => {
document.getElementById('tvip-cust-hist-modal')?.remove(); // Varsa kapat, yeniden aç
const modal = mk('div'); modal.id = 'tvip-cust-hist-modal'; modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box';
const content = mk('div'); content.style.cssText = 'background:#fff;border-radius:8px;padding:16px;width:clamp(340px,80vw,700px);max-height:88vh;display:flex;flex-direction:column;box-sizing:border-box';
const title = mk('h3', '', ({ TR:`📦 ${customer.name} — Tüm Teklifler`, EN:`📦 ${customer.name} — All Offers`, PT:`📦 ${customer.name} — Todas as Ofertas` }[LANG]));
title.style.cssText = 'margin:0 0 12px;font-size:14px;color:#333;flex-shrink:0';
const custLogs = getCustomerLogs()
.filter(log => log.customerId === customer.id)
.slice().reverse(); // En yeni önce
if (!custLogs.length) {
const msg = mk('p', '', ({ TR:'Bu müşteriye ait teklif kaydı bulunamadı.', EN:'No offer records found for this customer.', PT:'Nenhum registro encontrado.' }[LANG]));
msg.style.cssText = 'text-align:center;color:#aaa;font-style:italic;font-size:12px;margin:20px 0';
content.appendChild(msg);
} else {
// Özet satırı
const totalQty = custLogs.reduce((s, l) => s + (l.qty || 1), 0);
const totalPrice = custLogs.reduce((s, l) => s + (l.price || 0) * (l.qty || 1), 0);
const summary = mk('div');
summary.style.cssText = 'display:flex;gap:12px;background:#f0f8ff;border:1px solid #bee3f8;border-radius:5px;padding:8px 10px;margin-bottom:10px;font-size:11px;flex-shrink:0';
summary.innerHTML = `<span>📦 <b>${custLogs.length}</b> ${({ TR:'teklif', EN:'offers', PT:'ofertas' }[LANG])}</span><span>🔢 <b>${totalQty}</b> ${({ TR:'adet', EN:'qty', PT:'qtd' }[LANG])}</span><span>💰 <b>${totalPrice.toLocaleString('tr-TR')} M$</b> ${({ TR:'toplam', EN:'total', PT:'total' }[LANG])}</span>`;
content.appendChild(summary);
const tblWrap = mk('div');
tblWrap.style.cssText = 'overflow-y:auto;flex:1;min-height:0';
const tbl = document.createElement('table');
tbl.style.cssText = 'width:100%;border-collapse:collapse;font-size:11px;table-layout:fixed;word-break:break-word;';
const thead = document.createElement('thead');
thead.innerHTML = `<tr style="background:#f8f9fa;border-bottom:2px solid #dee2e6;position:sticky;top:0">
<th style="padding:5px 6px;text-align:left;color:#555;width:110px">${({ TR:'Tarih/Saat', EN:'Date/Time', PT:'Data/Hora' }[LANG])}</th>
<th style="padding:5px 6px;text-align:left;color:#555">${({ TR:'Eşya', EN:'Item', PT:'Item' }[LANG])}</th>
<th style="padding:5px 6px;text-align:right;color:#555;width:36px">${({ TR:'Adet', EN:'Qty', PT:'Qtd' }[LANG])}</th>
<th style="padding:5px 6px;text-align:right;color:#555;width:80px">${({ TR:'Fiyat', EN:'Price', PT:'Preço' }[LANG])}</th>
<th style="padding:5px 6px;text-align:center;color:#555;width:54px">${({ TR:'Kaynak', EN:'Src', PT:'Origem' }[LANG])}</th>
</tr>`;
tbl.appendChild(thead);
const tbody = document.createElement('tbody');
custLogs.forEach((log, i) => {
const d = new Date(log.timestamp);
const dateStr = `${d.getDate().toString().padStart(2,'0')}.${(d.getMonth()+1).toString().padStart(2,'0')}.${d.getFullYear().toString().slice(2)} ${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}:${d.getSeconds().toString().padStart(2,'0')}`;
const tr = document.createElement('tr');
tr.style.cssText = `border-bottom:1px solid #f0f0f0;${i%2===1?'background:#fafafa':''}`;
const tdDate = document.createElement('td'); tdDate.style.cssText = 'padding:4px 6px;color:#888;font-size:10px;white-space:nowrap'; tdDate.textContent = dateStr;
const tdItem = document.createElement('td'); tdItem.style.cssText = 'padding:4px 6px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:0'; tdItem.textContent = log.itemName || '-';
const tdQty = document.createElement('td'); tdQty.style.cssText = 'padding:4px 6px;text-align:right;color:#555;white-space:nowrap'; tdQty.textContent = log.qty || 1;
const tdPrice = document.createElement('td'); tdPrice.style.cssText = 'padding:4px 6px;text-align:right;color:#218838;white-space:nowrap;font-weight:500';
tdPrice.textContent = ((log.price || 0) * (log.qty || 1)).toLocaleString('tr-TR') + ' M$';
const tdSrc = document.createElement('td'); tdSrc.style.cssText = 'padding:4px 6px;text-align:center';
tdSrc.innerHTML = log.source === 'manual'
? `<span style="background:#fff3cd;color:#856404;padding:1px 5px;border-radius:3px;font-size:9px">M</span>`
: `<span style="background:#d4edda;color:#155724;padding:1px 5px;border-radius:3px;font-size:9px">T</span>`;
tr.append(tdDate, tdItem, tdQty, tdPrice, tdSrc);
tbody.appendChild(tr);
});
tbl.appendChild(tbody);
tblWrap.appendChild(tbl);
content.appendChild(tblWrap);
}
const closeBtn = mkB('✕ ' + ({ TR:'Kapat', EN:'Close', PT:'Fechar' }[LANG]), 'btn-grey', () => modal.remove());
closeBtn.style.cssText = 'width:100%;margin-top:12px;flex-shrink:0';
content.appendChild(closeBtn);
modal.appendChild(content);
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
};
// ── Müşteriler Modalı ──
const openCustomersModal = () => {
const modal = mk('div'); modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box';
const content = mk('div'); content.style.cssText = 'background:#fff;border-radius:8px;padding:12px;width:min(640px,96vw);max-height:90vh;overflow-y:auto;overflow-x:hidden;box-sizing:border-box;position:relative;';
const titleRow = mk('div'); titleRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:12px';
const titleEl = mk('h3', '', ({ TR:'👥 Müşteriler', EN:'👥 Customers', PT:'👥 Clientes' }[LANG]));
titleEl.style.cssText = 'margin:0;font-size:15px;color:#333;flex:1';
const searchInp = mk('input'); searchInp.type = 'text';
searchInp.placeholder = ({ TR:'İsim, ID, eşya, not...', EN:'Name, ID, item, note...', PT:'Nome, ID, item, nota...' }[LANG]);
searchInp.style.cssText = 'flex:2;padding:6px 8px;border:1px solid #ccc;border-radius:4px;font-size:12px';
titleRow.append(titleEl, searchInp);
content.appendChild(titleRow);
// ── BÖLÜM 1: Kayıtlı Müşteriler ──
const sec1Hdr = mk('div', '', ({ TR:'📋 Kayıtlı Müşteriler', EN:'📋 Registered Customers', PT:'📋 Clientes Registrados' }[LANG]));
sec1Hdr.style.cssText = 'font-size:11px;font-weight:bold;text-transform:uppercase;color:#888;letter-spacing:.5px;margin-bottom:6px';
content.appendChild(sec1Hdr);
const custListEl = mk('div');
custListEl.style.cssText = 'margin-bottom:16px';
content.appendChild(custListEl);
const formArea = mk('div'); formArea.style.cssText = 'margin-bottom:8px';
content.appendChild(formArea);
const addNewBtn = mkB(({ TR:'+ Yeni Müşteri', EN:'+ New Customer', PT:'+ Novo Cliente' }[LANG]), 'btn-g btn-sm', () => openAddCustForm());
addNewBtn.style.marginBottom = '12px';
content.appendChild(addNewBtn);
// ── BÖLÜM 2: Son 500 Teklif ──
const sec2Hdr = mk('div', '', ({ TR:'📦 Teklif Geçmişi (son 500)', EN:'📦 Offer History (last 500)', PT:'📦 Histórico (últimas 500)' }[LANG]));
sec2Hdr.style.cssText = 'font-size:11px;font-weight:bold;text-transform:uppercase;color:#888;letter-spacing:.5px;margin-bottom:6px;border-top:1px solid #e9ecef;padding-top:12px';
content.appendChild(sec2Hdr);
const logListEl = mk('div'); content.appendChild(logListEl);
const closeBtn = mkB('✕ ' + ({ TR:'Kapat', EN:'Close', PT:'Fechar' }[LANG]), 'btn-grey', () => modal.remove());
closeBtn.style.cssText = 'width:100%;margin-top:14px';
content.appendChild(closeBtn);
// Müşteri listesi renderer
const renderCustList = (query = '') => {
custListEl.innerHTML = '';
const list = searchCustomers(query);
if (!list.length) {
const msg = mk('p', '', ({ TR:'Müşteri bulunamadı.', EN:'No customers found.', PT:'Nenhum cliente encontrado.' }[LANG]));
msg.style.cssText = 'text-align:center;color:#aaa;font-style:italic;font-size:12px;margin:6px 0';
custListEl.appendChild(msg); return;
}
list.forEach(c => {
const card = mk('div'); card.style.cssText = 'display:flex;align-items:center;gap:8px;padding:7px 10px;border:1px solid #e9ecef;border-radius:5px;margin-bottom:5px;background:#fafafa';
const nameSpan = mk('span', '', c.name); nameSpan.style.cssText = 'font-weight:bold;font-size:12px;color:#333;flex:1;min-width:80px';
const idSpan = mk('span', '', c.characterId ? `#${c.characterId}` : '');
idSpan.style.cssText = 'font-size:10px;color:#aaa;min-width:60px';
const noteSpan = mk('span', '', c.note || '');
noteSpan.style.cssText = 'font-size:10px;color:#888;font-style:italic;flex:2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';
const btnRow = mk('div'); btnRow.style.cssText = 'display:flex;gap:3px;flex-shrink:0';
if (c.characterUrl) {
const lnk = mk('a', 'btn-sm btn-grey', '👤');
lnk.href = c.characterUrl; lnk.target = '_blank'; lnk.style.textDecoration = 'none';
lnk.title = c.characterUrl;
btnRow.appendChild(lnk);
// Bir Eşya Teklif Et butonu
const offerBtn = mk('a', 'btn-sm btn-g', ({ TR:'Bir Eşya Teklif Et', EN:'Offer an Item', PT:'Oferecer um Item' }[LANG]));
offerBtn.href = c.characterUrl.replace(/\/Character\/\d+/, (match) => {
const charId = match.match(/\d+/)[0];
return `/Character/OfferItem/${charId}`;
});
offerBtn.target = '_blank';
offerBtn.style.textDecoration = 'none';
offerBtn.style.marginLeft = '3px';
offerBtn.style.fontSize = '9px';
btnRow.appendChild(offerBtn);
}
const histBtn = mkB(({ TR:'Geçmiş Teklifler', EN:'Offer History', PT:'Histórico de Ofertas' }[LANG]), 'btn-sm btn-b', () => {
openCustomerHistoryModal(c);
});
histBtn.style.fontSize = '10px';
btnRow.append(
histBtn,
mkB(({ TR:'Düzenle', EN:'Edit', PT:'Editar' }[LANG]), 'btn-sm btn-b', () => openEditCustForm(c)),
mkB(({ TR:'Sil', EN:'Del', PT:'Exc' }[LANG]), 'btn-sm btn-r', () => {
if (!confirm(({ TR:`"${c.name}" silinsin mi?`, EN:`Delete "${c.name}"?`, PT:`Excluir "${c.name}"?` }[LANG]))) return;
saveCustomers(getCustomers().filter(x => x.id !== c.id));
renderCustList(searchInp.value.trim());
})
);
card.append(nameSpan, idSpan, noteSpan, btnRow);
custListEl.appendChild(card);
});
};
// Yeni müşteri formu
const openAddCustForm = () => {
formArea.innerHTML = '';
formArea.style.cssText = 'background:#f0f8ff;border:1px solid #bee3f8;border-radius:6px;padding:12px;margin-bottom:8px';
const ci = getPageCharacterInfo();
const nI = _mkFormRow(formArea, ({ TR:'Ad:', EN:'Name:', PT:'Nome:' }[LANG]), ci?.characterName || '');
const uI = _mkFormRow(formArea, ({ TR:'Eşya Teklif Linki:', EN:'Offer Item Link:', PT:'Link de Oferta:' }[LANG]), ci?.characterUrl || '');
const tI = _mkFormRow(formArea, ({ TR:'Not:', EN:'Note:', PT:'Nota:' }[LANG]), '');
const btns = mk('div'); btns.style.cssText = 'display:flex;gap:6px';
btns.append(
mkB(({ TR:'Kaydet', EN:'Save', PT:'Salvar' }[LANG]), 'btn-g btn-sm', () => {
const name = nI.value.trim();
if (!name) { nI.style.borderColor = '#dc3545'; return; }
saveCustomer(name, ci?.characterId || '', uI.value.trim(), tI.value.trim());
formArea.innerHTML = ''; formArea.style.cssText = 'margin-bottom:8px';
renderCustList(searchInp.value.trim());
}),
mkB(({ TR:'İptal', EN:'Cancel', PT:'Cancelar' }[LANG]), 'btn-grey btn-sm', () => { formArea.innerHTML = ''; formArea.style.cssText = 'margin-bottom:8px'; })
);
formArea.appendChild(btns);
};
const openEditCustForm = (c) => {
formArea.innerHTML = '';
formArea.style.cssText = 'background:#fffbe6;border:1px solid #f0c040;border-radius:6px;padding:12px;margin-bottom:8px';
const nI = _mkFormRow(formArea, ({ TR:'Ad:', EN:'Name:', PT:'Nome:' }[LANG]), c.name);
const uI = _mkFormRow(formArea, ({ TR:'Eşya Teklif Linki:', EN:'Offer Item Link:', PT:'Link de Oferta:' }[LANG]), c.characterUrl);
const tI = _mkFormRow(formArea, ({ TR:'Not:', EN:'Note:', PT:'Nota:' }[LANG]), c.note);
const btns = mk('div'); btns.style.cssText = 'display:flex;gap:6px';
btns.append(
mkB(({ TR:'Kaydet', EN:'Save', PT:'Salvar' }[LANG]), 'btn-g btn-sm', () => {
const cs = getCustomers(); const found = cs.find(x => x.id === c.id);
if (found) { found.name = nI.value.trim() || found.name; found.characterUrl = uI.value.trim(); found.note = tI.value.trim(); found.updatedAt = new Date().toISOString(); saveCustomers(cs); }
formArea.innerHTML = ''; formArea.style.cssText = 'margin-bottom:8px';
renderCustList(searchInp.value.trim());
}),
mkB(({ TR:'İptal', EN:'Cancel', PT:'Cancelar' }[LANG]), 'btn-grey btn-sm', () => { formArea.innerHTML = ''; formArea.style.cssText = 'margin-bottom:8px'; })
);
formArea.appendChild(btns);
};
// Log listesi renderer
const renderLogList = (query = '') => {
logListEl.innerHTML = '';
const log = getLog().slice().reverse(); // En yeni önde
const q = query.toLowerCase();
const filtered = q ? log.filter(e =>
(e.itemName || '').toLowerCase().includes(q) ||
(e.characterName || '').toLowerCase().includes(q) ||
(e.characterId || '').includes(q)
) : log;
const shown = filtered.slice(0, 500);
if (!shown.length) {
logListEl.appendChild(mk('p', '', ({ TR:'Teklif kaydı yok.', EN:'No offer records.', PT:'Sem registros.' }[LANG]))).style.cssText = 'text-align:center;color:#aaa;font-style:italic;font-size:12px;margin:6px 0';
return;
}
// Group by customer
const grouped = {};
shown.forEach(e => {
const key = e.characterName || 'Bilinmeyen';
if (!grouped[key]) {
grouped[key] = {
characterName: e.characterName,
characterUrl: e.characterUrl,
characterId: e.characterId,
items: [],
totalQty: 0,
totalPrice: 0
};
}
grouped[key].items.push(e);
grouped[key].totalQty += e.qty || 1;
grouped[key].totalPrice += (e.price || 0) * (e.qty || 1);
});
// Add buttons at the top
const btnRow = mk('div'); btnRow.style.cssText = 'display:flex;gap:8px;margin-bottom:10px;justify-content:flex-end';
const addCustBtn = mkB(({ TR:'+ Müşteri Ekle', EN:'+ Add Customer', PT:'+ Add Cliente' }[LANG]), 'btn-sm btn-g', () => openAddCustForm());
const clearLogBtn = mkB(({ TR:'Logları Temizle', EN:'Clear Logs', PT:'Limpar Logs' }[LANG]), 'btn-sm btn-r', () => {
if (!confirm(({ TR:'Tüm ana log kayıtları silinsin mi? (Müşteri kayıtları silinmez)', EN:'Clear all main log records? (Customer records will not be deleted)', PT:'Limpar todos os registros principais? (Registros de clientes não serão excluídos)' }[LANG]))) return;
saveLog([]); renderLogList(query);
});
btnRow.append(addCustBtn, clearLogBtn);
logListEl.appendChild(btnRow);
// Tablo
const tblWrap = document.createElement('div');
tblWrap.style.cssText = 'overflow-x:hidden;width:100%';
const tbl = document.createElement('table');
tbl.style.cssText = 'width:100%;border-collapse:collapse;font-size:11px;table-layout:fixed;word-break:break-word;';
const thead = document.createElement('thead');
thead.innerHTML = `<tr style="background:#f8f9fa;border-bottom:2px solid #dee2e6">
<th style="padding:5px 6px;text-align:left;color:#555;white-space:nowrap">${({ TR:'Tarih', EN:'Date', PT:'Data' }[LANG])}</th>
<th style="padding:5px 6px;text-align:left;color:#555">${({ TR:'Eşya', EN:'Item', PT:'Item' }[LANG])}</th>
<th style="padding:5px 6px;text-align:right;color:#555;white-space:nowrap">${({ TR:'Adet', EN:'Qty', PT:'Qtd' }[LANG])}</th>
<th style="padding:5px 6px;text-align:right;color:#555;white-space:nowrap">${({ TR:'Fiyat', EN:'Price', PT:'Preço' }[LANG])}</th>
<th style="padding:5px 6px;text-align:left;color:#555">${({ TR:'Karakter', EN:'Character', PT:'Personagem' }[LANG])}</th>
<th style="padding:5px 6px"></th>
</tr>`;
tbl.appendChild(thead);
const tbody = document.createElement('tbody');
Object.values(grouped).forEach((group, i) => {
const firstItem = group.items[0];
const d = new Date(firstItem.timestamp);
const dateStr = `${d.getDate().toString().padStart(2,'0')}.${(d.getMonth()+1).toString().padStart(2,'0')}.${d.getFullYear().toString().slice(2)}`;
const tr = document.createElement('tr');
tr.style.cssText = `border-bottom:1px solid #f0f0f0;${i%2===1?'background:#fafafa':''}`;
const tdDate = document.createElement('td'); tdDate.style.cssText = 'padding:4px 6px;white-space:nowrap;color:#888'; tdDate.textContent = dateStr;
const tdItem = document.createElement('td'); tdItem.style.cssText = 'padding:4px 6px;color:#333;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap'; tdItem.textContent = firstItem.itemName || '-';
const tdQty = document.createElement('td'); tdQty.style.cssText = 'padding:4px 6px;text-align:right;color:#555;white-space:nowrap'; tdQty.textContent = group.totalQty;
const tdPrice= document.createElement('td'); tdPrice.style.cssText = 'padding:4px 6px;text-align:right;color:#218838;white-space:nowrap;font-weight:500';
tdPrice.textContent = group.totalPrice.toLocaleString('tr-TR') + ' M$';
const tdChar = document.createElement('td'); tdChar.style.cssText = 'padding:4px 6px;max-width:140px';
if (group.characterName && group.characterUrl) {
const a = mk('a', '', group.characterName); a.href = group.characterUrl; a.target = '_blank';
a.style.cssText = 'color:#17a2b8;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;max-width:130px';
tdChar.appendChild(a);
} else {
tdChar.textContent = group.characterName || '-';
tdChar.style.color = '#aaa';
}
const tdAct = document.createElement('td'); tdAct.style.cssText = 'padding:4px 6px;text-align:right;white-space:nowrap';
const addB = mkB(({ TR:'+ Müşteri', EN:'+ Customer', PT:'+ Cliente' }[LANG]), 'btn-sm btn-g', () => {
openAddFromLogModal(firstItem, () => renderCustList(searchInp.value.trim()));
});
addB.style.fontSize = '10px';
tdAct.appendChild(addB);
tr.append(tdDate, tdItem, tdQty, tdPrice, tdChar, tdAct);
tbody.appendChild(tr);
});
tbl.appendChild(tbody);
tblWrap.appendChild(tbl);
logListEl.appendChild(tblWrap);
};
searchInp.addEventListener('input', () => {
const q = searchInp.value.trim();
renderCustList(q);
renderLogList(q);
});
modal.appendChild(content);
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
renderCustList();
renderLogList();
};
// ── Favoriler Modalı ──
const openFavoritesModal = (fillForm) => {
const modal = mk('div'); modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box';
const content = mk('div'); content.style.cssText = 'background:#fff;border-radius:8px;padding:16px;width:95vw;max-width:500px;max-height:85vh;overflow-y:auto;box-sizing:border-box';
const title = mk('h3', '', ({ TR:'⭐ Favoriler', EN:'⭐ Favorites', PT:'⭐ Favoritos' }[LANG])); title.style.cssText = 'margin:0 0 12px;font-size:15px;color:#333';
const addForm = mk('div'); addForm.style.cssText = 'background:#f8f9fa;border:1px solid #e9ecef;border-radius:6px;padding:12px;margin-bottom:12px';
const mkFld = (lbl, type) => {
const d = mk('div'); d.style.cssText = 'display:flex;flex-direction:column';
const l = mk('label', '', lbl); l.style.cssText = 'font-size:10px;font-weight:bold;color:#666;margin-bottom:2px';
const i = mk('input'); i.type = type;
i.style.cssText = 'padding:5px 6px;border:1px solid #ccc;border-radius:4px;font-size:12px;box-sizing:border-box';
d.append(l, i); return { wrap: d, inp: i };
};
const row1 = mk('div'); row1.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px';
const {wrap: wn, inp: nameInp} = mkFld(({ TR:'Favori Adı:', EN:'Favorite Name:', PT:'Nome do Favorito:' }[LANG]), 'text');
const {wrap: wi, inp: itemInp} = mkFld(({ TR:'Eşya Adı (prefix):', EN:'Item Name (prefix):', PT:'Nome do Item:' }[LANG]), 'text');
row1.append(wn, wi);
const row2 = mk('div'); row2.style.cssText = 'display:grid;grid-template-columns:80px 1fr auto;gap:8px;align-items:end;margin-bottom:8px';
const {wrap: wq, inp: qtyInp} = mkFld(({ TR:'Adet:', EN:'Qty:', PT:'Qtd:' }[LANG]), 'number');
const {wrap: wp, inp: priceInp} = mkFld(({ TR:'Fiyat:', EN:'Price:', PT:'Preço:' }[LANG]), 'text');
qtyInp.min = '1'; qtyInp.max = '100'; qtyInp.value = '1';
priceInp.placeholder = '50k / 2m...';
setupPriceInput(priceInp);
const addBtn = mkB(({ TR:'Ekle', EN:'Add', PT:'Adicionar' }[LANG]), 'btn-g btn-sm', () => {
const nm = nameInp.value.trim(), itm = itemInp.value.trim();
const qty = parseInt(qtyInp.value), price = getPriceVal(priceInp);
if (!nm || !itm || isNaN(qty) || qty < 1 || price < 0) { alert(({ TR:'Tüm alanları doldurun.', EN:'Fill in all fields.', PT:'Preencha todos.' }[LANG])); return; }
if (editingId) {
const fs = getFavorites(); const f = fs.find(x => x.id === editingId);
if (f) { f.name = nm; f.itemName = itm; f.qty = qty; f.price = price; f.updatedAt = new Date().toISOString(); saveFavorites(fs); }
clearEditMode();
} else {
saveFavorite(nm, itm, qty, price);
nameInp.value = ''; itemInp.value = ''; qtyInp.value = '1'; priceInp.value = '0'; priceInp.dataset.raw = '0';
}
renderFavList();
});
addBtn.style.alignSelf = 'flex-end';
row2.append(wq, wp, addBtn);
addForm.append(row1, row2);
const listEl = mk('div'); listEl.style.cssText = 'max-height:350px;overflow-y:auto';
let editingId = null;
const clearEditMode = () => {
editingId = null;
nameInp.value = ''; itemInp.value = ''; qtyInp.value = '1'; priceInp.value = '0'; priceInp.dataset.raw = '0';
addBtn.textContent = ({ TR:'Ekle', EN:'Add', PT:'Adicionar' }[LANG]);
addForm.style.borderColor = '';
};
const setEditMode = (fav) => {
editingId = fav.id;
nameInp.value = fav.name; itemInp.value = fav.itemName; qtyInp.value = fav.qty;
priceInp.dataset.raw = String(fav.price);
priceInp.value = fav.price > 0 ? fav.price.toLocaleString('tr-TR') : '0';
addBtn.textContent = ({ TR:'💾 Güncelle', EN:'💾 Update', PT:'💾 Atualizar' }[LANG]);
addForm.style.borderColor = '#f0c040';
addForm.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
};
const renderFavList = () => {
listEl.innerHTML = '';
const fs = getFavorites();
if (!fs.length) { listEl.appendChild(mk('p', '', ({ TR:'Henüz favori yok.', EN:'No favorites yet.', PT:'Nenhum favorito.' }[LANG]))).style.cssText = 'text-align:center;color:#999;font-style:italic;font-size:12px'; return; }
fs.forEach((fav, idx) => {
const row = mk('div'); row.style.cssText = 'display:flex;justify-content:space-between;align-items:center;background:#fff;border:1px solid #e9ecef;border-radius:5px;padding:8px 10px;margin-bottom:6px';
const info = mk('div');
info.innerHTML = `<strong style="font-size:12px;color:#333">${fav.name}</strong><div style="font-size:11px;color:#888">${fav.itemName || ({ TR:'(eşya adı girilmedi)', EN:'(no item name)', PT:'(sem nome)' }[LANG])} ×${fav.qty} @ ${fav.price > 0 ? fav.price.toLocaleString('tr-TR') + ' M$' : ({ TR:'Ücretsiz', EN:'Free', PT:'Grátis' }[LANG])}</div>`;
const btns = mk('div'); btns.style.cssText = 'display:flex;gap:4px';
btns.append(
mkB(({ TR:'Kullan', EN:'Use', PT:'Usar' }[LANG]), 'btn-sm btn-b', () => { if (fav.itemName === '__SEND_ALL__') {
// Tüm dropdown eşyalarını sepete ücretsiz ekle
modal.remove();
const allItems = filterItems('', 9999);
if (!allItems.length) {
alert(({ TR:'Teklif edilecek eşya bulunamadı.', EN:'No items available to offer.', PT:'Nenhum item disponível.' }[LANG]));
return;
}
const existingCart = getCart();
allItems.forEach(it => {
if (it.value) existingCart.push({ name: it.text, qty: 1, price: 0 });
});
saveCart(existingCart);
renderCart();
document.getElementById('tvip-bo-status').textContent =
({ TR:`${allItems.length} eşya sepete eklendi (0 M$).`, EN:`${allItems.length} items added to cart (free).`, PT:`${allItems.length} itens adicionados (grátis).` }[LANG]);
} else {
fillForm(fav.itemName, fav.qty, fav.price); modal.remove();
}
}),
mkB(({ TR:'Düzenle', EN:'Edit', PT:'Editar' }[LANG]), 'btn-sm btn-b', () => setEditMode(fav)),
mkB(({ TR:'Sil', EN:'Del', PT:'Exc' }[LANG]), 'btn-sm btn-r', () => {
if (!confirm(({ TR:'Bu favori silinsin mi?', EN:'Delete this favorite?', PT:'Excluir?' }[LANG]))) return;
const fs2 = getFavorites(); fs2.splice(idx, 1); saveFavorites(fs2);
if (editingId === fav.id) clearEditMode();
renderFavList();
})
);
row.append(info, btns); listEl.appendChild(row);
});
};
const closeBtn = mkB('✕ ' + ({ TR:'Kapat', EN:'Close', PT:'Fechar' }[LANG]), 'btn-grey', () => modal.remove());
closeBtn.style.cssText = 'width:100%;margin-top:12px';
content.append(title, addForm, listEl, closeBtn);
modal.appendChild(content);
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
renderFavList();
};
// TRADEHUB — LOG MODALı (Bulk Offer Log)
const openLogModal = () => {
const modal = mk('div'); modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box';
const content = mk('div'); content.style.cssText = 'background:#fff;border-radius:8px;padding:12px;width:min(640px,96vw);max-height:88vh;display:flex;flex-direction:column;box-sizing:border-box;overflow-x:hidden;';
const titleRow = mk('div'); titleRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:12px;flex-shrink:0';
const titleEl = mk('h3', '', s('boLogTitle'));
titleEl.style.cssText = 'margin:0;font-size:14px;color:#333;flex:1';
const clearBtn = mkB(({ TR:'Temizle', EN:'Clear', PT:'Limpar' }[LANG]), 'btn-sm btn-r', () => {
if (!confirm(({ TR:'Tüm log silinsin mi?', EN:'Clear all logs?', PT:'Limpar tudo?' }[LANG]))) return;
saveLog([]); modal.remove();
});
titleRow.append(titleEl, clearBtn);
content.appendChild(titleRow);
const log = getLog().slice().reverse();
if (!log.length) {
content.appendChild(mk('p', '', ({ TR:'Henüz gönderim kaydı yok.', EN:'No offer records yet.', PT:'Nenhum registro.' }[LANG]))).style.cssText = 'color:#aaa;font-style:italic;font-size:12px';
} else {
const tblWrap = mk('div'); tblWrap.style.cssText = 'overflow-y:auto;overflow-x:hidden;flex:1;min-height:0';
const tbl = document.createElement('table');
tbl.style.cssText = 'width:100%;border-collapse:collapse;font-size:11px;table-layout:fixed';
const thead = document.createElement('thead');
thead.innerHTML = `<tr style="background:#f0f0f0;border-bottom:2px solid #dee2e6;position:sticky;top:0">
<th style="padding:5px 8px;text-align:left;color:#555">#</th>
<th style="padding:5px 8px;text-align:left;color:#555">${({ TR:'Tarih/Saat', EN:'Date/Time', PT:'Data/Hora' }[LANG])}</th>
<th style="padding:5px 8px;text-align:left;color:#555">${({ TR:'Eşya', EN:'Item', PT:'Item' }[LANG])}</th>
<th style="padding:5px 8px;text-align:right;color:#555">${({ TR:'Adet', EN:'Qty', PT:'Qtd' }[LANG])}</th>
<th style="padding:5px 8px;text-align:right;color:#555">${({ TR:'Fiyat (M$)', EN:'Price (M$)', PT:'Preço (M$)' }[LANG])}</th>
<th style="padding:5px 8px;text-align:center;color:#555">${({ TR:'Kaynak', EN:'Source', PT:'Origem' }[LANG])}</th>
<th style="padding:5px 8px;text-align:left;color:#555">${({ TR:'Karakter', EN:'Character', PT:'Personagem' }[LANG])}</th>
</tr>`;
tbl.appendChild(thead);
// colgroup ile sütun genişlikleri sabit → yatay scroll yok
tbl.insertAdjacentHTML('afterbegin', `<colgroup>
<col style="width:30px">
<col style="width:120px">
<col style="width:auto">
<col style="width:36px">
<col style="width:80px">
<col style="width:58px">
<col style="width:110px">
</colgroup>`);
const tbody = document.createElement('tbody');
log.forEach((e, i) => {
const d = new Date(e.timestamp);
const dateStr = `${d.getDate().toString().padStart(2,'0')}.${(d.getMonth()+1).toString().padStart(2,'0')}.${d.getFullYear().toString().slice(2)} ${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}:${d.getSeconds().toString().padStart(2,'0')}`;
const tr = document.createElement('tr');
tr.style.cssText = `border-bottom:1px solid #f0f0f0;${i%2===1?'background:#fafafa':''}`;
const tdN = document.createElement('td'); tdN.style.cssText = 'padding:4px 6px;color:#aaa;font-size:10px'; tdN.textContent = i+1;
const tdDate = document.createElement('td'); tdDate.style.cssText = 'padding:4px 6px;color:#888;font-size:10px;white-space:nowrap'; tdDate.textContent = dateStr;
const tdItem = document.createElement('td'); tdItem.style.cssText = 'padding:4px 6px;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:0'; tdItem.textContent = e.itemName || '-';
const tdQty = document.createElement('td'); tdQty.style.cssText = 'padding:4px 6px;text-align:right;color:#555;white-space:nowrap'; tdQty.textContent = e.qty || 1;
const tdPrice = document.createElement('td'); tdPrice.style.cssText = 'padding:4px 6px;text-align:right;color:#218838;font-weight:500;white-space:nowrap';
tdPrice.textContent = e.price ? e.price.toLocaleString('tr-TR') : '-';
const tdSrc = document.createElement('td'); tdSrc.style.cssText = 'padding:4px 6px;text-align:center';
tdSrc.innerHTML = e.source === 'manual'
? `<span style="background:#fff3cd;color:#856404;padding:1px 5px;border-radius:3px;font-size:9px">M</span>`
: `<span style="background:#d4edda;color:#155724;padding:1px 5px;border-radius:3px;font-size:9px">T</span>`;
const tdChar = document.createElement('td'); tdChar.style.cssText = 'padding:4px 6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:0';
if (e.characterName && e.characterUrl) {
const a = mk('a', '', e.characterName); a.href = e.characterUrl; a.target = '_blank';
a.style.cssText = 'color:#17a2b8;text-decoration:none';
tdChar.appendChild(a);
} else {
tdChar.textContent = e.characterName || '-'; tdChar.style.color = '#aaa';
}
tr.append(tdN, tdDate, tdItem, tdQty, tdPrice, tdSrc, tdChar);
tbody.appendChild(tr);
});
tbl.appendChild(tbody);
tblWrap.appendChild(tbl);
content.appendChild(tblWrap);
}
const closeBtn = mkB('✕ ' + ({ TR:'Kapat', EN:'Close', PT:'Fechar' }[LANG]), 'btn-grey', () => modal.remove());
closeBtn.style.cssText = 'margin-top:12px;width:100%;flex-shrink:0';
content.appendChild(closeBtn);
modal.appendChild(content);
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
};
// TRADEHUB — EŞYA FİLTRELEME
// ── Dil-normalize helper ──
const _normItemName = str => (str||'').toLowerCase()
.replace(/[çÇ]/g,'c').replace(/[şŞ]/g,'s').replace(/[ğĞ]/g,'g')
.replace(/[ıİ]/g,'i').replace(/[öÖ]/g,'o').replace(/[üÜ]/g,'u')
.replace(/[^a-z0-9 ]/g,' ').replace(/\s+/g,' ').trim();
// ── Item filtering helper ──
const filterItems = (searchTerm, limit = 9999) => {
const dropdown = document.querySelector(SEL_DD);
const terms = searchTerm.split(',').map(t => t.trim().toLowerCase());
const filtered = [];
const seenValues = new Set();
// --- Katman 1: Sayfa dropdown'u ---
if (dropdown) {
const options = [...dropdown.querySelectorAll('option')].filter(opt => opt.value && opt.value !== '-1');
for (const option of options) {
const text = option.textContent.toLowerCase();
for (const term of terms) {
let match = false;
if (term.startsWith('*') && term.endsWith('*')) {
match = text.includes(term.slice(1,-1));
} else if (term.startsWith('*')) {
match = text.endsWith(term.slice(1));
} else if (term.endsWith('*')) {
match = text.startsWith(term.slice(0,-1));
} else {
match = text.startsWith(term);
}
if (match) {
filtered.push({ value: option.value, text: option.textContent.trim(), source: 'page' });
seenValues.add(option.value);
break;
}
}
if (filtered.length >= limit) break;
}
}
return filtered;
};
// TRADEHUB — TOPLU TEKLİF UI
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;
seedDefaultFavorites(); // varsayılan favorileri yükle (sadece boşsa)
removeWarningBox();
cleanHideOfferedCheckbox();
const charInfo = getPageCharacterInfo();
const ui = mk('div'); ui.id = 'tvip-bulk-offer-ui';
const charInfoHtml = charInfo
? `<div id="tvip-bo-char-info" style="font-size:11px;color:#495057;padding:5px 8px;background:#f0f8ff;border:1px solid #bee3f8;border-radius:4px;margin-bottom:8px">
👤 <a href="${charInfo.characterUrl}" target="_blank" style="color:#17a2b8;text-decoration:none;font-weight:bold">${charInfo.characterName}</a>
<span style="color:#aaa;margin-left:6px">#${charInfo.characterId}</span>
</div>`
: '';
ui.innerHTML = `
<h3 style="margin:0 0 8px;font-size:13px">${({ TR:'Toplu Teklif', EN:'Bulk Offer', PT:'Oferta em Massa' }[LANG])}</h3>
<div class="tvip-bulk-panel">
${charInfoHtml}
<div class="tvip-bulk-grid">
<div class="tvip-bulk-field" style="grid-column:1/-1"><label>${({ TR:'Eşya adı (virgülle ayırarak birden fazla girilebilir):', EN:'Item name (comma-separated for multiple):', PT:'Nome do item (vários separados por vírgula):' }[LANG])}</label><input type="text" id="tvip-bo-name" placeholder="${({ TR:'Örn: Ağrı kesici veya Zehir, Afrodizyak', EN:'e.g. Painkiller or Poison, Aphrodisiac', PT:'Ex: Analgésico ou Veneno, Afrodisíaco' }[LANG])}" style="font-size:14px;padding:8px"></div>
<div class="tvip-bulk-field"><label>${({ TR:'Adet:', EN:'Quantity:', PT:'Quantidade:' }[LANG])}</label><input type="number" id="tvip-bo-qty" min="1" max="100" value="1"></div>
<div class="tvip-bulk-field">
<label>${({ TR:'Fiyat (M$):', EN:'Price (M$):', PT:'Preço (M$):' }[LANG])}</label>
<div style="display:flex;gap:4px;align-items:center">
<input type="text" id="tvip-bo-price" placeholder="50k / 1.5m / 0" style="flex:1">
<button id="tvip-bo-lastprice" type="button" title="${({ TR:'Son teklif fiyatını gir', EN:'Use last offer price', PT:'Usar último preço' }[LANG])}" style="padding:4px 8px;border:1px solid #e67e22;border-radius:4px;background:#fef9e7;color:#7d4f00;cursor:pointer;font-size:11px;white-space:nowrap">💰</button>
</div>
</div>
<div class="tvip-bulk-field" style="grid-column:1/-1">
<div style="display:flex;align-items:center;gap:8px">
<label style="font-size:11px;color:#555;flex-shrink:0">${({ TR:'Aralık (sn):', EN:'Interval (s):', PT:'Intervalo (s):' }[LANG])}</label>
<input type="number" id="tvip-bo-delay" min="1" max="10" value="1" style="width:56px;padding:4px 6px;border:1px solid #ccc;border-radius:4px;font-size:12px">
<span style="font-size:10px;color:#aaa">± 1 sn random</span>
</div>
</div>
</div>
<div class="tvip-bulk-actions">
<button id="tvip-bo-preview" class="btn-b">${({ TR:'👁 Önizle', EN:'👁 Preview', PT:'👁 Prévia' }[LANG])}</button>
<button id="tvip-bo-start" class="tvip-btn-start">${({ TR:'▶ Teklif Et', EN:'▶ Offer', PT:'▶ Ofertar' }[LANG])}</button>
<button id="tvip-bo-stop" class="tvip-btn-stop">${({ TR:'■ Durdur', EN:'■ Stop', PT:'■ Parar' }[LANG])}</button>
</div>
<div class="tvip-bulk-actions" style="justify-content:center">
<button id="tvip-bo-favorites" class="btn-b btn-sm">${({ TR:'⭐ Favoriler', EN:'⭐ Favorites', PT:'⭐ Favoritos' }[LANG])}</button>
<button id="tvip-bo-customers" class="btn-g btn-sm">${({ TR:'👥 Müşteriler', EN:'👥 Customers', PT:'👥 Clientes' }[LANG])}</button>
<button id="tvip-bo-log" class="btn-grey btn-sm">📋 Log</button>
</div>
<div id="tvip-bo-status" class="tvip-status">${({ TR:'Hazır.', EN:'Ready.', PT:'Pronto.' }[LANG])}</div>
</div>`;
formBox.insertBefore(ui, formBox.firstChild);
const priceInp = document.getElementById('tvip-bo-price');
setupPriceInput(priceInp);
// ── Auto-Suggest: isim girilirken öneri listesi göster ──
const nameInp = document.getElementById('tvip-bo-name');
const suggBox = mk('div'); suggBox.id = 'tvip-bo-sugg';
suggBox.style.cssText = 'display:none;position:fixed;background:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 4px 12px rgba(0,0,0,.2);z-index:999999;max-height:200px;overflow-y:auto;font-size:12px;box-sizing:border-box;';
document.body.appendChild(suggBox);
const _positionSugg = () => {
const r = nameInp.getBoundingClientRect();
suggBox.style.left = r.left + 'px';
suggBox.style.top = (r.bottom + 2) + 'px';
suggBox.style.width = r.width + 'px';
};
const _fillSugg = (items) => {
if (!items.length) { suggBox.style.display='none'; return; }
suggBox.innerHTML = '';
items.slice(0,12).forEach(item => {
const row = mk('div'); row.style.cssText = 'padding:6px 10px;cursor:pointer;border-bottom:1px solid #f0f0f0;display:flex;justify-content:space-between;align-items:center;gap:8px;';
const nameSpan = mk('span','',item.text);
nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
const metaSpan = mk('span');
const spData = getSharedPriceH(item.text, '');
const metaParts = [];
if (spData) metaParts.push('<span style="color:#e67e22;font-weight:bold;cursor:pointer;" class="tvip-price-badge" data-name="'+encodeURIComponent(item.text)+'" data-variant="">'+spData.last.toLocaleString('tr-TR')+' M$</span>');
metaSpan.innerHTML = metaParts.join(' ');
metaSpan.style.cssText = 'font-size:11px;color:#888;white-space:nowrap;flex-shrink:0;';
row.append(nameSpan, metaSpan);
row.addEventListener('mouseenter', () => row.style.background='#f0f8ff');
row.addEventListener('mouseleave', () => row.style.background='');
row.addEventListener('mousedown', (e) => {
e.preventDefault();
nameInp.value = item.text.replace(/\s*\(.*\)\s*$/, ''); // Varyant parantezini çıkar
suggBox.style.display = 'none';
// Fiyat badge varsa price input'unu doldur
if (spData) {
priceInp.dataset.raw = String(spData.last);
priceInp.value = spData.last.toLocaleString('tr-TR');
}
nameInp.focus();
});
suggBox.appendChild(row);
});
// Fiyat badge tıklama: geçmiş popup
suggBox.querySelectorAll('.tvip-price-badge').forEach(badge => {
badge.addEventListener('mousedown', (e) => {
e.stopPropagation();
_showPriceHistoryPopup(decodeURIComponent(badge.dataset.name), decodeURIComponent(badge.dataset.variant), badge);
});
});
_positionSugg(); suggBox.style.display = 'block';
};
let _suggTimer = null;
nameInp.addEventListener('input', () => {
clearTimeout(_suggTimer);
const val = nameInp.value.trim();
if (!val) { suggBox.style.display='none'; return; }
_suggTimer = setTimeout(() => {
// Sadece ilk terimi suggest için kullan (virgülle ayrılmış çoklu girişte son terim)
const terms = val.split(',');
const lastTerm = terms[terms.length-1].trim();
if (!lastTerm) return;
const results = filterItems(lastTerm, 12);
_fillSugg(results);
}, 250);
});
nameInp.addEventListener('blur', () => setTimeout(() => { suggBox.style.display='none'; }, 200));
// ── Fiyat Badge & Geçmiş Popup ──
const _showPriceHistoryPopup = (name, variant, anchorEl) => {
document.getElementById('tvip-price-hist-popup')?.remove();
const sp = getSharedPriceH(name, variant);
const popup = mk('div'); popup.id = 'tvip-price-hist-popup';
popup.style.cssText = 'position:fixed;z-index:999999;background:#fff;border:1px solid #f0ad4e;border-radius:6px;padding:10px;min-width:180px;box-shadow:0 4px 16px rgba(0,0,0,.25);font-size:11px;';
const rect = anchorEl.getBoundingClientRect();
popup.style.left = Math.min(rect.left, window.innerWidth-200)+'px';
popup.style.top = (rect.bottom+4)+'px';
const title = mk('div','',({ TR:'📋 Fiyat Geçmişi', EN:'📋 Price History', PT:'📋 Histórico de Preços' }[LANG]));
title.style.cssText = 'font-weight:bold;color:#e67e22;margin-bottom:6px;';
popup.appendChild(title);
if (!sp || !sp.prices.length) {
popup.appendChild(mk('p','',({ TR:'Fiyat geçmişi yok.', EN:'No price history.', PT:'Sem histórico.' }[LANG]))).style.cssText='color:#aaa;font-style:italic;font-size:11px;';
} else {
[...sp.prices].reverse().forEach(e => {
const row = mk('div'); row.style.cssText = 'display:flex;justify-content:space-between;gap:10px;padding:3px 0;border-bottom:1px solid #f0f0f0;cursor:pointer;';
const pSpan = mk('span','',e.p.toLocaleString('tr-TR')+' M$'); pSpan.style.cssText='color:#e67e22;font-weight:bold;';
const dSpan = mk('span','',e.d); dSpan.style.cssText='color:#aaa;';
row.append(pSpan, dSpan);
row.title = ({ TR:'Tıkla: Bu fiyatı gir', EN:'Click: Use this price', PT:'Clique: Usar este preço' }[LANG]);
row.addEventListener('click', () => {
priceInp.dataset.raw = String(e.p);
priceInp.value = e.p.toLocaleString('tr-TR');
popup.remove();
// Fiyat input'una odaklan
priceInp.dispatchEvent(new Event('blur'));
});
popup.appendChild(row);
});
}
const closeB = mkB('✕', 'btn-grey btn-sm', () => popup.remove());
closeB.style.cssText = 'margin-top:6px;width:100%;';
popup.appendChild(closeB);
document.body.appendChild(popup);
setTimeout(() => {
const handler = (e) => { if (!popup.contains(e.target)) { popup.remove(); document.removeEventListener('click', handler); } };
document.addEventListener('click', handler);
}, 50);
};
// ── Fiyat Input badge: son fiyatı yanında göster ──
const _updatePriceBadge = () => {
document.getElementById('tvip-bo-price-hint')?.remove();
const rawName = nameInp.value.split(',')[0].trim();
if (!rawName) return;
const sp = getSharedPriceH(rawName, '');
if (!sp) return;
const hint = mk('span'); hint.id = 'tvip-bo-price-hint';
hint.style.cssText = 'font-size:10px;color:#e67e22;cursor:pointer;margin-left:4px;white-space:nowrap;';
hint.textContent = ({ TR:'Son:', EN:'Last:', PT:'Último:' }[LANG])+' '+sp.last.toLocaleString('tr-TR')+' M$';
hint.title = ({ TR:'Geçmişi gör', EN:'View history', PT:'Ver histórico' }[LANG]);
hint.addEventListener('click', () => _showPriceHistoryPopup(rawName, '', hint));
// Fiyat alanının hemen altına ekle
const priceField = priceInp.closest('.tvip-bulk-field') || priceInp.parentElement.parentElement || priceInp.parentElement;
priceField.appendChild(hint);
};
nameInp.addEventListener('change', _updatePriceBadge);
nameInp.addEventListener('blur', _updatePriceBadge);
// ── Sepet Sistemi ──
const CART_KEY = 'tvip_bo_cart';
const getCart = () => { try { return JSON.parse(GM_getValue(CART_KEY,'[]')); } catch { return []; } };
const saveCart = c => GM_setValue(CART_KEY, JSON.stringify(c));
const clearCart = () => GM_setValue(CART_KEY, '[]');
const renderCart = () => {
const cart = getCart();
const cartEl = document.getElementById('tvip-bo-cart-list');
if (!cartEl) return;
cartEl.innerHTML = '';
if (!cart.length) {
cartEl.appendChild(mk('p','',({ TR:'Sepet boş.', EN:'Cart empty.', PT:'Carrinho vazio.' }[LANG]))).style.cssText='color:#aaa;font-size:11px;text-align:center;margin:4px 0;';
document.getElementById('tvip-bo-cart-offer').disabled = true;
return;
}
cart.forEach((item, idx) => {
const row = mk('div'); row.style.cssText='display:flex;justify-content:space-between;align-items:center;padding:3px 0;border-bottom:1px solid #e9ecef;font-size:11px;gap:6px;';
const info = mk('span','',`${item.name} ×${item.qty} — ${item.price > 0 ? item.price.toLocaleString('tr-TR')+' M$' : ({ TR:'Ücretsiz', EN:'Free', PT:'Grátis' }[LANG])}`);
info.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#333;';
const delBtn = mkB('✕','btn-sm btn-r', () => {
const c = getCart(); c.splice(idx,1); saveCart(c); renderCart();
}); delBtn.style.cssText='padding:1px 5px!important;font-size:10px!important;';
row.append(info, delBtn);
cartEl.appendChild(row);
});
document.getElementById('tvip-bo-cart-offer').disabled = false;
};
// Sepet UI — status div'inin altına ekle
const cartSection = mk('div'); cartSection.id = 'tvip-bo-cart-section';
cartSection.style.cssText = 'margin-top:8px;padding:8px;background:#f8f9fa;border:1px solid #dee2e6;border-radius:5px;';
const cartTitle = mk('div','',({ TR:'🛒 Sepet', EN:'🛒 Cart', PT:'🛒 Carrinho' }[LANG]));
cartTitle.style.cssText = 'font-size:11px;font-weight:bold;color:#495057;margin-bottom:5px;display:flex;justify-content:space-between;align-items:center;';
const clearCartBtn = mkB(({ TR:'Temizle', EN:'Clear', PT:'Limpar' }[LANG]),'btn-sm btn-r',() => { if(confirm(({ TR:'Sepet temizlensin mi?', EN:'Clear cart?', PT:'Limpar carrinho?' }[LANG]))){ clearCart(); renderCart(); } });
clearCartBtn.style.cssText = 'padding:1px 6px!important;font-size:10px!important;';
cartTitle.appendChild(clearCartBtn);
const cartList = mk('div'); cartList.id = 'tvip-bo-cart-list';
cartList.style.cssText = 'max-height:150px;overflow-y:auto;';
const cartActions = mk('div'); cartActions.style.cssText = 'display:flex;gap:6px;margin-top:6px;';
const addToCartBtn = mkB(({ TR:'+ Sepete Ekle', EN:'+ Add to Cart', PT:'+ Adicionar' }[LANG]),'btn-b btn-sm',() => {
const name = nameInp.value.trim();
const qty = parseInt(document.getElementById('tvip-bo-qty')?.value||'1',10)||1;
const price = getPriceVal(priceInp);
if (!name) { document.getElementById('tvip-bo-status').textContent = s('boErrName'); return; }
const cart = getCart();
cart.push({ name, qty, price });
saveCart(cart);
renderCart();
document.getElementById('tvip-bo-status').textContent = ({ TR:`"${name}" sepete eklendi.`, EN:`"${name}" added to cart.`, PT:`"${name}" adicionado.` }[LANG]);
});
const offerCartBtn = mkB(({ TR:'▶ Tümünü Teklif Et', EN:'▶ Offer All', PT:'▶ Ofertar Todos' }[LANG]),'btn-g btn-sm',() => {
const cart = getCart(); if(!cart.length) return;
// İlk elemanı forma yükle, kalanları cart'ta bırak (processNext her seferinde cart'ı azaltır)
const first = cart[0]; cart.splice(0,1); saveCart(cart);
nameInp.value = first.name;
document.getElementById('tvip-bo-qty').value = String(first.qty);
priceInp.dataset.raw = String(first.price);
priceInp.value = first.price > 0 ? first.price.toLocaleString('tr-TR') : '0';
document.getElementById('tvip-bo-start').click();
});
offerCartBtn.id = 'tvip-bo-cart-offer';
cartActions.append(addToCartBtn, offerCartBtn);
cartSection.append(cartTitle, cartList, cartActions);
// Status div'den sonra ekle
document.getElementById('tvip-bo-status')?.after(cartSection);
renderCart();
// 💰 Son fiyatı gir
document.getElementById('tvip-bo-lastprice').onclick = () => {
const rawName = nameInp.value.split(',')[0].trim();
if (!rawName) return;
const sp = getSharedPriceH(rawName, '');
if (sp && sp.last) {
priceInp.dataset.raw = String(sp.last);
priceInp.value = sp.last.toLocaleString('tr-TR');
priceInp.dispatchEvent(new Event('blur'));
} else {
// Geçmiş yoksa fiyat geçmişi popup'ı aç
_showPriceHistoryPopup(rawName, '', document.getElementById('tvip-bo-lastprice'));
}
};
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;
document.getElementById('tvip-bo-delay').disabled = on;
document.getElementById('tvip-bo-preview').disabled = on;
document.getElementById('tvip-bo-favorites').disabled= on;
const lpBtn = document.getElementById('tvip-bo-lastprice'); if(lpBtn) lpBtn.disabled = on;
const cartOfferBtn = document.getElementById('tvip-bo-cart-offer');
if (cartOfferBtn) cartOfferBtn.disabled = on || !getCart().length;
};
const stopOffer = () => {
GM_deleteValue(DK.BO_ITEMS); GM_deleteValue(DK.BO_RUNNING);
GM_deleteValue(DK.BO_PRICE); GM_deleteValue(DK.BO_TOTAL);
document.getElementById('tvip-global-stop')?.remove();
setDisabled(false);
// Sepette daha ürün varsa otomatik devam et
const nextCart = getCart();
if (nextCart.length) {
const next = nextCart[0]; nextCart.splice(0,1); saveCart(nextCart);
setTimeout(() => {
nameInp.value = next.name;
document.getElementById('tvip-bo-qty').value = String(next.qty);
priceInp.dataset.raw = String(next.price);
priceInp.value = next.price > 0 ? next.price.toLocaleString('tr-TR') : '0';
renderCart();
document.getElementById('tvip-bo-start').click();
}, 1500);
} else {
renderCart();
}
};
const processNext = () => {
showGlobalStop(() => document.getElementById('tvip-bo-stop')?.click());
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 = ({ TR:'Tüm teklifler tamamlandı!', EN:'All offers completed!', PT:'Todas as ofertas concluídas!' }[LANG]);
document.getElementById('tvip-global-stop')?.remove();
stopOffer(); return;
}
const dropdown = document.querySelector(SEL_DD);
const offerBtn = document.querySelector(SEL_BTN);
const priceEl = document.querySelector(SEL_PRICE);
if (!dropdown || !offerBtn) {
status.textContent = ({ TR:'Kritik hata: Sayfa öğeleri kayboldu.', EN:'Critical error: Page elements gone.', PT:'Erro crítico: Elementos desapareceram.' }[LANG]);
stopOffer(); return;
}
const item = items.shift();
const price = parseInt(GM_getValue(DK.BO_PRICE, '0')) || 0;
const total = parseInt(GM_getValue(DK.BO_TOTAL, '0'));
const count = total - items.length;
if (priceEl) priceEl.value = String(price);
GM_setValue(DK.BO_ITEMS, JSON.stringify(items));
status.textContent = `${({ TR:'Teklif', EN:'Offering', PT:'Ofertando' }[LANG])} ${count}/${total}: '${item.text}'...`;
dropdown.value = item.value;
dropdown.dispatchEvent(new Event('change', { bubbles: true }));
if (dropdown.value !== item.value) {
status.textContent = `Atlandı: '${item.text}'`;
setTimeout(processNext, 1000); return;
}
const l = getOfferedList(); if (!l.includes(item.value)) { l.push(item.value); saveOfferedList(l); }
// Log kaydını click'ten ÖNCE al (sayfa yenilenmeden)
const ci = getPageCharacterInfo();
const entry = {
timestamp: new Date().toISOString(),
itemName: item.text, price, qty: 1,
source: 'bulk',
batchNumber: count, totalInBatch: total,
batchId: GM_getValue(DK.BO_BATCH_ID, ''),
characterName: ci?.characterName || '',
characterId: ci?.characterId || '',
characterUrl: ci?.characterUrl || '',
};
// Ana log
const log = getLog();
log.push(entry);
if (log.length > 500) log.splice(0, log.length - 500);
saveLog(log);
// Ortak fiyat geçmişine yaz
if (price >= 10000) addSharedPriceH(item.text, '', price);
// Müşteri logu — müşteri kaydı varsa ekle
const customers = getCustomers();
const existingCustomer = customers.find(c =>
(ci?.characterId && c.characterId === ci.characterId) ||
(ci?.characterName && c.name.toLowerCase() === ci.characterName.toLowerCase())
);
if (existingCustomer) {
const custLogs = getCustomerLogs();
custLogs.push({
customerId: existingCustomer.id,
customerName: existingCustomer.name,
timestamp: entry.timestamp,
itemName: entry.itemName,
qty: 1, price,
source: 'bulk',
batchNumber: count, totalInBatch: total,
batchId: entry.batchId
});
if (custLogs.length > 1000) custLogs.splice(0, custLogs.length - 1000);
saveCustomerLogs(custLogs);
}
// Süre: delay input değeri ± 1 sn random
const delaySec = Math.max(1, Math.min(10, parseInt(document.getElementById('tvip-bo-delay')?.value || '1')));
setTimeout(() => {
if (!GM_getValue(DK.BO_RUNNING, false)) { setDisabled(false); return; }
// Teslimat checkbox'ı zorla işaretle — Otomatik Teslimat açık olsun olmasın, her toplu teklifte uygula
document.querySelectorAll("input[type=checkbox][id$='chkDelivery']").forEach(cb => {
cb.removeAttribute('disabled');
cb.checked = true;
});
offerBtn.click();
}, Math.max(0, delaySec * 1000 + (Math.random() * 2000 - 1000)));
};
// ── Button handlers ──
document.getElementById('tvip-bo-preview').onclick = (e) => {
e.preventDefault(); e.stopPropagation();
const nameVal = document.getElementById('tvip-bo-name').value.trim();
const qtyVal = Math.max(1, Math.min(100, parseInt(document.getElementById('tvip-bo-qty').value, 10) || 1));
const price = getPriceVal(document.getElementById('tvip-bo-price'));
const status = document.getElementById('tvip-bo-status');
if (!nameVal) { status.textContent = ({ TR:'Hata: Eşya adı girin.', EN:'Error: Enter item name.', PT:'Erro: Digite o nome.' }[LANG]); return; }
const found = filterItems(nameVal, 9999); // tümünü bul
const toOffer = found.slice(0, qtyVal);
// Önizleme modalı
const modal = mk('div'); modal.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);z-index:99999;display:flex;align-items:center;justify-content:center;padding:10px;box-sizing:border-box';
const content = mk('div'); content.style.cssText = 'background:#fff;border-radius:8px;padding:16px;width:95vw;max-width:460px;max-height:85vh;overflow-y:auto;box-sizing:border-box';
const title = mk('h3', '', ({ TR:'👁 Önizleme', EN:'👁 Preview', PT:'👁 Prévia' }[LANG])); title.style.cssText = 'margin:0 0 10px;font-size:14px;color:#333';
const summary = mk('div'); summary.style.cssText = 'background:#f8f9fa;border-radius:5px;padding:10px;margin-bottom:10px;font-size:12px';
if (!found.length) {
summary.innerHTML = `<span style="color:#dc3545">⚠️ ${({ TR:`"${nameVal}" ile başlayan eşya bulunamadı.`, EN:`No items starting with "${nameVal}".`, PT:`Nenhum item com "${nameVal}".` }[LANG])}</span>`;
} else {
const total = toOffer.length * price;
summary.innerHTML = `
<div><b>${({ TR:'Envanterde bulunan:', EN:'Found in inventory:', PT:'Encontrado:' }[LANG])}</b> ${found.length}</div>
<div><b>${({ TR:'Gönderilecek:', EN:'Will offer:', PT:'Será ofertado:' }[LANG])}</b> ${toOffer.length}</div>
<div><b>${({ TR:'Birim fiyat:', EN:'Unit price:', PT:'Preço unitário:' }[LANG])}</b> ${price.toLocaleString('tr-TR')} M$</div>
<div style="border-top:1px solid #dee2e6;margin-top:6px;padding-top:6px;font-weight:bold;color:#218838">
${({ TR:'Toplam:', EN:'Total:', PT:'Total:' }[LANG])} ${total.toLocaleString('tr-TR')} M$
</div>`;
}
const listEl = mk('div'); listEl.style.cssText = 'max-height:200px;overflow-y:auto;font-size:11px';
toOffer.forEach((item, i) => {
const row = mk('div', '', `${i+1}. ${item.text}`);
row.style.cssText = 'padding:2px 4px;border-bottom:1px solid #f0f0f0;color:#555';
listEl.appendChild(row);
});
const closeBtn = mkB('✕ ' + ({ TR:'Kapat', EN:'Close', PT:'Fechar' }[LANG]), 'btn-grey', () => modal.remove());
closeBtn.style.cssText = 'width:100%;margin-top:12px';
content.append(title, summary, listEl, closeBtn);
modal.appendChild(content);
document.body.appendChild(modal);
modal.addEventListener('click', ev => { if (ev.target === modal) modal.remove(); });
};
document.getElementById('tvip-bo-start').onclick = () => {
const nameVal = document.getElementById('tvip-bo-name').value.trim();
const qtyVal = Math.max(1, Math.min(100, parseInt(document.getElementById('tvip-bo-qty').value, 10) || 1));
const priceVal = getPriceVal(document.getElementById('tvip-bo-price'));
const status = document.getElementById('tvip-bo-status');
if (!nameVal) {
const allItems = filterItems('', 9999).filter(it => it.value);
if (!allItems.length) { status.textContent = s('boErrName'); return; }
const D = (tr,en,pt) => ({TR:tr,EN:en,PT:pt}[LANG]);
if (!confirm(D(
`${allItems.length} eşya 0 M$ (ücretsiz) olarak gönderilecek. Devam edilsin mi?`,
`${allItems.length} items will be offered for free (0 M$). Proceed?`,
`${allItems.length} itens serão ofertados por 0 M$. Continuar?`
))) return;
GM_setValue(DK.BO_BATCH_ID, Date.now().toString());
GM_setValue(DK.BO_PRICE, 0);
GM_setValue(DK.BO_ITEMS, JSON.stringify(allItems));
GM_setValue(DK.BO_TOTAL, String(allItems.length));
GM_setValue(DK.BO_RUNNING, 'true');
setDisabled(true);
setTimeout(processNext, 300);
return;
}
if (priceVal < 0) { status.textContent = ({ TR:'Hata: Geçersiz fiyat.', EN:'Error: Invalid price.', PT:'Erro: Preço inválido.' }[LANG]); return; }
const toOffer = filterItems(nameVal, qtyVal);
if (!toOffer.length) { status.textContent = `"${nameVal}" — ${({ TR:'bulunamadı.', EN:'not found.', PT:'não encontrado.' }[LANG])}`; return; }
status.textContent = `${toOffer.length} ${({ TR:'eşya bulundu, başlıyor...', EN:'items found, starting...', PT:'itens encontrados...' }[LANG])}`;
GM_setValue(DK.BO_BATCH_ID, Date.now().toString());
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 = () => {
document.getElementById('tvip-bo-status').textContent = ({ TR:'Kullanıcı tarafından durduruldu.', EN:'Stopped by user.', PT:'Parado pelo usuário.' }[LANG]);
document.getElementById('tvip-global-stop')?.remove();
stopOffer();
};
document.getElementById('tvip-bo-favorites').onclick = (e) => {
e.preventDefault(); e.stopPropagation();
openFavoritesModal((itemName, qty, price) => {
document.getElementById('tvip-bo-name').value = itemName;
document.getElementById('tvip-bo-qty').value = qty;
const pi = document.getElementById('tvip-bo-price');
pi.dataset.raw = String(price);
pi.value = price > 0 ? price.toLocaleString('tr-TR') : '0';
});
};
document.getElementById('tvip-bo-customers').onclick = (e) => {
e.preventDefault(); e.stopPropagation();
openCustomersModal();
};
document.getElementById('tvip-bo-log').onclick = (e) => {
e.preventDefault(); e.stopPropagation();
openLogModal();
};
// Resume on page reload
if (GM_getValue(DK.BO_RUNNING, false)) {
setDisabled(true);
document.getElementById('tvip-bo-status').textContent = ({ TR:'Yeniden yüklendi, devam ediliyor...', EN:'Reloaded, resuming...', PT:'Recarregado, retomando...' }[LANG]);
setTimeout(processNext, 500);
} else { setDisabled(false); }
};
// TRADEHUB — TOPLU KABUL + TOPLU İPTAL
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 = () => {
showGlobalStop(() => document.getElementById('tvip-ba-stop')?.click());
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;
// Repeater indeksini item linkinden al, box içinde butonu bul
const itemLink = p.querySelector('a[id*="lnkItem"]');
const idxM = itemLink?.id.match(/_(ctl\d+)_lnkItem/i);
const repIdx = idxM ? idxM[1] : null;
const btn = (repIdx && box.querySelector(`input[id*="${repIdx}_btnAccept"]`))
|| p.querySelector('input[id*="btnAccept"]')
|| box.querySelector('input[id*="btnAccept"]');
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);
document.getElementById('tvip-global-stop')?.remove();
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);
document.getElementById('tvip-global-stop')?.remove();
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 = () => {
showGlobalStop(() => document.getElementById('tvip-bc-stop')?.click());
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;
// Repeater indeksini item linkinden al, box içinde butonu bul
const itemLinkC = p.querySelector('a[id*="lnkItem"]');
const idxMC = itemLinkC?.id.match(/_(ctl\d+)_lnkItem/i);
const repIdxC = idxMC ? idxMC[1] : null;
const btn = (repIdxC && box.querySelector(`input[id*="${repIdxC}_btnStop"]`))
|| p.querySelector('input[type="submit"][id*="btnStop"]')
|| box.querySelector('input[type="submit"][id*="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);
document.getElementById('tvip-global-stop')?.remove();
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);
document.getElementById('tvip-global-stop')?.remove();
stopCancel();
};
if (GM_getValue(DK.BC_RUNNING, false)) {
setDisabledC(true);
setTimeout(processNextCancel, 500);
} else { setDisabledC(false); }
}
};
// CITY SHORTCUTS
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"
};
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} ]},
};
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 setupDeleteConfirm = () => {
if (!guard(K.deleteConfirm, '/Character/Items')) return;
const btn = document.querySelector('#ctl00_cphLeftColumn_ctl00_btnThrowAwaySelItems');
const chk = document.querySelector('#ctl00_cphLeftColumn_ctl00_chkThrowAwaySelItems');
if (!btn || !chk) return;
btn.addEventListener('click', e => {
if (btn.disabled) return;
e.preventDefault(); e.stopImmediatePropagation();
const names = [...document.querySelectorAll('input[type=checkbox][id*="_chkItem"]:checked')]
.map(cb => cb.closest('tr')?.querySelector('a[id*="_lnkItem"]')?.textContent.trim())
.filter(Boolean);
if (!names.length) return;
if (confirm(sf.deleteConfirmItems(names))) {
chk.checked = true;
if (typeof unsafeWindow !== 'undefined' && typeof unsafeWindow.__doPostBack === 'function') {
unsafeWindow.__doPostBack('ctl00$cphLeftColumn$ctl00$btnThrowAwaySelItems', '');
} else if (typeof __doPostBack === 'function') {
__doPostBack('ctl00$cphLeftColumn$ctl00$btnThrowAwaySelItems', '');
}
}
}, true);
};
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);
});
};
const openCityManager = () => {
const cityDd = document.getElementById('ctl00_cphRightColumn_ctl01_ddlCities');
const activeCityId = cityDd?.value || Object.keys(CITY_DATA)[0];
mkModal('🏙️ ' + s('cityMgr'), (cont) => {
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);
const listDiv = mk('div'); cont.appendChild(listDiv);
const renderList = () => {
listDiv.innerHTML = '';
const data = getCityData()[citySelect.value];
const locs = data ? data.locations : [];
if (!locs.length) { listDiv.appendChild(mk('p','',s('cityMgrEmpty'))).style.cssText='font-size:12px;color:#888'; return; }
locs.forEach((loc,i) => {
const row = mk('div'); row.style.cssText='display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;padding:4px 6px;background:#f8f9fa;border-radius:4px';
const icon = mk('span','',typeIcon(loc.type)); icon.style.fontSize='14px';
const lbl = mk('a','',loc.name); lbl.href=`${PM}/Locale/${loc.placeId}`; lbl.target='_blank'; lbl.style.cssText='flex:1;color:#17a2b8;text-decoration:none';
const delB = mkB('✕','btn-sm btn-r',() => {
if (!confirm(s('cityMgrDelQ'))) return;
const custom = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
const cityId = citySelect.value;
if (!custom[cityId]) custom[cityId] = JSON.parse(JSON.stringify(getCityData()[cityId] || {locations:[]}));
custom[cityId].locations = custom[cityId].locations.filter((_,j)=>j!==i);
GM_setValue('tvip_city_custom', JSON.stringify(custom));
renderList();
});
row.append(icon, lbl);
if (loc.dur) { const dur=mk('span','',`${loc.dur} ${s('csMin')}`); dur.style.cssText='color:#888;font-size:11px'; row.appendChild(dur); }
row.appendChild(delB);
listDiv.appendChild(row);
});
const custom = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
if (custom[citySelect.value]) {
const tag = mk('span','',`✏️ ${s('cityMgrCustom')}`); tag.style.cssText='font-size:10px;color:#888;display:block;margin-top:2px';
listDiv.appendChild(tag);
}
};
citySelect.addEventListener('change', renderList);
renderList();
const addSection = mk('div'); addSection.style.cssText='margin-top:10px;padding:8px;background:#f0f8ff;border-radius:6px';
const addTitle = mk('div','',s('cityMgrAdd')); addTitle.style.cssText='font-size:11px;font-weight:bold;color:#555;margin-bottom:6px';
const addGrid = mk('div'); addGrid.style.cssText='display:grid;grid-template-columns:1fr 80px 60px auto;gap:4px;align-items:center';
const namInp = mk('input'); namInp.placeholder=s('cityMgrName'); namInp.style.cssText='padding:4px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px';
const idInp = mk('input'); idInp.type='number'; idInp.placeholder='Place ID'; idInp.style.cssText='padding:4px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px';
const durInp = mk('input'); durInp.type='number'; durInp.placeholder='Min'; durInp.style.cssText='padding:4px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px';
const addBtn = mkB(s('cityMgrAddBtn'),'btn-sm btn-g',() => {
const nm=namInp.value.trim(), pid=parseInt(idInp.value), dur=parseInt(durInp.value)||0;
if (!nm||isNaN(pid)) return;
const custom = (() => { try{return JSON.parse(GM_getValue('tvip_city_custom','{}'));}catch{return {};} })();
const cityId = citySelect.value;
if (!custom[cityId]) custom[cityId] = JSON.parse(JSON.stringify(getCityData()[cityId] || {locations:[]}));
custom[cityId].locations.push({id:`c${Date.now()}`,type:'path',placeId:pid,name:nm,dur});
GM_setValue('tvip_city_custom', JSON.stringify(custom));
namInp.value=''; idInp.value=''; durInp.value='';
renderList();
});
addGrid.append(namInp,idInp,durInp,addBtn);
addSection.append(addTitle,addGrid);
cont.appendChild(addSection);
});
};
// MENU
const injectMenu = () => {
if (document.getElementById('tvip-bar')) return;
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));
// ── GENEL GÖRÜNÜM ──
mkSec(s('secGeneral'));
mkCheck(K.pb, 'pb');
mkCheck(K.alignLeft, 'alignLeft');
mkCheck(K.imageCtrl, 'imageCtrl');
mkCheck(K.colorScoring, 'colorScoring');
mkCheck(K.cityShortcuts,'cityShortcuts');
mkCheck(K.tableTools, 'tableTools');
mkCheck(K.repertoireF, 'repertoireF');
mkHr();
// ── EŞYA ──
mkSec(s('secItems'));
mkCheck(K.itemId, 'itemId');
mkCheck(K.itemFilters, 'itemFilters');
mkCheck(K.hideOffBox, 'hideOfferedChk');
mkCheck(K.deleteConfirm,'deleteConfirmChk');
mkCheck(K.autoDelivery, 'autoDelivery');
mkCheck(K.ticketPricer, 'ticketPricer');
mkCheck(K.bulkOffer, 'bulkOffer');
mkCheck(K.bulkAccept, 'bulkAccept');
mkHr();
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);
const ecosystemRow = mk('div');
ecosystemRow.style.cssText = 'background:#f0f8ff;border-radius:8px;padding:16px;margin-bottom:12px';
ecosystemRow.innerHTML = `
<div style="display:flex;gap:20px;justify-content:center;align-items:center">
<div style="display:flex;align-items:center;gap:10px">
<span style="font-size:16px">🇹🇷</span>
<a href="https://rentry.org/PopControlEkosistemi" target="_blank" style="color:#1a5276;font-weight:500;text-decoration:none;font-size:13px">Beni Oku</a>
</div>
<div style="display:flex;align-items:center;gap:10px">
<span style="font-size:16px">🇬🇧</span>
<a href="https://rentry.org/PopControlEcosystem" target="_blank" style="color:#1a5276;font-weight:500;text-decoration:none;font-size:13px">Read Me</a>
</div>
<div style="display:flex;align-items:center;gap:10px">
<span style="font-size:16px">🇧🇷</span>
<a href="https://rentry.org/EcossistemaPopControl" target="_blank" style="color:#1a5276;font-weight:500;text-decoration:none;font-size:13px">Leia-me</a>
</div>
</div>
`;
hpanel.appendChild(ecosystemRow);
mkHr();
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.cssText = 'text-decoration:none;display:inline-flex;align-items:center;justify-content:center';
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';
const cityMgrBtn = mkB('🏙️ ' + s('cityMgr'), 'btn-sm btn-grey', () => openCityManager());
row2.append(tdClearBtn, resetOfferBtn, cityMgrBtn);
hpanel.append(row1, row2);
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(';');
hpanel.removeAttribute('style');
hpanel.style.cssText = [
'display:block!important;position:relative!important',
'top:auto!important;right:auto!important;left:auto!important',
'max-height:90vh;overflow-y:auto;border-radius:10px',
'min-width:280px;max-width:560px;width:96%;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();
// ── TOP VIP "Helper Ayarlar" butonu için erişim noktasını güncelle ──
window.__tvip_api.openSettings = togglePanelModal;
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);
document.addEventListener('click', e => {
if (_isModalOpen()) return;
if (!bar.contains(e.target) && !hpanel.contains(e.target)) hpanel.style.display = 'none';
});
// POPCONTROL BAĞLANTISI
function connectToPopControl() {
if (window.PPC_Helper_Done) return;
const pc = (typeof unsafeWindow !== 'undefined' && unsafeWindow.PopControl);
if (pc && typeof pc.register === 'function') {
try {
pc.register({
id: 'helper',
icon: '🎨',
label: 'Helper',
strings: window.__ppcStrHelper || {},
buttons: [{
icon: '🎨',
label: 'Helper',
onClick: function(e) { e.preventDefault(); togglePanelModal(); }
}],
onUndo: function() {
closeModal();
bar.style.display = '';
window.PPC_Helper_Done = false;
},
});
bar.style.display = 'none';
window.PPC_Helper_Done = true;
injectCharMenuItems();
console.log('[Helper] Successfully registered with PopControl');
} catch (error) {
console.error('[Helper] PopControl connection error:', error);
}
}
}
document.addEventListener('PopControlReady', () => setTimeout(connectToPopControl, 50), { once: true });
function checkPopControl(tries = 0) {
if (window.PPC_Helper_Done) return;
const pc = (typeof unsafeWindow !== 'undefined' && unsafeWindow.PopControl);
if (pc && typeof pc.register === 'function') {
connectToPopControl();
} else if (tries < 20) {
setTimeout(() => checkPopControl(tries + 1), 150);
}
}
checkPopControl(0);
if (isMobile) {
const mobileTrigger = () => {
if (!window.PPC_Helper_Done && !window.PPC_Helper_Mobile_Triggered) {
window.PPC_Helper_Mobile_Triggered = true;
setTimeout(() => checkPopControl(0), 2000);
}
};
document.addEventListener('touchstart', mobileTrigger, { once: true, passive: true });
setTimeout(() => {
if (!window.PPC_Helper_Done && !window.PPC_Helper_Mobile_Triggered) mobileTrigger();
}, 3000);
}
};
// INIT
applyAlignLeft();
applyProgressBar();
applyItemId();
applyColorScoring();
applyImageCtrl();
applyTableSort();
applyPageSearch();
applyAutoDelivery();
applyTicketPricer();
applyTableAvg();
applyRepertoireFilter();
setupBulkOfferUI();
setupBulkAcceptUI();
setupDeleteConfirm();
// ── PUBLIC API (diğer scriptler ve menü injection için) ──
window.__tvip_api = {
openCustomers: () => typeof openCustomersModal === 'function' && openCustomersModal(),
openFavorites: () => typeof openFavoritesModal === 'function' && openFavoritesModal(() => {}),
openLog: () => typeof openLogModal === 'function' && openLogModal(),
openSettings: () => typeof togglePanelModal === 'function' && togglePanelModal(),
};
// ── TOP VIP MENÜSÜ ENJEKSİYONU ──────────────────────────────────────────────
const injectCharMenuItems = () => {
if (!location.href.includes('/World/Popmundo.aspx/Character')) return;
const D = (tr, en, pt) => ({ TR: tr, EN: en, PT: pt }[LANG] || tr);
const helperButtons = [
{ id: 'mnu-tvip-customers', label: D('👥 Müşteriler', '👥 Customers', '👥 Clientes'), fn: () => window.__tvip_api?.openCustomers() },
{ id: 'mnu-tvip-log', label: D('📋 Toplu Teklif Logu', '📋 Offer Log', '📋 Log de Oferta'), fn: () => window.__tvip_api?.openLog() },
{ id: 'mnu-tvip-settings', label: D('🎨 Helper Ayarlar', '🎨 Helper Settings','🎨 Config Helper'), fn: () => window.__tvip_api?.openSettings() },
];
// PopControl varsa tamamen devret
const _pc = (typeof unsafeWindow !== 'undefined' && unsafeWindow.PopControl) || window.PopControl;
if (_pc?.MenuManager) {
_pc.MenuManager.registerMenu({ id: 'top-vip', title: '⭐ TOP VIP ⭐', position: 'above-career', items: helperButtons, collapsible: true });
return;
}
// ── Standalone fallback (PopControl yoksa) ──────────────────────────
if (document.getElementById('mnu-tvip-customers')) return;
let ul = document.querySelector('#top-vip-menu ul');
if (!ul) {
const ref = [...document.querySelectorAll('.menu h3')]
.find(h => /Kariyer|Career|Carreira/.test(h.textContent))?.closest('.menu');
if (!ref) return;
const isCol = localStorage.getItem('top-vip-collapsed') === 'true';
const h3 = Object.assign(document.createElement('h3'), { textContent: '⭐ TOP VIP ⭐' });
h3.style.cssText = 'background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;text-align:center;padding:8px;margin:0;border-radius:6px 6px 0 0;cursor:pointer;box-shadow:0 2px 8px rgba(102,126,234,.3);user-select:none;';
ul = document.createElement('ul');
ul.style.cssText = `margin:0;padding:8px 0;background:#f8f9fa;border:1px solid #e9ecef;border-top:none;border-radius:0 0 6px 6px;${isCol ? 'display:none;' : ''}`;
h3.onclick = () => { const c = ul.style.display === 'none'; ul.style.display = c ? '' : 'none'; localStorage.setItem('top-vip-collapsed', !c); };
const menu = Object.assign(document.createElement('div'), { id: 'top-vip-menu', className: 'menu' });
menu.append(h3, ul); ref.before(menu);
menu.after(Object.assign(document.createElement('div'), { style: 'height:12px' }));
}
helperButtons.forEach(btn => {
if (document.getElementById(btn.id)) return;
const a = Object.assign(document.createElement('a'), { href: '#', textContent: btn.label });
a.style.cssText = 'color:#667eea;font-weight:600;text-decoration:none;display:block;padding:4px 12px;border-radius:4px;transition:all .2s;';
a.onmouseover = () => { a.style.background = '#667eea'; a.style.color = '#fff'; };
a.onmouseout = () => { a.style.background = ''; a.style.color = '#667eea'; };
a.onclick = e => { e.preventDefault(); btn.fn(); };
const li = Object.assign(document.createElement('li'), { id: btn.id }); li.style.margin = '2px 0';
li.appendChild(a); ul.appendChild(li);
});
};
injectCharMenuItems();
injectMenu();
if (!window.PPC_Helper_Done) {
setTimeout(() => {
if (!window.PPC_Helper_Done) {
console.log('[Helper] Final connection attempt...');
connectToPopControl();
}
}, 500);
}
if (location.href.includes('/Locale/ItemsEquipment/')) applyOnlyYours();
if (location.href.includes('/Character/OfferItem/')) applyHideOffered();
if (document.getElementById('ctl00_cphLeftColumn_ctl00_lnkAirport')) applyCityShortcuts();
})();