Greasy Fork is available in English.
Popmundo'nun en kapsamlı sosyal scripti — Efsane Speed Calling ve yalnızca burada bulabileceğin Radar Takibi, Raf, Karakter Kartı, İlgilenme Rehberi ve daha fazlası.
// ==UserScript==
// @name 🌐 Social
// @name:en 🌐 Social
// @name:pt-BR 🌐 Social
// @namespace popmundo.social
// @version 2.4
// @description Popmundo'nun en kapsamlı sosyal scripti — Efsane Speed Calling ve yalnızca burada bulabileceğin Radar Takibi, Raf, Karakter Kartı, İlgilenme Rehberi ve daha fazlası.
// @description:en The most comprehensive social script for Popmundo — Legendary Speed Calling and features you won't find anywhere else: Radar Tracking, Shelf, Character Card, Interaction Guide and more.
// @description:pt-BR O script social mais completo do Popmundo — O lendário Speed Calling e recursos exclusivos: Radar, Prateleira, Cartão de Personagem, Guia de Interação e muito mais.
// @author luke-james-gibson
// @license MIT
// @id 9g1a6x
// @match https://*.popmundo.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant unsafeWindow
// @run-at document-end
// ==/UserScript==
(function () {
try {
'use strict';
// ─── POPCONTROL DISABLE CHECK ─────────────────────────────────────────────────
try { const _ppc = JSON.parse(localStorage.getItem('ppc_enabled')||'{}'); if (_ppc['social'] === false) return; } catch {}
// ─── DEVICE WARNING ───────────────────────────────────────────────────────────
(function() {
const _isMob = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
if (!_isMob) return;
if (localStorage.getItem('ppsm_soc_mob_ack')) return;
const ov = document.createElement('div');
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:99999;display:flex;align-items:center;justify-content:center;font-family:sans-serif;padding:16px';
ov.innerHTML = '<div style="background:#fff;border-radius:12px;padding:24px;max-width:340px;width:100%;text-align:center">'
+ '<div style="font-size:36px;margin-bottom:8px">📱</div>'
+ '<div style="font-weight:bold;font-size:15px;margin-bottom:8px">Mobil Cihaz Tespit Edildi</div>'
+ '<div style="font-size:13px;color:#555;margin-bottom:16px">Bu script masaüstü cihazlar için tasarlanmıştır. Mobil kullanım için <b>Social Mobile</b> scriptini kullanmanız önerilir.</div>'
+ '<a href="https://greasyfork.org/tr/scripts/568918-social-mobile" target="_blank" style="display:inline-block;margin-bottom:16px;color:#6f42c1;font-weight:bold;font-size:13px;text-decoration:none">📥 Social Mobile — İndir / Install</a>'
+ '<div style="display:flex;gap:8px;justify-content:center">'
+ '<button id="_ppsm_ack" style="background:#6f42c1;color:#fff;border:none;border-radius:6px;padding:8px 18px;cursor:pointer;font-size:13px;font-family:inherit">Yine de Kullan</button>'
+ '<button id="_ppsm_close" style="background:#eee;border:none;border-radius:6px;padding:8px 18px;cursor:pointer;font-size:13px;font-family:inherit">Kapat</button>'
+ '</div></div>';
document.body ? document.body.appendChild(ov) : document.addEventListener('DOMContentLoaded', () => document.body.appendChild(ov));
document.addEventListener('click', function _h(e) {
if (e.target.id === '_ppsm_ack') { localStorage.setItem('ppsm_soc_mob_ack','1'); location.reload(); }
if (e.target.id === '_ppsm_ack' || e.target.id === '_ppsm_close') { ov.remove(); document.removeEventListener('click', _h); }
});
// Block rest of script via thrown exception caught below
throw new Error('__ppsm_device_block__');
})();
// CSS
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: `
.tvis-bar{position:fixed;top:0;z-index:9986;background:#f5f0ff;border:1px solid #c9b8f0;border-radius:0 0 0 6px;padding:3px 10px;font-size:11px;display:flex;gap:8px;align-items:center;box-shadow:0 1px 6px rgba(0,0,0,.15)}
.tvis-bar a,.tvis-bar button.tvis-lnk{font-weight:bold;text-decoration:none;color:#6f42c1;cursor:pointer;background:none;border:none;font-size:11px;padding:0}
.tvis-hpanel{display:none;position:fixed;top:26px;z-index:9985;background:#fff;border:1px solid #c9b8f0;border-radius:0 0 0 6px;padding:12px;min-width:240px;max-width:300px;box-shadow:0 4px 12px rgba(0,0,0,.15);max-height:82vh;overflow-y:auto}
.tvis-ov{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:99999;display:flex;align-items:flex-start;padding:40px 10px 10px;justify-content:center;overflow-y:auto}
.tvis-box{background:#fff;border-radius:8px;padding:18px;min-width:320px;max-width:min(95vw,1500px);width:95%;box-shadow:0 4px 24px rgba(0,0,0,.35)}
.tvis-title{font-weight:bold;font-size:14px;margin-bottom:12px}
.tvis-chk{display:flex;align-items:flex-start;gap:6px;margin-bottom:8px;cursor:pointer;font-size:12px;line-height:1.4}
.tvis-chk input{margin-top:2px;flex-shrink:0;cursor:pointer}
.tvis-hr{border:none;border-top:1px solid #e0e0e0;margin:6px 0}
.tvis-sec{font-size:10px;font-weight:bold;color:#888;margin:8px 0 3px;text-transform:uppercase;letter-spacing:.5px}
.tvis-lang-row{display:flex;gap:4px;margin:6px 0}
.tvis-lang-btn{flex:1;padding:3px 4px;border:1px solid #ccc;border-radius:4px;cursor:pointer;font-size:11px;background:#f8f9fa;text-align:center}
.tvis-lang-btn.active{background:#6f42c1;color:#fff;border-color:#6f42c1;font-weight:bold}
.tvis-prow{display:flex;align-items:center;gap:4px;padding:4px 6px;background:#f8f9fa;border-radius:4px;margin-bottom:3px;border:1px solid #e8e8e8;font-size:12px}
.tvis-prow[draggable]{cursor:grab}
.tvis-prow[draggable]:active{opacity:.7}
.tvis-prow.drag-over{border-top:2px solid #6f42c1}
.tvis-plink{flex:1;text-decoration:none;color:#6f42c1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tvis-pnote{font-size:10px;color:#aaa;font-style:italic;max-width:70px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:0}
.tvis-pedit{background:#f5f0ff;border:1px solid #c9b8f0;border-radius:4px;padding:6px;margin-bottom:3px;font-size:11px}
.tvis-pin-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:8px}
.tvis-ia{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;font-size:12px;text-decoration:none!important;cursor:pointer;background:transparent;border:none;padding:0;margin-left:2px;opacity:.55;vertical-align:middle;line-height:1}
.tvis-ia:hover{opacity:1}
.tvis-ia-wrap{display:inline;white-space:nowrap}
.tvis-iarow{display:flex;align-items:center;gap:4px;padding:4px 6px;background:#f8f9fa;border-radius:4px;margin-bottom:3px;border:1px solid #e8e8e8;font-size:12px}
.tvis-iarow[draggable]{cursor:grab}
.tvis-iarow[draggable]:active{opacity:.7}
.tvis-iarow.drag-over{border-top:2px solid #6f42c1}
.tvis-icon-pick{display:flex;flex-wrap:wrap;gap:3px;margin:4px 0 6px;max-height:120px;overflow-y:auto}
.tvis-icon-pick button{width:26px;height:26px;font-size:13px;cursor:pointer;border-radius:3px;border:1px solid #ddd;background:#fff;padding:0;line-height:1}
.tvis-icon-pick button.sel{border:2px solid #6f42c1;background:#f0ebff}
.tvis-custom-icons{display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px;min-height:8px}
.tvis-custom-chip{background:#f0ebff;border:1px solid #c9b8f0;border-radius:3px;padding:1px 5px;font-size:13px;display:inline-flex;align-items:center;gap:3px}
.tvis-custom-chip button{background:none;border:none;color:#e74c3c;cursor:pointer;font-size:11px;padding:0;line-height:1}
.tvis-notebar{background:#fff9e6;border:1px solid #f0c040;border-radius:4px;padding:5px 8px;margin-top:6px;display:flex;align-items:center;gap:6px}
.tvis-notebar textarea{flex:1;font-size:11px;padding:3px;border:1px solid #ddd;border-radius:3px;resize:none;height:26px;font-family:inherit}
/* CHAR HOVER POPUP */
.tvis-chpop{position:fixed;z-index:99997;background:#fff;border:1px solid #bbb;border-radius:8px;width:380px;display:none;padding:0;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.22);pointer-events:auto}
.tvis-pop-img{width:114px;min-width:114px;height:156px;flex-shrink:0;background:linear-gradient(160deg,#ddd5ff,#9b72f0);display:flex;align-items:center;justify-content:center;font-size:44px}
.tvis-pop-img img{width:114px;height:156px;object-fit:cover}
.tvis-pop-right{flex:1;display:flex;flex-direction:column;border-left:1px solid #ede9ff;min-width:0}
.tvis-pop-head{background:#f5f0ff;padding:7px 9px 5px;border-bottom:1px solid #ede9ff}
.tvis-pop-nameline{display:flex;align-items:center;gap:5px}
.tvis-pop-name{font-weight:700;font-size:12px;color:#2d1e6b;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tvis-pop-copyid{background:#ede9ff;border:1px solid #c9b8f0;color:#6f42c1;border-radius:3px;font-size:9px;padding:1px 5px;cursor:pointer}
.tvis-pop-body{flex:1;padding:5px 9px 4px;display:flex;flex-direction:column;gap:3px;overflow:hidden}
.tvis-pop-links{padding:5px 9px;border-top:1px solid #f0f0f0;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.tvis-pop-link{font-size:17px;opacity:.6;text-decoration:none}
.tvis-pop-link:hover{opacity:1}
.tvis-trackbtn{background:none;border:1px solid #c9b8f0;color:#6f42c1;border-radius:3px;font-size:9px;padding:1px 6px;cursor:pointer;flex-shrink:0}
.tvis-trackbtn.tracked{background:#fff8e6;border-color:#e0a800;color:#c8900a}
.tvis-updatebtn{background:#ede9ff;border:1px solid #c9b8f0;color:#6f42c1;border-radius:3px;font-size:9px;padding:1px 5px;cursor:pointer;display:inline-flex;align-items:center;gap:2px}
.tvis-updatebtn:hover{background:#d4c5f9;border-color:#9b72f0}
/* RADAR BULK PROGRESS */
.tvis-rprog{position:fixed;bottom:20px;left:20px;z-index:999998;background:#2d1e6b;color:#fff;font-size:12px;padding:9px 14px;border-radius:7px;box-shadow:0 4px 16px rgba(0,0,0,.35);display:flex;align-items:center;gap:10px;min-width:220px}
.tvis-rprog-txt{flex:1;white-space:nowrap}
.tvis-rprog-cancel{background:#e74c3c;border:none;color:#fff;border-radius:4px;font-size:11px;padding:2px 8px;cursor:pointer;flex-shrink:0}
.tvis-rprog-cancel:hover{background:#c0392b}
.tvis-rprog.done{background:#218838}
.tvis-rprog.stopped{background:#6c757d}
.tvis-toast{position:fixed;bottom:20px;right:20px;background:#2d1e6b;color:#fff;font-size:12px;padding:10px 16px;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.35);z-index:999999;opacity:0;transition:opacity .3s;pointer-events:none}
.tvis-toast.show{opacity:1}
/* RADAR GRID — no inner scrollbar; outer modal box scrolls */
.tvis-tgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(280px,100%),1fr));gap:6px;overflow-x:hidden}
.tvis-tcard{width:100%;height:156px;min-width:0;border:1px solid #e0e0e0;border-radius:7px;overflow:hidden;background:#fafafa;display:flex;cursor:grab;position:relative;transition:box-shadow .15s,border-color .15s}
.tvis-tcard:hover{box-shadow:0 3px 12px rgba(111,66,193,.15);border-color:#c9b8f0}
.tvis-tcard.tc-online{border-left:3px solid #28a745}
.tvis-tcard.tc-moved{border:2px solid #e74c3c;background:#fff8f8;animation:tvis-pulse 2s infinite}
.tvis-tcard.tc-updated{border-left:3px solid #f0c040;background:#fffef5}
@keyframes tvis-pulse{0%,100%{border-color:#e74c3c}50%{border-color:#ff8080}}
.tvis-tc-img{width:114px;min-width:114px;height:156px;flex-shrink:0;position:relative;overflow:hidden}
.tvis-tc-imgbg{width:114px;height:156px;background:linear-gradient(160deg,#ddd5ff,#9b72f0);display:flex;align-items:center;justify-content:center;font-size:44px}
.tvis-tc-imgbg img{width:114px;height:156px;object-fit:cover;display:block}
.tvis-tc-drag{position:absolute;top:3px;left:3px;color:rgba(255,255,255,.9);font-size:10px;background:rgba(0,0,0,.3);border-radius:2px;padding:0 3px;line-height:1.6;cursor:grab}
.tvis-tc-badge{position:absolute;bottom:4px;left:0;right:0;display:flex;justify-content:center}
.tvis-tc-badge span{font-size:8px;padding:1px 7px;border-radius:10px;color:#fff}
.badge-on{background:rgba(40,167,69,.85)}
.badge-off{background:rgba(100,100,100,.75)}
.badge-mv{background:rgba(231,76,60,.9);font-weight:700}
.badge-upd{background:rgba(240,192,64,.9);color:#333!important}
.tvis-tc-body{flex:1;min-width:0;padding:7px 7px 5px;display:flex;flex-direction:column;gap:2px;border-left:1px solid #ede9ff;overflow:hidden}
.tvis-tc-name{font-weight:700;font-size:10px;color:#6f42c1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-decoration:none;display:block}
.tvis-tc-row{font-size:9px;color:#666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.35}
.tvis-tc-row a{color:#17a2b8;text-decoration:none}
.tvis-tc-warn{font-size:9px;color:#e74c3c;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tvis-tc-warn.upd{color:#c8900a}
/* Note textarea — 2 lines tall */
.tvis-tc-note{font-size:9px;border:1px solid #e0e0e0;border-radius:3px;padding:2px 4px;width:100%;box-sizing:border-box;resize:none;font-family:inherit;color:#555;height:34px;margin-top:1px}
.tvis-tc-actions{display:flex;gap:3px;align-items:center;margin-top:auto}
.tvis-tc-time{font-size:9px;color:#bbb;white-space:nowrap}
.tvis-tc-ref{background:#ede9ff;border:1px solid #c9b8f0;color:#6f42c1;border-radius:3px;font-size:8px;padding:1px 4px;cursor:pointer}
.tvis-tc-ref:hover{background:#d4c5f9}
.tvis-tc-ok{background:#218838;color:#fff;border:none;border-radius:3px;font-size:8px;padding:1px 4px;cursor:pointer}
.tvis-tc-x{background:#e74c3c;color:#fff;border:none;border-radius:3px;font-size:8px;padding:1px 4px;cursor:pointer}
.tvis-tc-empty{width:100%;height:156px;border:1px dashed #e8e8e8;border-radius:7px;background:#fafafa}
.tvis-page-tab{background:#f8f9fa;border:1px solid #e0e0e0;color:#555;border-radius:4px;font-size:10px;padding:3px 8px;cursor:pointer;white-space:nowrap;transition:background .15s,color .15s}
.tvis-page-tab.active{color:#fff;font-weight:700}
/* RENK SEÇİCİ */
.tvis-cpick{display:flex;flex-wrap:wrap;gap:3px;margin:4px 0 6px;align-items:center}
.tvis-cpick-sw{width:20px;height:20px;border-radius:3px;border:2px solid transparent;cursor:pointer;padding:0;flex-shrink:0}
.tvis-cpick-sw.sel{border-color:#333;transform:scale(1.2)}
.tvis-cpick-sw:hover{transform:scale(1.15)}
.tvis-cpick-custom{width:26px;height:22px;padding:1px;border:1px solid #ccc;border-radius:3px;cursor:pointer;margin-left:2px}
/* RAF */
.tvis-raf-tabs{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px}
.tvis-raf-tab{padding:4px 10px;border-radius:4px;border:2px solid #ccc;font-size:11px;cursor:pointer;font-weight:600;background:#fff;white-space:nowrap;transition:.15s;user-select:none}
.tvis-raf-tab.active{color:#fff}
.tvis-raf-tab.drag-over-tab{outline:2px dashed #6f42c1;outline-offset:2px}
.tvis-raf-tab-badge{display:inline-block;background:rgba(0,0,0,.18);color:inherit;font-size:9px;font-weight:700;border-radius:8px;padding:0 4px;margin-left:4px;vertical-align:middle;min-width:14px;text-align:center}
.tvis-raf-tab.active .tvis-raf-tab-badge{background:rgba(255,255,255,.3)}
.tvis-raf-tab-edit{background:none;border:none;font-size:10px;cursor:pointer;opacity:.5;padding:0 2px}
.tvis-raf-tab-edit:hover{opacity:1}
.tvis-raf-recent-tab{padding:4px 10px;border-radius:4px;border:2px dashed #c9b8f0;font-size:11px;cursor:pointer;font-weight:600;background:#fdf8ff;color:#6f42c1;white-space:nowrap;transition:.15s}
.tvis-raf-recent-tab.active{background:#6f42c1;color:#fff;border-color:#6f42c1}
.tvis-raf-colcount{display:flex;align-items:center;gap:4px;margin-bottom:6px;font-size:10px;color:#aaa}
.tvis-raf-colcount button{padding:1px 6px;border:1px solid #ccc;border-radius:3px;background:#fff;font-size:10px;cursor:pointer;color:#666}
.tvis-raf-colcount button.sel{background:#6f42c1;color:#fff;border-color:#6f42c1}
.tvis-raf-grid{display:grid;gap:6px;min-width:600px}
.tvis-raf-col{background:#f8f9fa;border:1px solid #e0e0e0;border-radius:6px;padding:5px;min-height:60px}
.tvis-raf-col.drag-target{outline:2px dashed #6f42c1;background:#f5f0ff}
.tvis-raf-col-head{display:flex;gap:3px;align-items:center;margin-bottom:5px}
.tvis-raf-col-name{flex:1;padding:2px 4px;border:1px solid #ddd;border-radius:3px;font-size:11px;font-weight:600;background:#fff}
.tvis-raf-card{display:flex;align-items:center;gap:3px;padding:3px 5px;background:#fff;border:1px solid #e8e8e8;border-radius:3px;margin-bottom:2px;font-size:11px;cursor:grab}
.tvis-raf-card:active{opacity:.6}
.tvis-raf-card.drag-over{border-top:2px solid #6f42c1}
.tvis-raf-card.colored{border-color:transparent}
.tvis-raf-card-color{width:4px;min-width:4px;border-radius:2px;flex-shrink:0;align-self:stretch}
.tvis-raf-card-ico{flex-shrink:0;font-size:13px}
.tvis-raf-card-lbl{flex:1;text-decoration:none;color:#333;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tvis-raf-card-lbl:hover{color:#6f42c1}
.tvis-raf-card.colored .tvis-raf-card-lbl{color:inherit}
.tvis-raf-card.colored .tvis-raf-card-lbl:hover{opacity:.85;color:inherit}
.tvis-raf-card-note{font-size:9px;color:#888;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:80px;flex-shrink:0;font-style:italic}
.tvis-raf-card.colored .tvis-raf-card-note{color:inherit;opacity:.75}
.tvis-raf-edit{background:#f8f9fa;border:1px solid #ddd;border-radius:4px;padding:6px;margin-bottom:4px;font-size:11px}
/* WATCH INDICATOR */
.tvis-tab-edit-btn{background:none;border:none;font-size:10px;cursor:pointer;opacity:.5;padding:0 2px;vertical-align:middle}
.tvis-tab-edit-btn:hover{opacity:1}
.tvis-tab-edit-pop{position:fixed;z-index:999999;background:#fff;border:1px solid #c9b8f0;border-radius:6px;padding:8px;box-shadow:0 4px 14px rgba(0,0,0,.2);min-width:220px}
/* Page picker popup */
.tvis-page-picker{position:fixed;z-index:999999;background:#fff;border:1px solid #c9b8f0;border-radius:6px;padding:8px;box-shadow:0 4px 14px rgba(0,0,0,.2);min-width:190px}
/* INTERACT HELPER */
.tvis-ih-bar{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-bottom:6px;padding:5px 8px;background:#f5f0ff;border:1px solid #c9b8f0;border-radius:5px}
.tvis-ih-typebtn{padding:2px 8px;border-radius:4px;border:1px solid #c9b8f0;background:#fff;color:#6f42c1;font-size:11px;cursor:pointer;font-weight:400;transition:all .12s}
.tvis-ih-typebtn.active{background:#6f42c1;color:#fff;font-weight:700}
.tvis-ih-typebtn.t-arkadaş{border-color:#2196f3;color:#2196f3}
.tvis-ih-typebtn.t-arkadaş.active{background:#2196f3;color:#fff}
.tvis-ih-typebtn.t-romantik{border-color:#e91e63;color:#e91e63}
.tvis-ih-typebtn.t-romantik.active{background:#e91e63;color:#fff}
.tvis-ih-typebtn.t-nefret{border-color:#f44336;color:#f44336}
.tvis-ih-typebtn.t-nefret.active{background:#f44336;color:#fff}
.tvis-ih-warn{background:#fff3cd;border:1px solid #ffc107;border-radius:4px;padding:4px 8px;font-size:11px;color:#856404;margin-bottom:4px}
.tvis-ih-guide-tbl{border-collapse:collapse;font-size:11px;width:100%;min-width:580px}
.tvis-ih-guide-tbl th{background:#f5f0ff;color:#6f42c1;font-size:10px;padding:2px 4px;text-align:left;border-bottom:1px solid #c9b8f0;white-space:nowrap}
.tvis-ih-guide-tbl td{padding:2px 4px;border-bottom:1px solid #f0f0f0;vertical-align:middle}
.tvis-ih-guide-tbl td:first-child{white-space:nowrap}
.tvis-ih-guide-tbl td:nth-child(2){max-width:160px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tvis-ih-guide-tbl tr:hover td{background:#fdf8ff}.tvis-ih-joy{color:#28a745;font-size:10px;font-weight:600}
.tvis-ih-love{color:#e91e63;font-size:10px;font-weight:600}
.tvis-ih-hate{color:#dc3545;font-size:10px;font-weight:600}
.tvis-ih-note{color:#999;font-size:9px;font-style:italic}
.tvis-ih-badge{display:inline-block;font-size:9px;padding:1px 4px;border-radius:3px;margin-left:3px;font-weight:700}
.tvis-ih-badge-a{background:#e3f2fd;color:#1565c0}
.tvis-ih-badge-r{background:#fce4ec;color:#b71c1c}
.tvis-ih-badge-n{background:#ffebee;color:#b71c1c}
.tvis-ih-jlabel{display:inline-flex;align-items:center;gap:3px;font-size:11px;cursor:pointer;padding:2px 6px;border-radius:3px;border:1px solid #ddd;background:#fff}
.tvis-ih-jlabel.active-no{border-color:#28a745;color:#28a745;background:#f0fff4}
.tvis-ih-jlabel.active-yes{border-color:#e91e63;color:#e91e63;background:#fff0f5}
.frl-addbtn{display:inline-block;margin-left:5px;padding:1px 6px;border-radius:3px;border:1px solid #c9b8f0;background:#f5f0ff;color:#6f42c1;font-size:12px;cursor:pointer;vertical-align:middle;transition:all .15s;line-height:1.5}
.frl-addbtn:hover{background:#e0d0ff;border-color:#9b72f0}
.frl-addbtn.on{background:#6f42c1;color:#fff;border-color:#6f42c1}
.frl-addbtn.on:hover{background:#5a32a3;border-color:#5a32a3}
/* GENRE POPUP */
.tvis-gpop-iframe{width:100%;height:68vh;min-height:480px;border:none;border-radius:4px;display:block}
/* SERENADE HELPER */
.tvis-sh-row{display:flex;gap:6px;align-items:baseline;padding:4px 4px;border-bottom:1px solid #f0f0f0;font-size:11px}
.tvis-sh-row:last-child{border-bottom:none}
.tvis-sh-prefix{font-weight:700;color:#6f42c1;min-width:54px;flex-shrink:0;font-size:10px}
.tvis-sh-track{flex:1;color:#444;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tvis-sh-empty{font-size:11px;color:#999;padding:10px 4px;font-style:italic}
.tvis-sh-list{max-height:52vh;overflow-y:auto;margin-bottom:2px;border:1px solid #ede9ff;border-radius:4px;padding:2px 0}
.tvis-sh-footer{font-size:10px;color:#888;padding:6px 0 2px;border-top:1px solid #e0e0e0;margin-top:6px;display:flex;flex-direction:column;gap:4px}
.tvis-sh-prog{font-size:11px;color:#6f42c1;padding:3px 0;font-weight:600;min-height:16px}
.tvis-sh-warn{font-size:11px;color:#c8900a;padding:2px 0;min-height:14px}
/* SPEED CALLING BAR */
.tvis-sc-bar{position:fixed;top:0;left:0;right:0;z-index:999999;background:#1a1035;color:#fff;font-size:12px;padding:5px 12px;display:flex;align-items:center;gap:10px;box-shadow:0 2px 8px rgba(0,0,0,.4);font-family:inherit}
.tvis-sc-bar a,.tvis-sc-bar button{font-family:inherit}
.tvis-sc-stat{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tvis-sc-name{color:#c9b8f0;font-weight:600}
.tvis-sc-timer{color:#ffd700;font-weight:700;min-width:28px;text-align:center}
.tvis-sc-btn{background:#6f42c1;border:none;color:#fff;border-radius:4px;font-size:11px;padding:3px 10px;cursor:pointer;flex-shrink:0}
.tvis-sc-btn:hover{background:#5a32a3}
.tvis-sc-btn.stop{background:#e74c3c}
.tvis-sc-btn.stop:hover{background:#c0392b}
.tvis-sc-btn.resume{background:#218838}
.tvis-sc-btn.resume:hover{background:#1a6e2e}
.tvis-sc-warn{background:#fff3cd;color:#856404;border:1px solid #ffc107;border-radius:4px;padding:2px 8px;font-size:11px;flex-shrink:0}
/* SPEED CALLING MODAL */
.tvis-sc-phonerow{display:flex;align-items:center;gap:6px;margin-bottom:5px;font-size:12px}
.tvis-sc-phoneinp{flex:1;padding:3px 6px;border:1px solid #c9b8f0;border-radius:4px;font-size:11px;font-family:monospace}
.tvis-sc-charlist{max-height:180px;overflow-y:auto;border:1px solid #e0e0e0;border-radius:4px;margin:4px 0 8px;font-size:11px}
.tvis-sc-charrow{display:flex;align-items:center;gap:6px;padding:3px 7px;border-bottom:1px solid #f0f0f0}
.tvis-sc-charrow:last-child{border-bottom:none}
.tvis-sc-charrow input[type=checkbox]{flex-shrink:0;cursor:pointer}
.tvis-sc-badge{font-size:9px;padding:1px 5px;border-radius:8px;font-weight:700}
.tvis-sc-badge.r{background:#fce4ec;color:#b71c1c}
.tvis-sc-badge.a{background:#e3f2fd;color:#1565c0}
` }));
// 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 gmGet = (k, def = null) => { try { const v = GM_getValue(k, null); return v !== null ? (typeof v === 'string' && (v[0] === '[' || v[0] === '{') ? JSON.parse(v) : v) : def; } catch { return def; } };
const gmSet = (k, v) => GM_setValue(k, typeof v === 'object' ? JSON.stringify(v) : v);
const gmDel = k => GM_deleteValue(k);
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) === '1';
const isOnDef = (k, def=false) => { const v = CK.get(k); return v !== null ? v === '1' : def; };
const guard = (key, ...urls) => isOnDef(key, true) && (!urls.length || urls.some(u => location.href.includes(u)));
const PM = '/World/Popmundo.aspx';
const normUrl = href => { try { const u = new URL(href, location.href); return u.pathname + u.search; } catch { return href; } };
// LANG
const LANG = CK.get('ppm_lang') || 'TR';
const _D = (tr, en, pt) => ({ TR: tr, EN: en, PT: pt });
const dateLocale = { TR: 'tr-TR', EN: 'en-GB', PT: 'pt-BR' }[LANG];
const STR = {
menuTitle: _D('🌐 Social', '🌐 Social', '🌐 Social'),
menuClip: _D('📋 Pano', '📋 Clipboard', '📋 Painel'),
menuRadar: _D('📍 Radar', '📍 Radar', '📍 Radar'),
save: _D('✔ Tamam', '✔ Save', '✔ Salvar'),
close: _D('Kapat', 'Close', 'Fechar'),
langLabel: _D('Dil', 'Language', 'Idioma'),
backup: _D('📤 Yedekle', '📤 Backup', '📤 Exportar'),
restore: _D('📥 Geri Yükle', '📥 Restore', '📥 Importar'),
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'),
// features — SOSYAL
pins: _D('📌 Rafım — Sanatçı, mekan, şehir, forum ve favori bağlantıları klasörlü raflarda sakla',
'📌 Shelf — Store artist, locale, city, forum & favourite links in organized shelves',
'📌 Prateleira — Salve artistas, locais, cidades, fórum e favoritos em prateleiras'),
charCard: _D('⭐ Karakter Kartı — İsme hover\'da anlık bilgi: konum, tavır, online durumu',
'⭐ Character Card — Hover for instant info: location, attitude, online status',
'⭐ Cartão de Personagem — Hover para info: localização, atitude, status online'),
radar: _D('📍 Radar — 10 sekmede 200 karakter takibi',
'📍 Radar — Track 200 characters on 10 tabs',
'📍 Radar — Rastreie 200 personagens em 10 abas'),
interactfilter: _D('👋 İlgilenme Rehberi — Seçeneklere keyif/romantizm/nefret değerlerini göster, filtrele',
'👋 Interaction Guide — Show joy/romance/hatred values, filter by type',
'👋 Guia de Interação — Mostre valores de alegria/romance/ódio, filtre'),
quickLinks: _D('🔗 Hızlı Bağlantılar — Karakter ve mekan sayfalarına mesaj/git/para ver butonları ekle',
'🔗 Quick Links — Add message/go/send money buttons to character & locale pages',
'🔗 Links Rápidos — Botões de mensagem/ir/dinheiro em personagens e locais'),
note: _D('📝 Karakter Notu — Her profilde kalıcı kişisel not alanı',
'📝 Character Note — Persistent personal note field on every profile',
'📝 Nota de Personagem — Campo de nota pessoal em cada perfil'),
// features — ARAÇLAR
diary: _D('🔍 Günlük Filtresi — Günlük girişlerini gerçek zamanlı ara ve filtrele',
'🔍 Diary Filter — Real-time search and filter for diary entries',
'🔍 Filtro de Diário — Busca em tempo real nas entradas do diário'),
moneyFmt: _D('💰 Para Biçimlendirici — Büyük sayıları otomatik noktalı yaz: 1.500.000',
'💰 Money Formatter — Automatically format large amounts: 1,500,000',
'💰 Formatador de Dinheiro — Formate valores grandes: 1.500.000'),
addIcon: _D('+ Simge Ekle', '+ Add Icon', '+ Adicionar Ícone'),
customIconPlh: _D('Emoji yapıştır...', 'Paste emoji...', 'Cole emoji...'),
customIconTitle: _D('Özel Simgeler', 'Custom Icons', 'Ícones Personalizados'),
delAllIcons: _D('🗑 Hepsini Sil', '🗑 Delete All', '🗑 Excluir Todos'),
delAllIconsConfirm:_D('Tüm özel simgeler silinsin mi?','Delete all custom icons?', 'Excluir todos os ícones?'),
exportIcons: _D('📤 Dışa Aktar', '📤 Export', '📤 Exportar'),
emojiRef: _D('🔗 Simge bul: getemoji.com', '🔗 Find icons: getemoji.com', '🔗 Encontrar ícones: getemoji.com'),
// pins
pinBtn: _D('📌 Rafım', '📌 Shelf', '📌 Prateleira'),
rafForum: _D('Forum', 'Forum', 'Fórum'),
rafArtists: _D('Sanatçılar', 'Artists', 'Artistas'),
rafLocales: _D('Mekanlar', 'Locales', 'Locais'),
rafCities: _D('Şehirler', 'Cities', 'Cidades'),
rafFav: _D('Favoriler', 'Favorites', 'Favoritos'),
rafChars: _D('Karakterler', 'Characters', 'Personagens'),
rafWork: _D('İş & Stüdyo', 'Work & Studio', 'Trabalho & Estúdio'),
rafGoals: _D('Hedefler', 'Goals', 'Objetivos'),
rafColDefault: _D('Sıra', 'Lane', 'Faixa'),
rafEmpty: _D('Klasör boş.', 'Folder is empty.', 'Pasta vazia.'),
rafEditFolder: _D('Klasörü Düzenle', 'Edit Folder', 'Editar Pasta'),
rafEditCol: _D('Sütunu Düzenle', 'Edit Column', 'Editar Coluna'),
rafItemNote: _D('Not...', 'Note...', 'Nota...'),
rafDup: _D('Bu URL zaten eklendi!', 'This URL already added!', 'Este URL já foi adicionado!'),
colorPicker: _D('Renk Seçici', 'Color Picker', 'Seletor de Cor'),
scAskSC: _D('Speed Calling listesine de eklensin mi?','Add to Speed Calling list?','Adicionar à lista Speed Calling?'),
yes: _D('Evet', 'Yes', 'Sim'),
ppEmpty: _D('Henüz pin yok.', 'No pins yet.', 'Nenhum pin ainda.'),
ppDup: _D('Bu sayfa zaten pinli!', 'This page is already pinned!', 'Esta página já está fixada!'),
pinAdd: _D('📌 Yeni Pin Ekle', '📌 Add New Pin', '📌 Adicionar Novo Pin'),
pinSave: _D('📌 Pinle', '📌 Pin', '📌 Fixar'),
pinName: _D('Ad:', 'Name:', 'Nome:'),
pinNote: _D('Not:', 'Note:', 'Nota:'),
pinIcon: _D('Simge', 'Icon', 'Ícone'),
// clipboard
chTitle: _D('📋 Pano Geçmişi', '📋 Clipboard History', '📋 Histórico de Área de Transferência'),
chEmpty: _D('Henüz kopyalanan ID yok.', 'No IDs copied yet.', 'Nenhum ID copiado ainda.'),
chClear: _D('Tümünü Temizle', 'Clear All', 'Limpar Tudo'),
chCopy: _D('Kopyala', 'Copy', 'Copiar'),
// quick links (inline actions)
iaEdit: _D('🔗 Hızlı Bağlantıları Düzenle', '🔗 Edit Quick Links', '🔗 Editar Links Rápidos'),
iaDefaults: _D('↩ Varsayılana Dön', '↩ Reset to Defaults', '↩ Restaurar Padrões'),
iaAddSec: _D('+ Yeni Link', '+ New Link', '+ Novo Link'),
iaUrlPlh: _D('URL yapıştır...', 'Paste URL...', 'Cole a URL...'),
iaUrlInfo: _D('Kaydedilecek yol:', 'Path to save:', 'Caminho a salvar:'),
iaLblPlh: _D('Buton adı...', 'Button name...', 'Nome do botão...'),
iaSave: _D('Kaydet', 'Save', 'Salvar'),
// diary
dfTitle: _D('🔍 Günlük Filtresi', '🔍 Diary Filter', '🔍 Filtro de Diário'),
dfPlh: _D('Ara...', 'Search...', 'Buscar...'),
dfClear: _D('Temizle', 'Clear', 'Limpar'),
dfCount: _D('eşleşme', 'matches', 'correspondências'),
dfTotal: _D('kayıt', 'entries', 'entradas'),
// note
notePlh: _D('Bu karakter için not...', 'Note for this character...', 'Nota para este personagem...'),
// char card popup
cpLoading: _D('⏳ Yükleniyor...', '⏳ Loading...', '⏳ Carregando...'),
cpTrack: _D('☆ Radar\'a Ekle', '☆ Add to Radar', '☆ Adicionar ao Radar'),
cpTracked: _D('⭐ Radarda', '⭐ On Radar', '⭐ No Radar'),
cpOffline: _D('Çevrimdışı', 'Offline', 'Desconectado'),
cpAttitude: _D('Tavır', 'Attitude', 'Atitude'),
cpState: _D('Durum', 'State', 'Estado'),
cpRefresh: _D('🔄 Manuel Güncelle', '🔄 Update Manually', '🔄 Atualizar Manualmente'),
// radar
tkTitle: _D('📍 Takip İstasyonu', '📍 Tracking Station', '📍 Estação de Rastreamento'),
tkEmpty: _D('Radar boş.', 'Radar is empty.', 'Radar está vazio.'),
tkConfirmRm: _D('Radardan çıkarılsın mı?', 'Remove from radar?', 'Remover do radar?'),
tkLastSeen: _D('Son Giriş:', 'Last Seen:', 'Último Acesso:'),
tkLastUpd: _D('','',''),
tkConfirmOk: _D('✔ Onayla', '✔ Confirm', '✔ Confirmar'),
tkNote: _D('Not...','Note...','Nota...'),
tkBgDone: _D('📍 Radar güncellendi', '📍 Radar updated', '📍 Radar atualizado'),
tkBgStopped: _D('⛔ Güncelleme durduruldu', '⛔ Update stopped', '⛔ Atualização interrompida'),
tkBgRunning: _D('📍 Radar güncelleniyor...', '📍 Updating radar...', '📍 Atualizando radar...'),
tkBgCancel: _D('Durdur', 'Stop', 'Parar'),
tkUpdate: _D('🔄 Sayfayı Güncelle', '🔄 Update Page', '🔄 Atualizar Página'),
tkTabEdit: _D('Sekmeyi Düzenle', 'Edit Tab', 'Editar Aba'),
tkGoTo: _D('Yanına Git', 'Go To', 'Ir Para'),
tkPageSelect: _D('Hangi sayfaya eklensin?', 'Which page to add to?', 'Em qual página adicionar?'),
tkPageMove: _D('Hangi sayfaya taşınsın?', 'Which page to move to?', 'Para qual página mover?'),
tkPageFull: _D('Bu sayfa dolu!', 'This page is full!', 'Esta página está cheia!'),
tkRemove: _D('⭐ Radardan Çıkar', '⭐ Remove from Radar', '⭐ Remover do Radar'),
tkMove: _D('↕ Sayfayı Değiştir', '↕ Move to Page', '↕ Mover para Página'),
// planner
plDesc: _D('Ne yapılacak?', 'What to do?', 'O que fazer?'),
plDescPlh: _D('Görev gir...', 'Enter task...', 'Digite a tarefa...'),
plSchedule: _D('PROGRAM', 'SCHEDULE', 'CRONOGRAMA'),
plTabDef: _D('Sekme', 'Tab', 'Aba'),
plNoTasks: _D('Görev yok.', 'No tasks.', 'Sem tarefas.'),
// custom icons
interactFilter: _D('İlişki Tipi:', 'Relationship Type:', 'Tipo de Relação:'),
interactAll: _D('🌐 Standart', '🌐 Standard', '🌐 Padrão'),
interactFriend: _D('👥 Arkadaşlık', '👥 Friendship', '👥 Amizade'),
interactRomantic: _D('💕 Romantizm', '💕 Romance', '💕 Romance'),
interactHate: _D('😡 Nefret', '😡 Hatred', '😡 Ódio'),
interactGuide: _D('ℹ️ Rehber', 'ℹ️ Guide', 'ℹ️ Guia'),
interactGuideTitle: _D('İlgilen Seçenekleri Rehberi', 'Interaction Options Guide', 'Guia de Opções de Interação'),
interactJealous: _D('💘 Kıskançlık:', '💘 Jealousy:', '💘 Ciúme:'),
interactJealousNo: _D('Kıskanmam', "Won't be jealous", 'Não vou ciúmar'),
interactJealousYes: _D('Kıskanırım', 'Will be jealous', 'Vou ciúmar'),
interactWarn: _D('⚠️ Romantizm 70+ — Kıskançlık riski yüksek!', '⚠️ Romance 70+ — High jealousy risk!', '⚠️ Romance 70+ — Alto risco de ciúme!'),
interactColJoy: _D('Keyif', 'Joy', 'Alegria'),
interactColLove: _D('Romantizm', 'Romance', 'Romance'),
interactColHate: _D('Nefret', 'Hatred', 'Ódio'),
interactColNote: _D('Koşul', 'Condition', 'Condição'),
interactSave: _D('Kaydet', 'Save', 'Salvar'),
interactType: _D('Tip', 'Type', 'Tipo'),
interactName: _D('Seçenek', 'Option', 'Opção'),
interactDataEdit: _D('📊 İlgilen Verilerini Düzenle', '📊 Edit Interact Data', '📊 Editar Dados de Interação'),
interactDataId: _D('Seçenek ID', 'Option ID', 'ID da Opção'),
interactDataAdd: _D('+ Ekle', '+ Add', '+ Adicionar'),
interactDataEmpty: _D('Özel veri yok.', 'No custom data.', 'Nenhum dado personalizado.'),
interactDataDup: _D('Bu ID zaten var!', 'This ID already exists!', 'Este ID já existe!'),
interactDataReset: _D('🗑 Tümünü Sil', '🗑 Delete All', '🗑 Excluir Todos'),
interactDataResetQ: _D('Tüm özel veriler silinsin mi?', 'Delete all custom data?', 'Excluir todos os dados personalizados?'),
// radar badges
badgeMoved: _D('📍 TAŞINDI', '📍 MOVED', '📍 MUDOU'),
badgeUpdated: _D('✎ Güncellendi', '✎ Updated', '✎ Atualizado'),
badgeOnline: _D('● Çevrimiçi', '● Online', '● Online'),
// radar card warnings
tcWarnMoved: _D('⚠️ Önceki:', '⚠️ Previous:', '⚠️ Anterior:'),
tcWarnUpdated: _D('✎ Tavır/durum değişti', '✎ Attitude/status changed', '✎ Atitude/estado mudou'),
// radar footer
tcFooterHint: _D('⠿ Sürükle & sırala · ✏️ Sekme düzenle', '⠿ Drag & sort · ✏️ Edit tab', '⠿ Arrastar & ordenar · ✏️ Editar aba'),
tcPage: _D('Sayfa', 'Page', 'Página'),
tcTotal: _D('Toplam', 'Total', 'Total'),
// misc
cash: _D('Nakit', 'Cash', 'Dinheiro'),
// custom icons
ciNoIcons: _D('Simge yok!', 'No icons!', 'Nenhum ícone!'),
ciCopied: _D('📋 Simgeler kopyalandı!', '📋 Icons copied!', '📋 Ícones copiados!'),
// ia reset confirm
iaResetConfirm: _D('Varsayılana dön?', 'Reset to defaults?', 'Restaurar padrões?'),
// genre popup
genrePopupBtn: _D('🎼 Tür Popülerliğini Gör', '🎼 View Genre Popularity', '🎼 Ver Popularidade do Gênero'),
genrePopupTitle: _D('🎼 Müzik Türü Popülerliği', '🎼 Genre Popularity', '🎼 Popularidade do Gênero'),
// serenade helper
shBtn: _D('🎵 Serenat Helper', '🎵 Serenade Helper', '🎵 Ajudante de Serenata'),
shTitle: _D('🎵 Serenat Helper', '🎵 Serenade Helper', '🎵 Ajudante de Serenata'),
shNoCache: _D('Cache yok — güncelle butonuna bas.', 'No cache — press update.', 'Sem cache — pressione atualizar.'),
shLastUpd: _D('Son güncelleme:', 'Last update:', 'Última atualização:'),
shNextUpd: _D('Sonraki güncelleme:', 'Next update:', 'Próxima atualização:'),
shUpdateBtn: _D('🔄 Güncelle', '🔄 Update', '🔄 Atualizar'),
shRadioLink: _D('📻 Radyo Sıralamaları', '📻 Radio Charts', '📻 Paradas de Rádio'),
shWarn1: _D('Son güncelleme: {date} — Çarşamba 10:00 CET\'e kadar güncellemeye gerek yok.',
'Last update: {date} — No update needed until Wednesday 10:00 CET.',
'Última atualização: {date} — Sem necessidade até quarta 10:00 CET.'),
shWarn2: _D('Güncelleme gerçekten gerekli mi? Tekrar bas.',
'Is an update really necessary? Press again.',
'A atualização é realmente necessária? Pressione novamente.'),
shFetching: _D('📻 İstasyon yükleniyor: {n}/17', '📻 Fetching station: {n}/17', '📻 Buscando estação: {n}/17'),
shDone: _D('✅ Güncelleme tamamlandı!', '✅ Update complete!', '✅ Atualização concluída!'),
shErr: _D('❌ Bir hata oluştu.', '❌ An error occurred.', '❌ Erro ao atualizar.'),
shNoneMatched: _D('Bu restoranda radyo listesiyle eşleşen şarkı bulunamadı.',
'No songs matched the radio list at this restaurant.',
'Nenhuma música correspondeu à lista de rádio neste restaurante.'),
// feature toggles
genrePopup: _D('🎼 Tür Popülerliği — Şarkı eklerken tür popülerliği popup\'ı',
'🎼 Genre Popularity — Popup when adding songs to repertoire',
'🎼 Popularidade do Gênero — Popup ao adicionar músicas'),
serenadeHelper: _D('🎵 Serenat Helper — Serenat sayfasında radyo önekleri ve güncelleme',
'🎵 Serenade Helper — Radio prefixes & cache on serenade page',
'🎵 Ajudante de Serenata — Prefixos de rádio na serenata'),
speedcall: _D('📞 Hızlı Arama — Arkadaş ve romantik karakterleri sırayla otomatik ara; aralık ve telefon seçenekleri ayarlanabilir',
'📞 Speed Calling — Auto-call friends & romantics in sequence; adjustable interval and call options',
'📞 Ligação Rápida — Ligue automaticamente para amigos e românticos em sequência; intervalo e opções ajustáveis'),
// hardcoded strings
folderTitle: _D('Başlık', 'Title', 'Título'),
recentTab: _D('🕐 Son Eklenenler', '🕐 Recently Added', '🕐 Adicionados Recentemente'),
recentEmpty: _D('Henüz kayıtlı öğe yok.', 'No saved items yet.', 'Nenhum item salvo ainda.'),
recentCount: _D('Son {n} eklenen', 'Last {n} added', 'Últimos {n} adicionados'),
goToFolder: _D('Klasöre git', 'Go to folder', 'Ir para pasta'),
colLabel: _D('Sütun:', 'Column:', 'Coluna:'),
addWhere: _D('Nereye ekleyelim?', 'Where to add?', 'Onde adicionar?'),
deleteEntry: _D('Bu kaydı sil?', 'Delete this entry?', 'Excluir este registro?'),
interactDataWip: _D('⚠️ Bu bölüm çalışma aşamasındadır — eklenen veriler rehbere ve dropdown\'a anında yansır.',
'⚠️ This section is a work in progress — added data reflects immediately.',
'⚠️ Esta seção está em desenvolvimento — os dados adicionados refletem imediatamente.'),
interactDataLegend: _D('✓ = Bu sayfada mevcut · Soluk = Sadece rehberde · 🟢 Arka plan = Kullanıcı ekledi',
'✓ = On this page · Faded = Guide only · 🟢 Background = User added',
'✓ = Nesta página · Esmaecido = Apenas guia · 🟢 Fundo = Adicionado pelo usuário'),
// speed calling
scBtn: _D('📞 Ara', '📞 Call', '📞 Ligar'),
scTitle: _D('📞 Speed Calling', '📞 Speed Calling', '📞 Speed Calling'),
scInterval: _D('Arama arası (sn):', 'Interval (sec):', 'Intervalo (seg):'),
scIntervalNote: _D('+ 0-2sn rastgele eklenir', '+ 0-2s random added', '+ 0-2s aleatório'),
scPhoneIds: _D('Telefon Seçenek ID\'leri', 'Phone Option IDs', 'IDs de Opção de Telefone'),
scPhoneNote: _D('Virgülle ayır · Soldan denenecek · Güncel ID bilinmiyorsa boş bırak',
'Comma separated · Tried left to right · Leave empty if unknown',
'Separado por vírgula · Da esquerda para direita · Deixe vazio se desconhecido'),
scFriendIds: _D('Arkadaş aramaları:', 'Friendship calls:', 'Ligações de amizade:'),
scRomIds: _D('Romantik aramalar:', 'Romantic calls:', 'Ligações românticas:'),
scWho: _D('Kimler aransın?', 'Who to call?', 'Quem ligar?'),
scWhoFriend: _D('Arkadaşlar', 'Friends', 'Amigos'),
scWhoRom: _D('Romantikler', 'Romantics', 'Românticos'),
scStart: _D('▶ Başlat', '▶ Start', '▶ Iniciar'),
scResume: _D('▶ Kaldığı Yerden Devam', '▶ Resume', '▶ Retomar'),
scReset: _D('↺ Sıfırla', '↺ Reset', '↺ Reiniciar'),
scNoChars: _D('Aramak için kayıtlı arkadaş/romantik karakter bulunamadı.',
'No saved friend/romantic characters found.',
'Nenhum personagem amigo/romântico encontrado.'),
scBarCalling: _D('📞 Aranıyor:', '📞 Calling:', '📞 Ligando:'),
scBarDone: _D('✅ Tüm aramalar tamamlandı!', '✅ All calls done!', '✅ Todas as ligações concluídas!'),
scBarStop: _D('Durdur', 'Stop', 'Parar'),
scBarResume: _D('Devam Et', 'Resume', 'Continuar'),
scBarSkip: _D('Geç', 'Skip', 'Pular'),
scBarFail: _D('⚠️ 2 ardışık hata — Script arama yapamıyor!', '⚠️ 2 consecutive errors — Script cannot call!', '⚠️ 2 erros consecutivos — Script não consegue ligar!'),
scBarFailNote: _D('Denemeye devam et?', 'Continue trying?', 'Continuar tentando?'),
scBarOf: _D('/', '/', '/'),
scBarNext: _D('Sonraki:', 'Next:', 'Próximo:'),
scBarNoId: _D('ID bulunamadı — atlandı', 'No ID found — skipped', 'ID não encontrado — pulado'),
};
const _clSocial = (() => { try { const v = localStorage.getItem('ppc_lc_social'); return v ? JSON.parse(v) : null; } catch { return null; } })();
const s = k => { if (_clSocial && _clSocial[k]) return _clSocial[k]; const v = STR[k]; if (!v) return k; return v[LANG] ?? v['TR'] ?? k; };
// SETTINGS KEYS (cookies)
const K = {
pins: 'tvis_feat_pins',
charPopup: 'tvis_feat_chpopup',
tracking: 'tvis_feat_tracking',
ia: 'tvis_feat_ia',
note: 'tvis_feat_cnote',
diary: 'tvis_feat_dfl',
};
// DATA KEYS (GM_setValue)
const DK = {
PINS: 'tvis_pins',
CLIP: 'tvis_clip',
IA: 'tvis_ia_',
CACHE: 'tvis_char_cache',
TRACK: 'tvis_track', // legacy — kept for backup compat
TRACK_V2: 'tvis_track_v2_', // prefix; append page index 0-9
TRACK_PAGE: 'tvis_track_page',
NOTES: 'tvis_notes',
LAST_NOTIF: 'ayu_last_notif_date',
RADAR_TABS: 'tvis_radar_tabs',
RAF: 'tvis_raf',
WATCH: 'tvis_watch_state',
WATCH_NOTIF: 'tvis_watch_notified',
CUSTOM_ICONS:'tvis_custom_icons',
INTERACT_TYPE: 'tvis_interact_type_',
INTERACT_CUSTOM: 'tvis_interact_custom',
RADIO_CACHE: 'tvis_radio_cache',
SPEEDCALL_Q: 'tvis_sc_queue',
SPEEDCALL_STATE: 'tvis_sc_state',
SPEEDCALL_CFG: 'tvis_sc_cfg',
SC_CHARS: 'tvis_sc_chars',
};
// RADAR TABS
const RADAR_PAGE_SIZE = 20;
const RADAR_PAGES = 10;
// ── Radar V2 helpers (per-page independent arrays, no sentinels) ──────────────
const getRadarPage = idx => gmGet(DK.TRACK_V2 + idx, []) || [];
const setRadarPage = (idx, arr) => gmSet(DK.TRACK_V2 + idx, arr);
const getTrackedPage = charId => {
for (let i = 0; i < RADAR_PAGES; i++) {
if (getRadarPage(i).some(e => String(e.charId) === String(charId))) return i;
}
return -1;
};
const isTrackedV2 = charId => getTrackedPage(charId) >= 0;
const removeFromRadar = charId => {
for (let i = 0; i < RADAR_PAGES; i++) {
const p = getRadarPage(i);
const idx = p.findIndex(e => String(e.charId) === String(charId));
if (idx >= 0) { p.splice(idx, 1); setRadarPage(i, p); return true; }
}
return false;
};
const addToRadar = (pageIdx, entry) => {
if (getRadarPage(pageIdx).length >= RADAR_PAGE_SIZE) return false;
if (isTrackedV2(entry.charId)) return false;
const p = getRadarPage(pageIdx);
p.push(entry);
setRadarPage(pageIdx, p);
return true;
};
const moveInRadar = (charId, newPageIdx) => {
const oldPage = getTrackedPage(charId);
if (oldPage < 0) return false;
const oldArr = getRadarPage(oldPage);
const idx = oldArr.findIndex(e => String(e.charId) === String(charId));
if (idx < 0) return false;
const [entry] = oldArr.splice(idx, 1);
setRadarPage(oldPage, oldArr);
const newArr = getRadarPage(newPageIdx);
newArr.push(entry);
setRadarPage(newPageIdx, newArr);
return true;
};
const updateInRadar = (charId, data) => {
const pageIdx = getTrackedPage(charId);
if (pageIdx < 0) return;
const p = getRadarPage(pageIdx);
const idx = p.findIndex(e => String(e.charId) === String(charId));
if (idx >= 0) { p[idx] = { ...p[idx], ...data, savedAt: Date.now() }; setRadarPage(pageIdx, p); }
};
const getAllTracked = () => {
const all = [];
for (let i = 0; i < RADAR_PAGES; i++) getRadarPage(i).forEach(e => all.push(e));
return all;
};
const DEFAULT_RADAR_TABS = [
{ icon:'⭐', name:'YAKIN TAKİP', color:'#ffd700' },
{ icon:'👪', name:'AİLE', color:'#28a745' },
{ icon:'🤝', name:'ARKADAŞ', color:'#007bff' },
{ icon:'🎯', name:'TAKİP', color:'#fd7e14' },
{ icon:'⚔️', name:'RAKİP', color:'#dc3545' },
{ icon:'🎸', name:'BAND', color:'#6f42c1' },
{ icon:'💼', name:'İŞ', color:'#17a2b8' },
{ icon:'🌐', name:'DÜNYA', color:'#20c997' },
{ icon:'📌', name:'ÖZEL', color:'#e83e8c' },
{ icon:'📡', name:'DİĞER', color:'#6c757d' },
];
const getRadarTabs = () => {
const saved = gmGet(DK.RADAR_TABS, null);
if (saved && Array.isArray(saved) && saved.length >= RADAR_PAGES) return saved;
// Migrate if shorter
if (saved && Array.isArray(saved) && saved.length > 0) {
const merged = DEFAULT_RADAR_TABS.map((def, i) =>
saved[i] ? { ...def, ...saved[i] } : def
);
return merged;
}
return DEFAULT_RADAR_TABS.map(t => ({ ...t }));
};
// CUSTOM ICONS
const getCustomIcons = () => gmGet(DK.CUSTOM_ICONS, []) || [];
// BACKUP & RESTORE
const dbExport = () => {
const data = { v: 2, script: 'social', cookies: {}, gm: {} };
Object.values(K).forEach(k => { const v = CK.get(k); if (v !== null) data.cookies[k] = v; });
data.cookies['ppm_lang'] = CK.get('ppm_lang') || 'TR';
[DK.PINS, DK.RAF, DK.CLIP, DK.CACHE, DK.NOTES, DK.RADAR_TABS, DK.CUSTOM_ICONS, DK.INTERACT_CUSTOM, DK.SPEEDCALL_CFG].forEach(k => {
const v = gmGet(k, null); if (v !== null) data.gm[k] = v;
});
// Radar V2 pages
for (let i = 0; i < RADAR_PAGES; i++) {
const k = DK.TRACK_V2 + i; const v = gmGet(k, null); if (v !== null) data.gm[k] = v;
}
['character','locale','artist','city'].forEach(sec => {
const k = DK.IA + sec, v = gmGet(k, null); if (v !== null) data.gm[k] = v;
});
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-social-${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 apply = (mode) => {
close();
if (mode === 'merge') {
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 (gmGet(k, null) === null) gmSet(k,v); });
} else {
if (data.cookies) Object.entries(data.cookies).forEach(([k,v]) => CK.set(k,v));
if (data.gm) Object.entries(data.gm).forEach(([k,v]) => gmSet(k,v));
}
location.reload();
};
const row = mk('div'); row.style.cssText='display:flex;gap:8px';
row.append(mkB(s('mergeLbl'),'btn-b',()=>apply('merge')), mkB(s('replaceLbl'),'btn-r',()=>apply('replace')), mkB(s('cancelLbl'),'btn-grey',close));
cont.appendChild(row);
});
};
reader.readAsText(f);
};
inp.click();
};
// DRAG & DROP
const mkDraggable = (container, onReorder) => {
let drag = null;
container.addEventListener('dragstart', e => { drag = e.target.closest('[draggable]'); drag?.classList.add('dragging'); });
container.addEventListener('dragend', () => {
drag?.classList.remove('dragging');
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
if (drag) onReorder([...container.querySelectorAll('[draggable]')].map(el => el.dataset.did));
drag = null;
});
container.addEventListener('dragover', e => {
e.preventDefault();
const tgt = e.target.closest('[draggable]'); if (!tgt || tgt === drag) return;
container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
tgt.classList.add('drag-over');
const r = tgt.getBoundingClientRect();
container.insertBefore(drag, e.clientY > r.top + r.height / 2 ? tgt.nextSibling : tgt);
});
container.addEventListener('drop', e => e.preventDefault());
};
// ICON PICKER (includes custom icons at top)
const ICONS = [
'🧑','👤','👑','🎭','🕵️','🧙','🦸','💃','🕺','🎤','🧛','🧟','🧝','🧜','🤴','👸','🤵','👩🎤','👨🎤','🤩','😎',
'😀','😃','😄','😁','😆','😅','😂','🙂','🙃','😉','😊','😇','🥰','😍','🤗','🤭','🤫','🤔','🫡','🤠','🥳',
'😺','😸','😹','😻','😼','😽','🙀','😿','😾','👋','🤚','✋','🖐️','🖖','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙',
'👈','👉','👆','👇','☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🎵','🎶','🎸','🎹','🎺','🎻',
'🥁','🎷','🎼','🎙️','🎚️','🎛️','🔊','📢','📣','🎧','🪕','🏠','🏡','🏢','🏤','🏥','🏦','🏨','🏩','🏪','🏫','🏬',
'🏭','🏯','🏰','🗼','🗽','⛪','🕌','🎪','🎠','🎡','🏟️','🚪','🛋️','🛏️','🌆','🌇','🌃','🌉','🌁','🌍','🗺️','📍',
'⛰️','🏕️','🏝️','🏖️','🚀','🚒','✈️','🍳','🌾','🏫','⚖️','🔧','🏭','🎓','🎬','✈️','🛩️','🚀','🛸','🛰️','🚁','🛶',
'⛵','🚤','🚢','🚗','🚕','🚙','🚌','🏎️','🚓','🚑','🧭','🧳','☀️','🌤️','🌥️','🌦️','🌩️','🌨️','❄️','⛄','🌬️','💨',
'🌪️','🌫️','🌈','☂️','☔','🌙','⚡','🌊','🌱','🌿','🍀','🌵','🌴','🌳','🌲','🪵','🌾','🌺','🌸','🌼','🌻','🌹','🥀',
'🌷','🍁','🍂','🍃','🦁','🐯','🦊','🐺','🦝','🐻','🐗','🐴','🐐','🐑','🐄','🦒','🐘','🦛','🐁','🐀','🦄','🐲','🐉',
'🐊','🐢','🦎','🐍','🦅','🦉','🦋','🦆','🦢','🦩','🦚','🦜','🦇','🐦','🐧','🐟','🐠','🐬','🐳','🦈','🦂','🕷️','🕸️',
'🐞','🐜','🐝','🐾','🍺','🍻','🥂','🍷','🍸','🍹','🥃','🍾','☕','🧃','🍵','🧋','🥤','🍼','🥛','🍕','🍔','🌮','🍜','🍣',
'🍱','🎂','🍰','🍫','🍬','🌭','🥗','🥘','🍲','🍛','🍝','🥟','🍤','🍚','🍢','🍡','🍧','🍨','🍦','🥧','🧁','🍮','🧊',
'🍏','🍎','🍊','🍋','🍌','🍉','🍇','🍓','🍒','🍑','🍍','🥥','🥝','🍅','🥑','🥕','🌽','🌶️','🥒','🥦','🧄','🍄','🥔',
'🍞','🥐','🥖','🧀','🥚','🍳','🥞','🧇','🥓','🍗','🍖','⭐','🌟','💫','✨','🔥','❤️','🧡','💛','💚','💙','💜','🖤',
'🤍','🤎','💎','💰','🏆','🥇','🥈','🥉','🎖️','🏅','💼','📋','📊','📈','📉','📂','🗃️','📌','🔖','🏷️','✂️','🖊️','🖋️',
'✒️','📝','⌚','⏰','⌛','⏳','🎉','🎊','🧸','🪅','🧨','🪓','🔔','⏱️','⏲️','🕰️','⌨️','🖱️','📀','📸','🕯️','🪔','📃',
'📜','📄','🖇️','🧷','🧾','🗂️','🗄️','🗑️','🪣','⛓️','🛌','🚿','🛁','🚽','🧻','🅰️','🅱️','🆒','🆓','🆔','🆕','🆗','⤴️',
'⤵️','🔙','🔚','🔛','🔜','🔝','🥎','🏉','🥏','🏒','🏑','🥍','🏏','🛹','🛷','⛸️','🥌','🎮','🕹️','🎲','🎯','🎬','🎞️',
'📽️','🎥','📺','🎽','✅','❌','⚠️','🚨','❔','❕','❓','❗','🛑','⛔','🚫','💯','🔴','🟠','🟡','🟢','🔵','🟣','🟤','⚪',
'⚫','🟥','🟧','🟨','🟩','🟦','🟪','🟫','⬜','⬛','🚸','🚦','🚧','🔶','🔸','🔷','🔹','🔺','🔻','🔮','💀','👁️','🗡️','🛡️',
];
const mkIconPicker = (container, initial) => {
let sel = initial || ICONS[0];
const pick = mk('div', 'tvis-icon-pick');
const custom = getCustomIcons();
const all = custom.length ? [...custom, '|', ...ICONS] : ICONS;
all.forEach(ico => {
if (ico === '|') {
const sep = mk('div'); sep.style.cssText='width:100%;height:1px;background:#eee;margin:2px 0';
pick.appendChild(sep); return;
}
const b = mk('button', ico === sel ? 'sel' : '', ico); b.type = 'button';
b.onclick = () => { sel = ico; pick.querySelectorAll('button').forEach(x => x.className = ''); b.className = 'sel'; };
pick.appendChild(b);
});
container.appendChild(pick);
return () => sel;
};
// MODAL
const mkModal = (title, renderFn) => {
document.getElementById('tvis-modal')?.remove();
const ov = mk('div', 'tvis-ov'); ov.id = 'tvis-modal';
const box = mk('div', 'tvis-box');
box.appendChild(mk('div', 'tvis-title', title));
const cont = mk('div'); box.appendChild(cont);
const close = () => {
ov.remove();
window.removeEventListener('resize', onResize);
};
// Close if box goes off screen on resize
const onResize = () => {
const r = box.getBoundingClientRect();
if (r.left < 0 || r.right > innerWidth + 2) close();
};
window.addEventListener('resize', onResize);
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);
return cont;
};
// TOAST
const showToast = msg => {
document.getElementById('tvis-toast')?.remove();
const t = mk('div','tvis-toast',msg); t.id='tvis-toast';
document.body.appendChild(t);
requestAnimationFrame(()=>t.classList.add('show'));
setTimeout(()=>{ t.classList.remove('show'); setTimeout(()=>t.remove(),400); },3500);
};
// RENK SEÇİCİ
// Returns true if hex color is dark enough to warrant white text
const isColorDark = hex => {
const h = hex.replace('#','');
if (h.length < 6) return false;
const r=parseInt(h.slice(0,2),16), g=parseInt(h.slice(2,4),16), b=parseInt(h.slice(4,6),16);
return (0.299*r + 0.587*g + 0.114*b) < 155;
};
const PRESET_COLORS = [
// Temel mor / mavi ailesi
'#6f42c1','#9b59b6','#8e44ad','#5a32a3','#4a235a',
// Maviler
'#007bff','#2980b9','#17a2b8','#0d6efd','#1abc9c',
// Yeşiller
'#28a745','#27ae60','#20c997','#2ecc71','#16a085',
// Kırmızı / turuncu / sarı
'#dc3545','#c0392b','#e74c3c','#fd7e14','#f39c12',
'#ffc107','#ffd700','#f1c40f',
// Pembe / mor tonları
'#e83e8c','#e91e63','#c2185b',
// Koyu / nötr
'#343a40','#6c757d','#495057','#2c3e50','#34495e',
// Açık ve özel
'#3498db','#00b894','#e17055','#a29bfe','#74b9ff',
];
const mkColorPicker = (container, initial) => {
let sel = initial || '#6f42c1';
const wrap = mk('div','tvis-cpick');
const label = mk('div','tvis-sec',s('colorPicker')); container.appendChild(label);
PRESET_COLORS.forEach(c => {
const sw = mk('button','tvis-cpick-sw'+(c===sel?' sel':'')); sw.type='button';
sw.style.background=c; sw.title=c;
sw.onclick=()=>{ sel=c; wrap.querySelectorAll('.tvis-cpick-sw').forEach(x=>x.classList.remove('sel')); sw.classList.add('sel'); };
wrap.appendChild(sw);
});
const ci = mk('input'); ci.type='color'; ci.value=sel; ci.className='tvis-cpick-custom'; ci.title='Özel renk';
ci.oninput=()=>{ sel=ci.value; wrap.querySelectorAll('.tvis-cpick-sw').forEach(x=>x.classList.remove('sel')); };
wrap.appendChild(ci);
container.appendChild(wrap);
return () => sel;
};
// RAF SİSTEMİ
const RAF_FOLDER_DEFS = [
{ id:'chars', icon:'👤', nameKey:'rafChars', color:'#e83e8c', type:'character' },
{ id:'cities', icon:'🏙️', nameKey:'rafCities', color:'#fd7e14', type:'city' },
{ id:'locales', icon:'📍', nameKey:'rafLocales', color:'#28a745', type:'locale' },
{ id:'artists', icon:'🎸', nameKey:'rafArtists', color:'#6f42c1', type:'artist' },
{ id:'forum', icon:'📋', nameKey:'rafForum', color:'#17a2b8', type:'forum' },
{ id:'fav', icon:'⭐', nameKey:'rafFav', color:'#ffc107', type:'any' },
{ id:'work', icon:'💼', nameKey:'rafWork', color:'#343a40', type:'any' },
{ id:'goals', icon:'🎯', nameKey:'rafGoals', color:'#dc3545', type:'any' },
];
const SHELF_COLS = 5;
const getShelf = () => {
const saved = gmGet(DK.RAF, null);
// ── Existing v1 save: patch missing folders + fields ──────────────────────
if (saved?.v && saved?.folders) {
let dirty = false;
// Add missing folders
RAF_FOLDER_DEFS.forEach(f => {
if (!saved.folders[f.id]) {
saved.folders[f.id] = {
icon:f.icon, name:s(f.nameKey), color:f.color, colCount:5,
columns: Array.from({length:5},(_,i)=>({name:`${s('rafColDefault')} ${i+1}`,items:[]}))
};
dirty = true;
}
// Patch missing colCount
if (!saved.folders[f.id].colCount) { saved.folders[f.id].colCount=5; dirty=true; }
});
// Add folderOrder if missing
if (!saved.folderOrder) {
saved.folderOrder = RAF_FOLDER_DEFS.map(f=>f.id);
dirty = true;
}
// Pre-populate cities if empty
if (!saved.folders.cities.columns.some(c=>c.items.length)) {
saved.folders.cities.columns = buildCityCols();
dirty = true;
}
if (dirty) gmSet(DK.RAF, saved);
return saved;
}
// ── Fresh init ─────────────────────────────────────────────────────────────
const folders = {};
RAF_FOLDER_DEFS.forEach(f => {
const existing = saved?.[f.id];
const cols = f.id==='cities' ? buildCityCols() :
(existing?.columns || Array.from({length:5},(_,i)=>({name:`${s('rafColDefault')} ${i+1}`,items:[]})));
folders[f.id] = { icon:f.icon, name:s(f.nameKey), color:f.color, colCount:5, columns:cols };
});
// Migrate old PIN data
const oldPins = gmGet(DK.PINS,[]);
if (oldPins?.length && !saved?.v) {
oldPins.forEach(p=>{
const fid=p.type==='artist'?'artists':p.type==='locale'?'locales':p.type==='city'?'cities':'fav';
folders[fid]?.columns[0].items.push({id:'p'+Date.now()+Math.random(),label:p.label,url:p.url,icon:p.icon||'📌',note:p.note||'',color:null,type:p.type||'page',savedAt:Date.now()});
});
}
// Migrate old forum list data
const oldFl = gmGet('tvis_forum_list',null);
if (oldFl?.lanes) {
oldFl.lanes.forEach((lane,li)=>{
if(li>=5)return;
folders['forum'].columns[li].name=lane.name;
lane.threads?.forEach(t=>folders['forum'].columns[li].items.push({id:'f'+Date.now()+Math.random(),label:t.name,url:t.url,icon:'📋',note:'',color:null,type:'forum',savedAt:Date.now()}));
});
}
return {v:1, folderOrder:RAF_FOLDER_DEFS.map(f=>f.id), folders};
};
const saveShelf = d => gmSet(DK.RAF, d);
// Build pre-sorted city columns (alphabetical, 5 cols)
const buildCityCols = () => {
const PM_BASE = '/World/Popmundo.aspx/City/';
const cities = [
{n:'Amsterdam',id:8},{n:'Ankara',id:35},{n:'Antalya',id:61},
{n:'Bakü',id:58},{n:'Barselona',id:9},{n:'Belgrad',id:36},
{n:'Berlin',id:7},{n:'Brüksel',id:33},{n:'Budapeşte',id:42},
{n:'Buenos Aires',id:17},{n:'Bükreş',id:46},{n:'Cakarta',id:55},
{n:'Dubrovnik',id:29},{n:'Glasgow',id:27},{n:'Helsinki',id:19},
{n:'İstanbul',id:30},{n:'İzmir',id:47},{n:'Johannesburg',id:51},
{n:'Kopenhag',id:22},{n:'Kyiv',id:56},{n:'Londra',id:5},
{n:'Los Angeles',id:14},{n:'Madrid',id:24},{n:'Manila',id:54},
{n:'Melbourne',id:10},{n:'Mexico City',id:32},{n:'Milano',id:52},
{n:'Montreal',id:38},{n:'Moskova',id:18},{n:'Nashville',id:11},
{n:'New York',id:6},{n:'Paris',id:20},{n:'Porto',id:31},
{n:'Rio de Janeiro',id:25},{n:'Roma',id:23},{n:'Sao Paulo',id:21},
{n:'Saraybosna',id:49},{n:'Seattle',id:50},{n:'Singapur',id:39},
{n:'Sofya',id:53},{n:'Stokholm',id:1},{n:'Şangay',id:45},
{n:'Şikago',id:60},{n:'Tallinn',id:34},{n:'Tokyo',id:62},
{n:'Toronto',id:16},{n:'Tromsø',id:26},{n:'Varşova',id:48},
{n:'Vilnius',id:28},
];
// Already alphabetically sorted (TR locale). Distribute into 5 cols.
const colCount=5, perCol=Math.ceil(cities.length/colCount);
const colRanges=[{s:'A–B',e:9},{s:'B–C',e:19},{s:'L–N',e:29},{s:'N–S',e:39},{s:'S–V',e:49}];
return Array.from({length:colCount},(_,ci)=>{
const slice=cities.slice(ci*perCol,ci*perCol+perCol);
return {
name:colRanges[ci].s,
items:slice.map(c=>({
id:'city'+c.id, label:c.n, url:PM_BASE+c.id,
icon:'🏙️', note:'', color:null, type:'city', savedAt:0
}))
};
});
};
const detectPage = () => {
const url = location.href; let m;
if ((m = url.match(/\/Character\/(\d+)(?:[/?#]|$)/)))
return { type:'character', id:m[1], icon:'🧑', label: document.querySelector('.charPresBox h2,.charPresBox h3')?.textContent.trim() || `Karakter #${m[1]}` };
if ((m = url.match(/\/Locale\/(\d+)(?:[/?#]|$)/)))
return { type:'locale', id:m[1], icon:'📍', label: document.querySelector('h1,h2')?.textContent.trim() || `Mekan #${m[1]}` };
if ((m = url.match(/\/Artist\/(\d+)(?:[/?#]|$)/)))
return { type:'artist', id:m[1], icon:'🎸', label: document.querySelector('h1,h2')?.textContent.trim() || `Sanatçı #${m[1]}` };
if ((m = url.match(/\/City\/(\d+)(?:[/?#]|$)/)))
return { type:'city', id:m[1], icon:'🏙️', label: document.querySelector('h1,h2')?.textContent.trim() || `Şehir #${m[1]}` };
if (url.includes('/Forum/')||url.includes('/Thread/'))
return { type:'forum', id:'', icon:'📋', label:(document.title||'').replace(/^Popmundo\s*[-–]\s*/i,'').trim()||location.pathname };
return { type:'page', id:'', icon:'📌', label:(document.title||'').replace(/^\(\d+\)\s*/,'').replace(/^Popmundo\s*[-–]\s*/i,'').trim()||location.pathname };
};
const openMetaEdit = (anchorEl, current, onSave) => {
document.getElementById('tvis-meta-edit-pop')?.remove();
const pop=mk('div','tvis-tab-edit-pop'); pop.id='tvis-meta-edit-pop';
const rect=anchorEl.getBoundingClientRect();
pop.style.left=Math.min(rect.left,innerWidth-260)+'px'; pop.style.top=(rect.bottom+4)+'px'; pop.style.minWidth='240px';
const nameI=mk('input'); nameI.value=current.name||''; nameI.maxLength=20;
nameI.style.cssText='width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px;margin:4px 0';
pop.appendChild(mk('div','tvis-sec',s('folderTitle'))); pop.appendChild(nameI);
let selIcon=current.icon||'📌';
const iconPick=mk('div','tvis-icon-pick'); iconPick.style.maxHeight='80px';
const allIcos=[...getCustomIcons(),'|',...ICONS];
allIcos.forEach(ico=>{
if(ico==='|'){const sep=mk('div');sep.style.cssText='width:100%;height:1px;background:#eee;margin:2px 0';iconPick.appendChild(sep);return;}
const b=mk('button',ico===selIcon?'sel':'',ico);b.type='button';
b.onclick=()=>{selIcon=ico;iconPick.querySelectorAll('button').forEach(x=>x.className='');b.className='sel';};
iconPick.appendChild(b);
});
pop.appendChild(mk('div','tvis-sec',s('pinIcon'))); pop.appendChild(iconPick);
const getColor=mkColorPicker(pop,current.color||'#6f42c1');
pop.appendChild(mkB('✔ '+s('iaSave'),'btn-sm btn-g',()=>{pop.remove();onSave({name:nameI.value.trim()||current.name,icon:selIcon,color:getColor()});}));
const cb=mkB(s('cancelLbl'),'btn-sm btn-grey',()=>pop.remove()); cb.style.marginLeft='4px'; pop.lastChild.insertAdjacentElement('afterend',cb);
document.body.appendChild(pop);
setTimeout(()=>{const close=e=>{if(!pop.contains(e.target)){pop.remove();document.removeEventListener('mousedown',close);}};document.addEventListener('mousedown',close);},10);
};
const openRaf = () => mkModal(s('pinBtn'), cont => {
cont.style.minWidth='720px';
const shelf0=getShelf();
const order0=shelf0.folderOrder||RAF_FOLDER_DEFS.map(f=>f.id);
// Start on first folder that has items, else first folder
let activeFolder=(()=>{ for(const fid of order0){ if(shelf0.folders[fid]?.columns.some(c=>c.items.length)) return fid; } return order0[0]||RAF_FOLDER_DEFS[0].id; })();
let editItemKey=null;
let drag=null;
let tabDrag=null;
let rafSearchQ='';
// ── Shared card renderer ─────────────────────────────────────────────────
const renderRafCard=(item,itemKey,onEdit,fid,ci,ii,grid,getDrag,setDrag)=>{
const card=mk('div','tvis-raf-card');
if(item.color){
card.classList.add('colored');
card.style.background=item.color;
card.style.borderColor=item.color;
card.style.color=isColorDark(item.color)?'#fff':'#222';
}
const cbar=mk('div','tvis-raf-card-color');
if(item.color) cbar.style.background=isColorDark(item.color)?'rgba(255,255,255,.25)':'rgba(0,0,0,.15)';
const ico=mk('span','tvis-raf-card-ico',item.icon||'📌');
const lbl=mk('a','tvis-raf-card-lbl',item.label); lbl.href=item.url; lbl.target='_blank'; lbl.title=item.label+(item.note?'\n'+item.note:'');
card.append(mk('span','frl-drag','⠿'),cbar,ico,lbl);
if(item.note){ const noteEl=mk('span','tvis-raf-card-note',item.note); if(item.color) noteEl.style.color=isColorDark(item.color)?'rgba(255,255,255,.75)':'rgba(0,0,0,.55)'; card.appendChild(noteEl); }
if(itemKey!=null && onEdit){
const eB=mkB('✏','btn-sm btn-grey',()=>onEdit(itemKey));
const dB=mkB('✕','btn-sm btn-r',()=>{const sh=getShelf();sh.folders[fid].columns[ci].items.splice(ii,1);saveShelf(sh);render();});
eB.style.cssText=dB.style.cssText='font-size:9px;padding:1px 3px;flex-shrink:0';
card.append(eB,dB);
card.draggable=true; card.dataset.itemkey=`${fid}-${ci}`;
card.addEventListener('dragstart',()=>setDrag({type:'card',fid,ci,ii}));
card.addEventListener('dragover',e=>{
e.preventDefault();
if(getDrag()?.type==='card'){ grid?.querySelectorAll('.drag-over').forEach(x=>x.classList.remove('drag-over')); card.classList.add('drag-over'); }
});
card.addEventListener('drop',e=>{
e.preventDefault();e.stopPropagation();
const d=getDrag(); if(!d||d.type!=='card') return;
const sh=getShelf();
const [itm]=sh.folders[d.fid].columns[d.ci].items.splice(d.ii,1);
const to=e.clientY>card.getBoundingClientRect().top+card.offsetHeight/2?ii+1:ii;
sh.folders[fid].columns[ci].items.splice(to,0,itm);
setDrag(null);saveShelf(sh);render();
});
}
return card;
};
// ── Inline item editor ───────────────────────────────────────────────────
const buildItemEditor=(item,ci,ii)=>{
const ed=mk('div','tvis-raf-edit');
const r1=mk('div');r1.style.cssText='display:flex;gap:4px;flex-wrap:wrap;margin-bottom:4px';
const nI=mk('input');nI.value=item.label;nI.style.cssText='flex:1;min-width:100px;padding:3px 5px;border:1px solid #ccc;border-radius:3px;font-size:11px';
const ntI=mk('input');ntI.value=item.note||'';ntI.placeholder=s('rafItemNote');ntI.style.cssText='flex:1;min-width:80px;padding:3px 5px;border:1px solid #ccc;border-radius:3px;font-size:11px';
r1.append(nI,ntI); ed.appendChild(r1);
ed.appendChild(mk('div','tvis-sec',s('pinIcon')));
let edIcon=item.icon||'📌';
const edIp=mk('div','tvis-icon-pick');edIp.style.maxHeight='60px';
[...getCustomIcons(),'|',...ICONS].forEach(ico2=>{
if(ico2==='|'){const sep=mk('div');sep.style.cssText='width:100%;height:1px;background:#eee;margin:2px 0';edIp.appendChild(sep);return;}
const b=mk('button',ico2===edIcon?'sel':'',ico2);b.type='button';
b.onclick=()=>{edIcon=ico2;edIp.querySelectorAll('button').forEach(x=>x.className='');b.className='sel';};
edIp.appendChild(b);
});
ed.appendChild(edIp);
const getEdColor=mkColorPicker(ed,item.color||null);
const sv=mkB('✔ '+s('iaSave'),'btn-sm btn-g',()=>{
const sh=getShelf();const it=sh.folders[activeFolder].columns[ci].items[ii];
it.label=nI.value.trim()||it.label;it.note=ntI.value.trim();it.icon=edIcon;it.color=getEdColor()||null;
saveShelf(sh);editItemKey=null;render();
});
sv.style.marginTop='4px';ed.appendChild(sv);
return ed;
};
const render=()=>{
cont.innerHTML='';
const shelf=getShelf();
const order=shelf.folderOrder||RAF_FOLDER_DEFS.map(f=>f.id);
// ── SEARCH BAR ──────────────────────────────────────────────────────
const searchRow=mk('div'); searchRow.style.cssText='display:flex;gap:6px;align-items:center;margin-bottom:10px';
const searchI=mk('input'); searchI.id='tvis-raf-search';
searchI.placeholder='🔍 Raflarda ara... (isim, not)';
searchI.style.cssText='flex:1;padding:4px 8px;border:1px solid #c9b8f0;border-radius:4px;font-size:12px';
searchI.value=rafSearchQ;
const clrBtn=mkB('✕','btn-sm btn-grey',()=>{ rafSearchQ=''; render(); });
clrBtn.style.display=rafSearchQ?'':'none';
searchI.addEventListener('input',()=>{ rafSearchQ=searchI.value; clrBtn.style.display=rafSearchQ?'':'none'; renderContent(); });
searchRow.append(searchI,clrBtn);
cont.appendChild(searchRow);
// ── FOLDER TABS (draggable) ──────────────────────────────────────────
const tabRow=mk('div','tvis-raf-tabs');
if(rafSearchQ) tabRow.style.opacity='0.5';
order.forEach(fid=>{
const def=RAF_FOLDER_DEFS.find(d=>d.id===fid); if(!def) return;
const fd=shelf.folders[fid]; if(!fd) return;
const totalItems=fd.columns.flatMap(c=>c.items).length;
const isActive=activeFolder===fid&&!rafSearchQ;
const tw=mk('span'); tw.style.cssText='display:inline-flex;align-items:center;gap:1px';
tw.draggable=true; tw.dataset.fid=fid;
tw.addEventListener('dragstart',e=>{ tabDrag=fid; tw.style.opacity='.4'; e.dataTransfer.effectAllowed='move'; });
tw.addEventListener('dragend',()=>{ tw.style.opacity=''; tabRow.querySelectorAll('.drag-over-tab').forEach(x=>x.classList.remove('drag-over-tab')); });
tw.addEventListener('dragover',e=>{ if(tabDrag&&tabDrag!==fid){e.preventDefault();tw.querySelector('button')?.classList.add('drag-over-tab');} });
tw.addEventListener('dragleave',()=>tw.querySelector('button')?.classList.remove('drag-over-tab'));
tw.addEventListener('drop',e=>{
e.preventDefault();e.stopPropagation();
if(!tabDrag||tabDrag===fid) return;
const sh=getShelf(); const ord=sh.folderOrder||RAF_FOLDER_DEFS.map(f=>f.id);
const from=ord.indexOf(tabDrag),to=ord.indexOf(fid);
if(from>=0&&to>=0){ord.splice(from,1);ord.splice(to,0,tabDrag);sh.folderOrder=ord;saveShelf(sh);}
tabDrag=null;render();
});
const tab=mk('button','tvis-raf-tab'+(isActive?' active':''), `${fd.icon} ${fd.name}`);
tab.style.borderColor=fd.color;
if(isActive){tab.style.background=fd.color;tab.style.color='#fff';}
if(totalItems>0){ const badge=mk('span','tvis-raf-tab-badge',String(totalItems)); tab.appendChild(badge); }
// Trailing icon
const trailIco=mk('span',''); trailIco.textContent=' '+fd.icon; trailIco.style.cssText='font-size:11px;opacity:.7';
tab.appendChild(trailIco);
tab.onclick=()=>{rafSearchQ='';activeFolder=fid;editItemKey=null;render();};
const eb=mk('button','tvis-raf-tab-edit','✏️');
eb.onclick=e=>{e.stopPropagation();openMetaEdit(eb,fd,(meta)=>{const sh=getShelf();sh.folders[fid]={...sh.folders[fid],...meta};saveShelf(sh);render();});};
tw.append(tab,eb); tabRow.appendChild(tw);
});
// Son Eklenenler pseudo-tab
const recTab=mk('button','tvis-raf-recent-tab'+(activeFolder==='__recent__'&&!rafSearchQ?' active':''),s('recentTab'));
recTab.onclick=()=>{rafSearchQ='';activeFolder='__recent__';editItemKey=null;render();};
tabRow.appendChild(recTab);
cont.appendChild(tabRow);
const contentArea=mk('div'); cont.appendChild(contentArea);
const renderContent=()=>{
contentArea.innerHTML='';
const shelf2=getShelf();
const order2=shelf2.folderOrder||RAF_FOLDER_DEFS.map(f=>f.id);
if(rafSearchQ.trim()){
// ── SEARCH ────────────────────────────────────────────────────
const q=rafSearchQ.trim().toLowerCase();
const results=[];
order2.forEach(fid=>{ const fd2=shelf2.folders[fid]; if(!fd2) return;
fd2.columns.forEach((col,ci)=>{ col.items.forEach((item,ii)=>{
if((item.label||'').toLowerCase().includes(q)||(item.note||'').toLowerCase().includes(q))
results.push({item,fid,ficon:fd2.icon,fname:fd2.name,ci,ii,colname:col.name});
});});
});
if(!results.length){
const em=mk('div',''); em.style.cssText='color:#999;font-size:12px;padding:16px 0;text-align:center';
em.textContent=`"${rafSearchQ}" için sonuç bulunamadı.`; contentArea.appendChild(em);
} else {
const info=mk('div',''); info.style.cssText='font-size:10px;color:#aaa;margin-bottom:8px';
info.textContent=`${results.length} sonuç`; contentArea.appendChild(info);
const list=mk('div'); list.style.cssText='display:flex;flex-direction:column;gap:3px';
results.forEach(({item,fid,ficon,fname,ci,colname})=>{
const row=renderRafCard(item,null,null,fid,ci,0,null,()=>null,()=>{});
row.style.cursor='default';
const bc=mk('span',''); bc.style.cssText='font-size:9px;white-space:nowrap;flex-shrink:0';
bc.style.color=item.color?(isColorDark(item.color)?'rgba(255,255,255,.6)':'rgba(0,0,0,.45)'):'#aaa';
bc.textContent=`${ficon} ${fname} › ${colname}`;
const goBtn=mkB('→','btn-sm btn-grey',()=>{rafSearchQ='';activeFolder=fid;render();});
goBtn.title='Bu klasöre git';goBtn.style.flexShrink='0';
row.append(bc,goBtn); list.appendChild(row);
});
contentArea.appendChild(list);
}
setTimeout(()=>{ const si=document.getElementById('tvis-raf-search'); if(si){si.focus();si.setSelectionRange(si.value.length,si.value.length);} },10);
} else if(activeFolder==='__recent__'){
// ── SON EKLENENLER ────────────────────────────────────────────
const allItems=[];
order2.forEach(fid=>{ const fd2=shelf2.folders[fid]; if(!fd2) return;
fd2.columns.forEach((col,ci)=>{ col.items.forEach((item,ii)=>{
const ts=item.savedAt||parseInt(String(item.id).replace(/\D/g,'').slice(0,13))||0;
if(ts>0) allItems.push({item,fid,ficon:fd2.icon,fname:fd2.name,ci,ii,colname:col.name,ts});
});});
});
allItems.sort((a,b)=>b.ts-a.ts);
const recent=allItems.slice(0,20);
if(!recent.length){
const em=mk('div',''); em.style.cssText='color:#999;font-size:12px;padding:16px 0;text-align:center';
em.textContent=s('recentEmpty'); contentArea.appendChild(em);
} else {
const info=mk('div',''); info.style.cssText='font-size:10px;color:#aaa;margin-bottom:8px';
info.textContent=s('recentCount').replace('{n}',recent.length); contentArea.appendChild(info);
const list=mk('div'); list.style.cssText='display:flex;flex-direction:column;gap:3px';
recent.forEach(({item,fid,ficon,fname,ci,colname,ts})=>{
const row=renderRafCard(item,null,null,fid,ci,0,null,()=>null,()=>{});
row.style.cursor='default';
const dateStr=ts>0?new Date(ts).toLocaleDateString('tr-TR',{day:'2-digit',month:'2-digit',year:'2-digit'}):'';
const meta=mk('span',''); meta.style.cssText='font-size:9px;color:#aaa;white-space:nowrap;flex-shrink:0';
if(item.color) meta.style.color=isColorDark(item.color)?'rgba(255,255,255,.6)':'rgba(0,0,0,.45)';
meta.textContent=`${ficon} ${fname} › ${colname}${dateStr?' · '+dateStr:''}`;
const goBtn=mkB('→','btn-sm btn-grey',()=>{activeFolder=fid;render();});
goBtn.title=s('goToFolder');goBtn.style.flexShrink='0';
row.append(meta,goBtn); list.appendChild(row);
});
contentArea.appendChild(list);
}
} else {
// ── NORMAL GRID ───────────────────────────────────────────────
const fd=shelf2.folders[activeFolder]; if(!fd) return;
const colCount=fd.colCount||5;
// Column count selector
const ccRow=mk('div','tvis-raf-colcount');
ccRow.appendChild(mk('span','',s('colLabel')));
[3,4,5,6].forEach(n=>{
const b=mkB(String(n),'',()=>{
if(n===colCount) return;
const sh=getShelf(); const fdr=sh.folders[activeFolder];
if(n>colCount){ for(let i=colCount;i<n;i++) fdr.columns.push({name:`${s('rafColDefault')} ${i+1}`,items:[]}); }
else { const overflow=fdr.columns.splice(n).flatMap(c=>c.items); fdr.columns[n-1].items.push(...overflow); }
fdr.colCount=n; saveShelf(sh); render();
});
b.style.cssText=n===colCount
? 'padding:1px 7px;border:1px solid #6f42c1;border-radius:3px;background:#6f42c1;font-size:10px;cursor:pointer;color:#fff'
: 'padding:1px 7px;border:1px solid #ccc;border-radius:3px;background:#fff;font-size:10px;cursor:pointer;color:#666';
ccRow.appendChild(b);
});
contentArea.appendChild(ccRow);
const grid=mk('div','tvis-raf-grid');
grid.style.gridTemplateColumns=`repeat(${colCount},1fr)`;
contentArea.appendChild(grid);
fd.columns.forEach((col,ci)=>{
const colEl=mk('div','tvis-raf-col');
const head=mk('div','tvis-raf-col-head');
const nameI=mk('input','tvis-raf-col-name'); nameI.value=col.name; nameI.maxLength=20;
nameI.onchange=()=>{const sh=getShelf();sh.folders[activeFolder].columns[ci].name=nameI.value.trim()||col.name;saveShelf(sh);};
const cnt=mk('span',''); cnt.style.cssText='font-size:9px;color:#aaa;flex-shrink:0;padding:0 2px';
cnt.textContent=col.items.length?`(${col.items.length})`:'';
head.append(nameI,cnt); colEl.appendChild(head);
col.items.forEach((item,ii)=>{
const itemKey=`${ci}-${ii}`;
const card=renderRafCard(item,itemKey,(key)=>{editItemKey=editItemKey===key?null:key;render();},
activeFolder,ci,ii,grid,()=>drag,(d)=>{drag=d;});
colEl.appendChild(card);
if(editItemKey===itemKey) colEl.appendChild(buildItemEditor(item,ci,ii));
});
colEl.addEventListener('dragover',e=>e.preventDefault());
colEl.addEventListener('dragenter',()=>colEl.classList.add('drag-target'));
colEl.addEventListener('dragleave',e=>{if(!colEl.contains(e.relatedTarget))colEl.classList.remove('drag-target');});
colEl.addEventListener('drop',e=>{
e.preventDefault();colEl.classList.remove('drag-target');
if(!drag||drag.type!=='card') return;
const sh=getShelf();
const [itm]=sh.folders[drag.fid].columns[drag.ci].items.splice(drag.ii,1);
sh.folders[activeFolder].columns[ci].items.push(itm);
drag=null;saveShelf(sh);render();
});
if(!col.items.length){const em=mk('div','',s('rafEmpty'));em.style.cssText='color:#bbb;font-size:10px;text-align:center;padding:8px 0';colEl.appendChild(em);}
grid.appendChild(colEl);
});
}
};
renderContent();
};
render();
})
// "Serbest" klasörler — sayfa tipine göre otomatik yönlendirilemeyen klasörler
const FREE_FOLDER_IDS = ['fav','work','goals'];
const quickRaf = () => {
const pg = detectPage();
// Bilinen tipe sahip sayfalar → direkt klasör
const autoFid = pg.type==='artist'?'artists' : pg.type==='locale'?'locales' : pg.type==='city'?'cities'
: pg.type==='forum'?'forum' : pg.type==='character'?'chars' : null;
const shelf = getShelf();
// Zaten eklenmiş mi kontrol et (tüm klasörlerde)
const allItems = Object.values(shelf.folders).flatMap(fd=>fd.columns.flatMap(c=>c.items));
if(allItems.find(it=>normUrl(it.url)===normUrl(location.href))){ showToast(s('rafDup')); return; }
const openForm = (targetFid) => {
const fd = shelf.folders[targetFid];
mkModal(s('pinAdd'),(cont,close)=>{
const fld=(lbl,val,plh)=>{
const l=mk('div','',lbl); l.style.cssText='font-size:11px;color:#666;margin-bottom:2px';
const i=mk('input'); i.style.cssText='width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #ccc;border-radius:3px;font-size:12px;margin-bottom:6px';
if(val!==undefined) i.value=val; if(plh) i.placeholder=plh;
cont.append(l,i); return i;
};
const nI=fld(s('pinName'),pg.label);
const ntI=fld(s('pinNote'),'','...');
cont.appendChild(mk('div','tvis-sec',s('pinIcon')));
let selIcon=pg.icon;
const ip=mk('div','tvis-icon-pick'); ip.style.maxHeight='80px';
[...getCustomIcons(),'|',...ICONS].forEach(ico=>{
if(ico==='|'){const sep=mk('div');sep.style.cssText='width:100%;height:1px;background:#eee;margin:2px 0';ip.appendChild(sep);return;}
const b=mk('button',ico===selIcon?'sel':'',ico); b.type='button';
b.onclick=()=>{selIcon=ico;ip.querySelectorAll('button').forEach(x=>x.className='');b.className='sel';};
ip.appendChild(b);
});
cont.appendChild(ip);
const getColor=mkColorPicker(cont,null);
cont.appendChild(mk('div','tvis-sec',s('rafColDefault')));
const colSel=mk('select'); colSel.style.cssText='width:100%;padding:3px 5px;border:1px solid #ccc;border-radius:3px;font-size:11px;margin-bottom:6px';
fd.columns.forEach((c,i)=>{const o=mk('option','',`${c.name} (${c.items.length})`);o.value=i;colSel.appendChild(o);});
cont.appendChild(colSel);
cont.appendChild(mkB(s('pinSave'),'btn-v',()=>{
const sh=getShelf();
sh.folders[targetFid].columns[parseInt(colSel.value)].items.push({
id:'i'+Date.now(), label:nI.value.trim()||pg.label, url:normUrl(location.href),
icon:selIcon, note:ntI.value.trim(), color:getColor()||null, type:pg.type, savedAt:Date.now()
});
saveShelf(sh); close(); showToast('📌 '+s('pinSave'));
}));
});
};
if(autoFid){
// Bilinen tip → direkt forma git
openForm(autoFid);
} else {
// Serbest tip → Hangi klasöre? seçici
const freeFolders = FREE_FOLDER_IDS.map(fid=>({ fid, fd:shelf.folders[fid] })).filter(x=>x.fd);
const order = shelf.folderOrder || RAF_FOLDER_DEFS.map(f=>f.id);
freeFolders.sort((a,b)=>order.indexOf(a.fid)-order.indexOf(b.fid));
// Mini popup — add button'un yakınında göster
const addBtn = document.querySelector('#tvis-bar .tvis-bar a') || document.body;
const pop = mk('div');
pop.style.cssText='position:fixed;z-index:999999;background:#fff;border:1px solid #c9b8f0;border-radius:8px;padding:8px;box-shadow:0 4px 16px rgba(0,0,0,.2);min-width:180px';
pop.style.top='36px'; pop.style.right='4px';
const title=mk('div','tvis-sec',s('addWhere')); title.style.marginBottom='6px';
pop.appendChild(title);
freeFolders.forEach(({fid,fd})=>{
const btn=mk('button'); btn.type='button';
btn.style.cssText='display:flex;align-items:center;gap:8px;width:100%;padding:6px 10px;margin-bottom:4px;border:2px solid;border-radius:5px;cursor:pointer;font-size:12px;font-weight:600;background:#fff;text-align:left';
btn.style.borderColor=fd.color;
btn.style.color=fd.color;
const totalItems=fd.columns.flatMap(c=>c.items).length;
btn.innerHTML=`<span style="font-size:16px">${fd.icon}</span><span>${fd.name}</span><span style="margin-left:auto;font-size:10px;opacity:.6">${totalItems}</span>`;
btn.onmouseover=()=>{ btn.style.background=fd.color; btn.style.color=isColorDark(fd.color)?'#fff':'#222'; };
btn.onmouseout=()=>{ btn.style.background='#fff'; btn.style.color=fd.color; };
btn.onclick=()=>{ pop.remove(); openForm(fid); };
pop.appendChild(btn);
});
document.body.appendChild(pop);
setTimeout(()=>{
const close=e=>{ if(!pop.contains(e.target)){pop.remove();document.removeEventListener('mousedown',close);} };
document.addEventListener('mousedown',close);
},50);
}
};
const addForumToRaf=(id,name,url,targetCol=-1)=>{
const sh=getShelf(); const fd=sh.folders['forum'];
const nUrl=normUrl(url);
if(fd.columns.flatMap(c=>c.items).find(it=>normUrl(it.url)===nUrl))return false;
let tc = targetCol >= 0 && targetCol < fd.columns.length ? targetCol
: fd.columns.findIndex(c=>c.items.length<20);
if(tc<0)tc=SHELF_COLS-1;
fd.columns[tc].items.push({id:'f'+Date.now(),label:name,url:nUrl,icon:'📋',note:'',color:null,type:'forum',savedAt:Date.now()});
saveShelf(sh); return true;
};
const isForumInRaf=url=>getShelf().folders['forum'].columns.flatMap(c=>c.items).some(it=>normUrl(it.url)===normUrl(url));
// CLIPBOARD HISTORY
const addClip = (id, name, type) => {
const log = gmGet(DK.CLIP, []);
log.unshift({ id, name, type, ts: Date.now() });
if (log.length > 25) log.length = 25;
gmSet(DK.CLIP, log);
};
const openClip = () => mkModal(s('chTitle'), cont => {
const render = () => {
cont.innerHTML = '';
const log = gmGet(DK.CLIP, []);
if (!log.length) { const d=mk('div','',s('chEmpty')); d.style.cssText='color:#999;font-size:12px'; cont.appendChild(d); return; }
const clr = mkB(s('chClear'), 'btn-sm btn-r', () => { gmSet(DK.CLIP,[]); render(); });
clr.style.marginBottom = '8px'; cont.appendChild(clr);
log.forEach(e => {
const row = mk('div'); row.style.cssText='display:flex;align-items:center;gap:6px;margin-bottom:4px;padding:4px 6px;background:#f8f9fa;border-radius:3px';
const ico = e.type==='character'?'🧑':e.type==='locale'?'📍':'🎸';
const lbl = mk('span','',`${ico} ${e.name||'?'} — ID: ${e.id}`); lbl.style.cssText='flex:1;font-size:12px';
const ts = mk('span','',new Date(e.ts).toLocaleTimeString(dateLocale,{hour:'2-digit',minute:'2-digit'})); ts.style.cssText='font-size:10px;color:#999';
const cp = mkB(s('chCopy'), 'btn-sm btn-b', () => { navigator.clipboard?.writeText(e.id); });
row.append(lbl,ts,cp); cont.appendChild(row);
});
};
render();
});
// CHARACTER CACHE
const CACHE_TTL = 86400000, CACHE_MAX = 200;
const CC = {
_s: () => gmGet(DK.CACHE, {}),
get(id) {
const s = this._s(), e = s[String(id)]; if (!e) return null;
if (Date.now() - e.t > CACHE_TTL) { delete s[id]; gmSet(DK.CACHE,s); return null; }
return e;
},
set(id, data) {
const s = this._s();
const tracked = new Set(getAllTracked().map(t => String(t.charId)));
const keys = Object.keys(s);
if (keys.length >= CACHE_MAX) {
const evict = keys.filter(k=>!tracked.has(k)).sort((a,b)=>s[a].t-s[b].t)[0] || keys.sort((a,b)=>s[a].t-s[b].t)[0];
if (evict) delete s[evict];
}
s[String(id)] = { ...data, t: Date.now() };
gmSet(DK.CACHE,s);
},
del(id) { const s=this._s(); delete s[String(id)]; gmSet(DK.CACHE,s); }
};
let lastFetch = 0;
const FETCH_GAP = 3000;
const waitGap = () => { const g = FETCH_GAP-(Date.now()-lastFetch); return g>0?new Promise(r=>setTimeout(r,g)):Promise.resolve(); };
// BULK UPDATE STATE
let bulkRunning = false;
let bulkCancelled = false;
const showBulkProgress = (current, total) => {
let el = document.getElementById('tvis-rprog');
if (!el) {
el = mk('div','tvis-rprog'); el.id='tvis-rprog';
const txt = mk('span','tvis-rprog-txt'); txt.id='tvis-rprog-txt';
const stop = mk('button','tvis-rprog-cancel', s('tkBgCancel'));
stop.onclick = () => { bulkCancelled = true; stop.disabled = true; stop.textContent = '...'; };
el.append(txt, stop);
document.body.appendChild(el);
}
document.getElementById('tvis-rprog-txt').textContent = `${s('tkBgRunning')} ${current}/${total}`;
};
const hideBulkProgress = (stopped, current, total) => {
const el = document.getElementById('tvis-rprog');
if (!el) return;
el.classList.remove('done','stopped');
el.classList.add(stopped ? 'stopped' : 'done');
const msg = stopped ? `${s('tkBgStopped')} ${current}/${total}` : `${s('tkBgDone')} ${total}/${total}`;
document.getElementById('tvis-rprog-txt').textContent = msg;
const stopBtn = el.querySelector('.tvis-rprog-cancel');
if (stopBtn) stopBtn.remove();
setTimeout(() => el.remove(), 3500);
};
// RADIO FETCH STATE
let radioRunning = false;
let radioCancelled = false;
const showRadioProgress = (current, total) => {
let el = document.getElementById('tvis-rdprog');
if (!el) {
el = mk('div','tvis-rprog'); el.id='tvis-rdprog';
el.style.bottom = '60px'; // offset above radar progress if both active
const txt = mk('span','tvis-rprog-txt'); txt.id='tvis-rdprog-txt';
const stop = mk('button','tvis-rprog-cancel', s('tkBgCancel'));
stop.onclick = () => { radioCancelled = true; stop.disabled = true; stop.textContent = '...'; };
el.append(txt, stop);
document.body.appendChild(el);
}
document.getElementById('tvis-rdprog-txt').textContent =
`📻 ${s('shFetching').replace('{n}', current).replace('/17', `/${total}`)}`;
};
const hideRadioProgress = (stopped, current, total, onDone) => {
const el = document.getElementById('tvis-rdprog');
if (!el) return;
el.classList.remove('done','stopped');
el.classList.add(stopped ? 'stopped' : 'done');
const doneMsg = { TR:'📻 Radyo listesi güncellendi!', EN:'📻 Radio list updated!', PT:'📻 Lista de rádio atualizada!' }[LANG];
const stopMsg = { TR:`📻 Durduruldu ${current}/${total}`, EN:`📻 Stopped ${current}/${total}`, PT:`📻 Parado ${current}/${total}` }[LANG];
document.getElementById('tvis-rdprog-txt').textContent = stopped ? stopMsg : doneMsg;
const stopBtn = el.querySelector('.tvis-rprog-cancel');
if (stopBtn) stopBtn.remove();
setTimeout(() => { el.remove(); if (!stopped) onDone?.(); }, 3500);
};
// PARSE CHAR PAGE
const parseCharPage = html => {
const doc = new DOMParser().parseFromString(html,'text/html');
const name = doc.querySelector('.charPresBox h2')?.textContent.trim(); if (!name) return null;
const pres = doc.querySelector('.characterPresentation');
const bandLink = pres?.querySelector('a[href*="/Artist/"]');
const cityLink = pres?.querySelector('a[href*="/City/"]');
const localeLink = [...(pres?.querySelectorAll('a[href*="/Locale/"]')||[])].find(a=>!a.href.includes('CharactersPresent')&&!a.href.includes('MoveToLocale'));
const onlineTxt = doc.getElementById('ctl00_cphLeftColumn_ctl00_lnkOnlineStatus')?.textContent.trim()
|| doc.getElementById('ctl00_cphLeftColumn_ctl00_trOnlineStatus')?.querySelector('td')?.textContent.replace(/Durum[u]?\s*:/,'').trim() || '';
const attitude = doc.getElementById('ctl00_cphLeftColumn_ctl00_lnkAttitude')?.textContent.trim() || '';
const stateImg = doc.getElementById('ctl00_cphLeftColumn_ctl00_imgState');
const state = stateImg ? [...stateImg.closest('tr').querySelectorAll('td')].pop()?.textContent.trim()||'' : '';
const avatarDiv = doc.querySelector('.avatar.idTrigger');
const avatarUrl = avatarDiv?.style.backgroundImage?.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1] || '';
const cashRow = doc.getElementById('ctl00_cphLeftColumn_ctl00_imgCash');
const cash = cashRow ? [...cashRow.closest('tr').querySelectorAll('td')].pop()?.textContent.trim()||'' : '';
return {
name, band: bandLink?.textContent.trim()||'', bandHref: bandLink?.getAttribute('href')||'',
city: cityLink?.textContent.trim()||'', cityHref: cityLink?.getAttribute('href')||'',
locale: localeLink?.textContent.trim()||'', localeHref: localeLink?.getAttribute('href')||'',
localeId: localeLink?.href.match(/\/Locale\/(\d+)/)?.[1]||'',
online: onlineTxt, attitude, state, avatarUrl, cash
};
};
// ONLINE DOT — extracts just the date part to avoid prefix duplication
const onlineDot = (online, small) => {
const dateMatch = (online||'').match(/(\d{1,2}\.\d{2}\.\d{4})/);
const isDate = !!dateMatch;
const dateStr = dateMatch?.[1] || '';
const off = !online || online === s('cpOffline') || online === 'Offline' || online === 'Çevrimdışı' || online === 'Desconectado' || isDate;
const sz = small ? '6px' : '7px';
const dot = `<span style="background:${off?'#aaa':'#28a745'};width:${sz};height:${sz};border-radius:50%;display:inline-block;margin-right:3px;vertical-align:middle;flex-shrink:0"></span>`;
if (off) return dot + (isDate ? `${s('cpOffline')} · ${s('tkLastSeen')} ${dateStr}` : s('cpOffline'));
return dot + online;
};
// SC CHARS (Speed Calling listesi)
const getSCChars = () => gmGet(DK.SC_CHARS, []) || [];
const setSCChars = a => gmSet(DK.SC_CHARS, a);
const addSCChar = (charId, name, type) => { const a=getSCChars(); if(a.some(c=>String(c.charId)===String(charId)))return false; a.push({charId:String(charId),name,type}); setSCChars(a); return true; };
const removeSCChar = charId => setSCChars(getSCChars().filter(c=>String(c.charId)!==String(charId)));
// CHARACTER HOVER POPUP (Karakter Kartı)
let popEl, hideT, fetchT, fetchSeq = 0;
const getPopEl = () => {
if (!popEl) {
popEl = mk('div','tvis-chpop');
popEl.addEventListener('mouseenter', () => clearTimeout(hideT));
popEl.addEventListener('mouseleave', () => { hideT = setTimeout(()=>{ popEl.style.display='none'; },1000); });
document.body.appendChild(popEl);
}
return popEl;
};
const posPopup = (x,y) => {
const el=getPopEl(), w=388, h=180;
let l=x+14, t=y-10;
if (l+w>innerWidth-8) l=x-w-14;
if (t+h>innerHeight-8) t=innerHeight-h-8;
el.style.left=Math.max(4,l)+'px'; el.style.top=Math.max(4,t)+'px';
};
const renderPopup = (id, data) => {
const el = getPopEl();
const timeStr = new Date(data.t||Date.now()).toLocaleTimeString(dateLocale,{hour:'2-digit',minute:'2-digit'});
const radarOn = isOnDef(K.tracking, true);
const tracked = radarOn && isTrackedV2(id);
const imgHtml = data.avatarUrl
? `<img src="${data.avatarUrl}" onerror="this.parentElement.innerHTML='🧑'" alt="">`
: '🧑';
el.innerHTML = `
<div class="tvis-pop-img">${imgHtml}</div>
<div class="tvis-pop-right">
<div class="tvis-pop-head">
<div class="tvis-pop-nameline">
${radarOn ? `<button class="tvis-trackbtn${tracked?' tracked':''}">${tracked?s('cpTracked'):s('cpTrack')}</button>` : ''}
<span class="tvis-pop-name">${data.name}</span>
<button class="tvis-pop-copyid">📋 ID</button>
</div>
<div style="font-size:10px;margin-top:2px">${onlineDot(data.online)}</div>
</div>
<div class="tvis-pop-body">
${data.band?`<div style="font-size:10px"><a href="${data.bandHref}" target="_blank" style="color:#6f42c1;text-decoration:none">🎸 ${data.band}</a></div>`:''}
${(data.city||data.locale)?`<div style="font-size:10px">${data.city?`<a href="${data.cityHref}" target="_blank" style="color:#17a2b8;text-decoration:none">🏙️ ${data.city}</a>`:''}${data.locale?` <a href="${data.localeHref}" target="_blank" style="color:#17a2b8;text-decoration:none">📍 ${data.locale}</a>`:''}</div>`:''}
${data.attitude?`<div style="font-size:10px;color:#555"><span style="color:#aaa;min-width:38px;display:inline-block">${s('cpAttitude')}</span>${data.attitude}</div>`:''}
${data.state?`<div style="font-size:10px;color:#555"><span style="color:#aaa;min-width:38px;display:inline-block">${s('cpState')}</span>${data.state}</div>`:''}
${data.cash?`<div style="font-size:10px;color:#555"><span style="color:#aaa;min-width:38px;display:inline-block">${s('cash')}</span><span style="color:#218838;font-weight:600">${data.cash}</span></div>`:''}
</div>
<div class="tvis-pop-links">
<a href="${PM}/Interact/Phone/${id}" class="tvis-pop-link" target="_blank" title="Telefon">📞</a>
<a href="${PM}/Conversations/Conversation/${id}" class="tvis-pop-link" target="_blank" title="Mesaj">✉️</a>
<a href="${PM}/Interact/${id}" class="tvis-pop-link" target="_blank" title="İlgilen">👋</a>
<a href="${PM}/Character/OfferItem/${id}" class="tvis-pop-link" target="_blank" title="Eşya Ver">🎁</a>
<a href="${PM}/Character/GiveMoney/${id}" class="tvis-pop-link" target="_blank" title="Para Ver">💰</a>
<a href="${PM}/Character/Blog/${id}" class="tvis-pop-link" target="_blank" title="Blog">📖</a>
<div style="margin-left:auto;display:flex;align-items:center;gap:4px;flex-shrink:0">
<button class="tvis-updatebtn" title="${s('cpRefresh')}">🔄 ${s('cpRefresh').replace('🔄 ','')}</button>
<span style="font-size:10px;color:#bbb">${timeStr}</span>
</div>
</div>
</div>`;
if (radarOn) {
el.querySelector('.tvis-trackbtn')?.addEventListener('click', () => {
const trackBtn = el.querySelector('.tvis-trackbtn');
const alreadyTracked = isTrackedV2(id);
if (alreadyTracked) {
// Show mini menu: Remove or Move
document.getElementById('tvis-track-menu')?.remove();
const menu = mk('div','tvis-tab-edit-pop'); menu.id='tvis-track-menu';
const rect = trackBtn.getBoundingClientRect();
menu.style.left = Math.min(rect.left, innerWidth-200)+'px';
menu.style.top = (rect.bottom+4)+'px';
const rmBtn = mkB(s('tkRemove'),'btn-sm btn-r',()=>{
removeFromRadar(id);
menu.remove(); renderPopup(id,data);
});
const mvBtn = mkB(s('tkMove'),'btn-sm btn-v',()=>{
menu.remove();
showPagePicker(trackBtn, pageIdx => {
moveInRadar(id, pageIdx);
renderPopup(id,data);
}, true);
});
rmBtn.style.cssText='width:100%;margin-bottom:3px;display:block;text-align:left';
mvBtn.style.cssText='width:100%;display:block;text-align:left';
menu.append(rmBtn,mvBtn);
document.body.appendChild(menu);
setTimeout(()=>{
const close=e=>{if(!menu.contains(e.target)){menu.remove();document.removeEventListener('click',close);}};
document.addEventListener('click',close);
},50);
} else {
showPagePicker(trackBtn, pageIdx => {
const newEntry = { charId:String(id), charName:data.name, band:data.band, city:data.city, cityHref:data.cityHref, locale:data.locale, localeHref:data.localeHref, localeId:data.localeId, online:data.online, attitude:data.attitude, state:data.state, cash:data.cash||'', avatarUrl:data.avatarUrl||'', savedAt:Date.now() };
addToRadar(pageIdx, newEntry);
renderPopup(id,data);
});
}
});
}
el.querySelector('.tvis-pop-copyid').onclick = () => {
navigator.clipboard?.writeText(id);
addClip(id,data.name,'character');
};
el.querySelector('.tvis-updatebtn').onclick = ev => {
ev.stopPropagation(); CC.del(id);
const rect=el.getBoundingClientRect(); loadAndShow(id,rect.left+10,rect.top+10);
};
// Speed Calling — Friend / Romantic toggle (no confirm)
const linksDiv = el.querySelector('.tvis-pop-links');
if (linksDiv) {
const scRow = mk('div');
scRow.style.cssText = 'display:flex;align-items:center;gap:4px;margin-top:4px;flex-wrap:nowrap;';
scRow.appendChild(Object.assign(mk('span','','Speed Calling'), {style:'font-size:9px;color:#888;white-space:nowrap;flex-shrink:0;'}));
let fBtn, rBtn;
const refreshFR = () => {
const cur = getSCChars().find(c=>String(c.charId)===String(id));
fBtn.textContent = cur?.type==='arkadaş' ? '👥 ✓' : '👥';
rBtn.textContent = cur?.type==='romantik' ? '💕 ✓' : '💕';
fBtn.className = 'tvis-trackbtn' + (cur?.type==='arkadaş'?' tracked':'');
rBtn.className = 'tvis-trackbtn' + (cur?.type==='romantik'?' tracked':'');
};
fBtn = mk('button'); rBtn = mk('button');
fBtn.title = 'Arkadaş'; rBtn.title = 'Romantik';
fBtn.onclick = () => {
const cur = getSCChars().find(c=>String(c.charId)===String(id));
if (cur?.type==='arkadaş') removeSCChar(id);
else { if (cur) removeSCChar(id); addSCChar(id, data.name||id, 'arkadaş'); }
refreshFR();
};
rBtn.onclick = () => {
const cur = getSCChars().find(c=>String(c.charId)===String(id));
if (cur?.type==='romantik') removeSCChar(id);
else { if (cur) removeSCChar(id); addSCChar(id, data.name||id, 'romantik'); }
refreshFR();
};
refreshFR();
scRow.append(fBtn, rBtn);
linksDiv.appendChild(scRow);
}
el.style.display='flex';
};
// PAGE PICKER — choose radar page when adding a character
const showPagePicker = (anchorEl, onSelect, moveMode=false) => {
document.getElementById('tvis-page-picker')?.remove();
const tabs = getRadarTabs();
const pop = mk('div','tvis-page-picker'); pop.id='tvis-page-picker';
const rect = anchorEl ? anchorEl.getBoundingClientRect() : { left: innerWidth/2-100, bottom: innerHeight/2 };
pop.style.left = Math.min(Math.max(10, rect.left), innerWidth-210)+'px';
pop.style.top = (rect.bottom+6)+'px';
pop.appendChild(mk('div','tvis-sec', moveMode ? s('tkPageMove') : s('tkPageSelect')));
tabs.forEach((tab, i) => {
const pageArr = getRadarPage(i);
const count = pageArr.length;
const full = count >= RADAR_PAGE_SIZE;
const label = `${tab.icon} ${tab.name}`;
const btn = mkB(`${label} (${count}/${RADAR_PAGE_SIZE})`, 'btn-sm '+(full?'btn-grey':'btn-v'), () => {
if (full) { showToast(s('tkPageFull')); return; }
pop.remove(); onSelect(i);
});
btn.style.cssText='width:100%;margin-bottom:3px;text-align:left;display:block';
pop.appendChild(btn);
});
document.body.appendChild(pop);
setTimeout(() => {
const close = e => { if (!pop.contains(e.target)) { pop.remove(); document.removeEventListener('click', close); } };
document.addEventListener('click', close);
}, 50);
};
const loadAndShow = async (id,x,y) => {
const seq = ++fetchSeq;
const cached = CC.get(id);
if (cached) { if (fetchSeq===seq) { renderPopup(id,cached); posPopup(x,y); } return; }
const el = getPopEl();
el.innerHTML = `<div style="color:#999;font-size:11px;padding:12px">${s('cpLoading')}</div>`;
el.style.display='flex'; posPopup(x,y);
await waitGap(); if (fetchSeq!==seq) return;
lastFetch = Date.now();
const html = await fetch(`${PM}/Character/${id}`).then(r=>r.ok?r.text():null).catch(()=>null);
if (fetchSeq!==seq) return;
if (!html) { el.style.display='none'; return; }
const data = parseCharPage(html);
if (!data) { el.style.display='none'; return; }
CC.set(id,data); renderPopup(id,data); posPopup(x,y);
};
const applyCharPopup = () => {
if (!isOnDef(K.charPopup, true)) return;
let activeLink = null;
document.addEventListener('mouseover', e => {
const link = e.target.closest('a[href*="/Character/"]');
if (link===activeLink) return;
if (!link||link.closest('#tvis-bar,#tvip-bar,.tvis-ov,.tvis-chpop')) return;
const m = link.href?.match(/\/Character\/(\d+)(?:[/?#]|$)/); if (!m) return;
activeLink = link; const id=m[1];
clearTimeout(fetchT); clearTimeout(hideT);
const onMove = ev => posPopup(ev.clientX,ev.clientY);
link.addEventListener('mousemove',onMove);
fetchT = setTimeout(()=>loadAndShow(id,e.clientX,e.clientY),300);
link.addEventListener('mouseleave',()=>{ activeLink=null; clearTimeout(fetchT); link.removeEventListener('mousemove',onMove); hideT=setTimeout(()=>{ if(popEl)popEl.style.display='none'; },1000); },{once:true});
});
};
// RADAR (Takip İstasyonu)
const openTrackModal = () => mkModal(s('tkTitle'), box => {
let trackPage = Math.max(0, Math.min(RADAR_PAGES-1, parseInt(gmGet(DK.TRACK_PAGE, 0)) || 0));
let radarSearchQ = '';
const getBadge = (cls, txt) => `<div class="tvis-tc-badge"><span class="${cls}">${txt}</span></div>`;
const renderCard = entry => {
const cached = CC.get(entry.charId);
const d = cached || entry;
const moved = cached?.localeId && entry.localeId && cached.localeId !== entry.localeId;
const isOff = !d.online || /Çevrimdışı|Offline|Desconectado/.test(d.online) || /\d{1,2}\.\d{2}\.\d{4}/.test(d.online);
const updated = !moved && cached && (cached.online !== entry.online || cached.attitude !== entry.attitude || cached.state !== entry.state);
const online = !isOff;
const card = mk('div','tvis-tcard');
card.dataset.did = entry.charId;
card.draggable = true; // FIX: enable drag on radar cards
if (moved) card.classList.add('tc-moved');
else if (updated) card.classList.add('tc-updated');
else if (online) card.classList.add('tc-online');
const imgWrap = mk('div','tvis-tc-img');
const imgBg = mk('div','tvis-tc-imgbg');
if (d.avatarUrl||entry.avatarUrl) {
const img=mk('img'); img.src=d.avatarUrl||entry.avatarUrl;
img.onerror=()=>{imgBg.innerHTML='🧑';}; imgBg.appendChild(img);
} else { imgBg.textContent='🧑'; }
const dragH = mk('span','tvis-tc-drag','⠿');
let badgeHtml = '';
if (moved) badgeHtml = getBadge('badge-mv', s('badgeMoved'));
else if (updated) badgeHtml = getBadge('badge-upd', s('badgeUpdated'));
else if (online) badgeHtml = getBadge('badge-on', s('badgeOnline'));
else badgeHtml = getBadge('badge-off', s('cpOffline'));
imgBg.insertAdjacentHTML('beforeend', badgeHtml);
imgWrap.append(imgBg, dragH);
const body = mk('div','tvis-tc-body');
const nameA = mk('a','tvis-tc-name', entry.charName);
nameA.href = `${PM}/Character/${entry.charId}`;
const onlineRow = mk('div','tvis-tc-row');
onlineRow.innerHTML = onlineDot(d.online||'', true);
const locRow = mk('div','tvis-tc-row');
locRow.innerHTML = [
d.city ? `<a href="${d.cityHref||'#'}">${d.city}</a>` : '',
d.locale? `<a href="${d.localeHref||'#'}">📍 ${d.locale}</a>` : ''
].filter(Boolean).join(' › ');
const allNotes = gmGet(DK.NOTES,{})||{};
const noteTA = mk('textarea','tvis-tc-note'); noteTA.placeholder=s('tkNote'); noteTA.value=allNotes[entry.charId]||'';
const saveBtn = mk('button','tvis-tc-ok','💾'); saveBtn.style.cssText='font-size:8px;padding:1px 3px;flex-shrink:0';
saveBtn.onclick = () => { const n=gmGet(DK.NOTES,{})||{}; n[entry.charId]=noteTA.value; gmSet(DK.NOTES,n); };
const updTime = entry.savedAt ? new Date(entry.savedAt).toLocaleTimeString(dateLocale,{hour:'2-digit',minute:'2-digit'}) : '--:--';
const actions = mk('div','tvis-tc-actions');
// Mesaj butonu
const msgA = mk('a','','✉️');
msgA.style.cssText='font-size:13px;opacity:.6;text-decoration:none;flex-shrink:0';
msgA.href = `${PM}/Conversations/Conversation/${entry.charId}`;
msgA.title = 'Mesaj';
msgA.target= '_blank';
// Para ver butonu
const moneyA = mk('a','','💰');
moneyA.style.cssText='font-size:13px;opacity:.6;text-decoration:none;flex-shrink:0';
moneyA.href = `${PM}/Character/GiveMoney/${entry.charId}`;
moneyA.title = 'Para Ver';
moneyA.target= '_blank';
// Yanına Git butonu
const locId = d.localeId || entry.localeId;
const gotoA = mk('a','','🗺️');
gotoA.style.cssText='font-size:13px;opacity:.6;text-decoration:none;flex-shrink:0';
gotoA.title = s('tkGoTo');
gotoA.target= '_blank';
if (locId) {
gotoA.href = `${PM}/Locale/MoveToLocale/${locId}/${entry.charId}`;
} else {
gotoA.style.opacity = '0.2';
gotoA.style.cursor = 'default';
gotoA.href = '#';
gotoA.onclick = e => e.preventDefault();
}
const timeSpan = mk('span','tvis-tc-time',updTime);
const refBtn = mk('button','tvis-tc-ref','🔄'); refBtn.title=s('cpRefresh');
refBtn.style.marginLeft = 'auto'; // push right group to end
const xBtn = mk('button','tvis-tc-x','✕');
refBtn.onclick = async () => {
refBtn.disabled=true; refBtn.textContent='⏳';
await waitGap(); lastFetch=Date.now();
const html=await fetch(`${PM}/Character/${entry.charId}`).then(r=>r.ok?r.text():null).catch(()=>null);
if (html){const data=parseCharPage(html);if(data){CC.set(entry.charId,data);updateInRadar(entry.charId,data);}}
render();
};
const mvBtn = mk('button','tvis-tc-x','↕'); mvBtn.title=s('tkMove');
mvBtn.onclick = () => {
showPagePicker(mvBtn, pageIdx => { moveInRadar(entry.charId, pageIdx); render(); }, true);
};
xBtn.onclick = () => {
if(!confirm(s('tkConfirmRm')))return;
removeFromRadar(entry.charId);
render();
};
// Build body
body.appendChild(nameA);
body.appendChild(onlineRow);
body.appendChild(locRow);
if (moved) {
const warnRow=mk('div','tvis-tc-warn',`${s('tcWarnMoved')} ${entry.locale}`);
body.appendChild(warnRow);
} else if (updated) {
const warnRow=mk('div','tvis-tc-warn upd', s('tcWarnUpdated'));
body.appendChild(warnRow);
} else {
// Show state + cash compactly
const infoParts = [];
if (d.state) infoParts.push(d.state);
if (d.cash) infoParts.push(`<span style="color:#218838;font-weight:600">💰 ${d.cash}</span>`);
if (infoParts.length) {
const infoRow = mk('div','tvis-tc-row');
infoRow.innerHTML = infoParts.join(' · ');
body.appendChild(infoRow);
} else if (d.band) {
body.appendChild(mk('div','tvis-tc-row','🎸 '+d.band));
}
}
const noteRow = mk('div'); noteRow.style.cssText='display:flex;gap:2px;margin-top:auto';
noteRow.append(noteTA, saveBtn);
body.appendChild(noteRow);
if (moved) {
const okBtn=mk('button','tvis-tc-ok','✔'); okBtn.title=s('tkConfirmOk'); okBtn.style.flexShrink='0';
okBtn.onclick=()=>{ updateInRadar(entry.charId,{locale:cached.locale,localeId:cached.localeId,city:cached.city,cityHref:cached.cityHref}); render(); };
actions.append(msgA, moneyA, gotoA, okBtn, refBtn, mvBtn, timeSpan, xBtn);
} else {
actions.append(msgA, moneyA, gotoA, refBtn, mvBtn, timeSpan, xBtn);
}
body.appendChild(actions);
card.append(imgWrap,body);
return card;
};
const bulkFetch = entries => {
if (bulkRunning) return;
bulkRunning = true; bulkCancelled = false;
const total = entries.length; let done = 0;
showBulkProgress(0, total);
(async()=>{
for(let i=0;i<entries.length;i++){
if (bulkCancelled) break;
if(i>0) await new Promise(r=>setTimeout(r,2000+Math.random()*4000));
if (bulkCancelled) break;
await waitGap(); lastFetch=Date.now();
const html=await fetch(`${PM}/Character/${entries[i].charId}`).then(r=>r.ok?r.text():null).catch(()=>null);
if(html){const data=parseCharPage(html);if(data){CC.set(entries[i].charId,data);updateInRadar(entries[i].charId,data);}}
done++; showBulkProgress(done, total);
}
bulkRunning = false;
hideBulkProgress(bulkCancelled, done, total);
if(document.getElementById('tvis-modal')) render();
})();
};
// TAB EDIT POPUP — opens via ✏️ button next to each tab
const openTabEdit = (tabIdx, anchorEl) => {
document.getElementById('tvis-tab-edit-pop')?.remove();
const pop = mk('div','tvis-tab-edit-pop'); pop.id='tvis-tab-edit-pop';
const rect = anchorEl.getBoundingClientRect();
pop.style.left = Math.min(rect.left, innerWidth-240)+'px';
pop.style.top = (rect.bottom+4)+'px';
const tabs = getRadarTabs();
const cur = tabs[tabIdx];
let selIcon = cur.icon;
pop.appendChild(mk('div','tvis-sec',s('tkTabEdit')));
const iconPick = mk('div','tvis-icon-pick'); iconPick.style.maxHeight='80px';
const custom = getCustomIcons();
const allIcos = custom.length ? [...custom, '|', ...ICONS] : ICONS;
allIcos.forEach(ico => {
if (ico === '|') {
const sep = mk('div'); sep.style.cssText='width:100%;height:1px;background:#eee;margin:2px 0';
iconPick.appendChild(sep); return;
}
const b = mk('button', ico===selIcon?'sel':'', ico); b.type='button';
b.onclick = () => { selIcon=ico; iconPick.querySelectorAll('button').forEach(x=>x.className=''); b.className='sel'; };
iconPick.appendChild(b);
});
pop.appendChild(iconPick);
const nameI = mk('input'); nameI.value=cur.name; nameI.maxLength=12;
nameI.style.cssText='width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px;margin:4px 0';
pop.appendChild(nameI);
const getTabColor = mkColorPicker(pop, cur.color||'#6f42c1');
pop.appendChild(mkB('✔ ' + s('iaSave'), 'btn-sm btn-g', () => {
const t = getRadarTabs();
t[tabIdx] = { icon: selIcon, name: (nameI.value.trim().toUpperCase().substring(0,12)) || String(tabIdx+1), color: getTabColor() };
gmSet(DK.RADAR_TABS, t);
pop.remove();
render();
}));
const cancelBtn = mkB(s('cancelLbl'), 'btn-sm btn-grey', () => pop.remove());
cancelBtn.style.marginLeft='4px';
pop.lastChild.insertAdjacentElement('afterend', cancelBtn);
document.body.appendChild(pop);
setTimeout(() => {
const close = e => { if (!pop.contains(e.target)) { pop.remove(); document.removeEventListener('mousedown', close); } };
document.addEventListener('mousedown', close);
}, 10);
};
const render = () => {
box.querySelectorAll('.tvis-tl').forEach(e=>e.remove());
const pageEntries = getRadarPage(trackPage);
const tabs = getRadarTabs();
const realEntries = pageEntries;
if (!pageEntries.length && trackPage === 0) {
const anyTracked = getAllTracked().length > 0;
if (!anyTracked) {
const w=mk('div','tvis-tl'); const d=mk('div','',s('tkEmpty')); d.style.cssText='color:#999;font-size:12px;margin-bottom:10px';
w.appendChild(d); box.appendChild(w); return;
}
}
const wrap = mk('div','tvis-tl');
// ── SEARCH BAR ──────────────────────────────────────────────────────────
const searchRow = mk('div'); searchRow.style.cssText='display:flex;gap:6px;align-items:center;margin-bottom:8px';
const searchI = mk('input'); searchI.id='tvis-radar-search';
searchI.placeholder='🔍 Karakterleri ara...';
searchI.style.cssText='flex:1;padding:4px 8px;border:1px solid #c9b8f0;border-radius:4px;font-size:12px';
searchI.value = radarSearchQ;
const clearBtn = mkB('✕','btn-sm btn-grey',()=>{ radarSearchQ=''; render(); });
clearBtn.style.display = radarSearchQ ? '' : 'none';
searchI.addEventListener('input',()=>{ radarSearchQ=searchI.value; clearBtn.style.display=radarSearchQ?'':'none'; renderSearchOrNormal(); });
searchRow.append(searchI, clearBtn);
wrap.appendChild(searchRow);
const renderSearchOrNormal = () => {
// Remove previous content below search bar
wrap.querySelectorAll('.tvis-tl-body').forEach(e=>e.remove());
const body = mk('div','tvis-tl-body');
wrap.appendChild(body);
if (radarSearchQ.trim()) {
// Search mode
const q = radarSearchQ.trim().toLowerCase();
const results = [];
for (let i=0; i<RADAR_PAGES; i++) {
getRadarPage(i).forEach(entry => {
if ((entry.charName||'').toLowerCase().includes(q)) results.push({entry,pageIdx:i});
});
}
if (!results.length) {
const em=mk('div','',`"${radarSearchQ}" için sonuç bulunamadı.`); em.style.cssText='color:#999;font-size:12px;padding:16px 0;text-align:center';
body.appendChild(em);
} else {
const infoRow=mk('div',''); infoRow.style.cssText='font-size:10px;color:#aaa;margin-bottom:6px';
infoRow.textContent=`${results.length} sonuç`;
body.appendChild(infoRow);
const grid=mk('div','tvis-tgrid');
results.forEach(({entry,pageIdx})=>{
const card=renderCard(entry);
// Tab badge overlay
const tab=tabs[pageIdx];
const badge=mk('div',''); badge.style.cssText='position:absolute;top:3px;right:3px;font-size:8px;padding:1px 5px;border-radius:8px;color:#fff;font-weight:700;z-index:2;pointer-events:none';
badge.style.background=tab.color||'#6f42c1';
badge.textContent=`${tab.icon} ${tab.name}`;
card.style.position='relative';
card.appendChild(badge);
grid.appendChild(card);
});
body.appendChild(grid);
}
// Tab row — dimmed in search mode
const tabRow2 = buildTabRow(tabs, true);
body.insertAdjacentElement('afterbegin', tabRow2);
} else {
// Normal mode
const tabRow2 = buildTabRow(tabs, false);
body.appendChild(tabRow2);
buildNormalContent(body, tabs, realEntries);
}
};
const buildTabRow = (tabs, dimmed) => {
const tabRow = mk('div'); tabRow.style.cssText='display:flex;gap:4px;align-items:center;margin-bottom:10px;flex-wrap:wrap';
if(dimmed) tabRow.style.opacity='0.55';
tabs.forEach((tab, i) => {
const tabWrap = mk('span'); tabWrap.style.cssText='display:inline-flex;align-items:center;gap:1px';
const isWT = false;
const lbl = `${tab.icon} ${tab.name}`;
const btn = mk('button', 'tvis-page-tab'+(trackPage===i&&!dimmed?' active':''), lbl);
const tc = tab.color||'#6f42c1';
btn.style.borderColor = tc;
if(trackPage===i&&!dimmed){ btn.style.background=tc; btn.style.color='#fff'; }
else { btn.style.color=tc; }
btn.onclick = () => { radarSearchQ=''; trackPage=i; gmSet(DK.TRACK_PAGE,i); render(); };
btn.title = tab.icon+' '+tab.name;
const editBtn = mk('button','tvis-tab-edit-btn','✏️');
editBtn.title = s('tkTabEdit');
editBtn.onclick = e => { e.stopPropagation(); openTabEdit(i, editBtn); };
tabWrap.append(btn, editBtn);
tabRow.appendChild(tabWrap);
});
const bulkBtn = mk('button','btn-b',s('tkUpdate')); bulkBtn.style.cssText='font-size:10px;padding:3px 10px;margin-left:auto;white-space:nowrap;flex-shrink:0';
bulkBtn.onclick = () => { if (bulkRunning) return; bulkFetch(pageEntries.filter(e=>!e._empty)); };
if (bulkRunning) { bulkBtn.disabled=true; bulkBtn.style.opacity='.5'; }
tabRow.appendChild(bulkBtn);
return tabRow;
};
const buildNormalContent = (body, tabs, realEntries) => {
const grid = mk('div','tvis-tgrid');
mkDraggable(grid, ids => {
const pageArr = getRadarPage(trackPage);
const map = {}; pageArr.forEach(e => { map[e.charId] = e; });
const reordered = ids.map(id => map[id]).filter(Boolean);
setRadarPage(trackPage, reordered); render();
});
pageEntries.forEach(entry => { grid.appendChild(renderCard(entry)); });
const totalTracked = getAllTracked().length;
body.appendChild(grid);
const footer = mk('div'); footer.style.cssText='margin-top:8px;font-size:10px;color:#bbb;display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px';
footer.innerHTML=`<span>${s('tcFooterHint')}</span><span>${s('tcPage')} ${trackPage+1}: ${realEntries.length}/${RADAR_PAGE_SIZE} · ${s('tcTotal')}: ${totalTracked}</span>`;
body.appendChild(footer);
};
renderSearchOrNormal();
box.appendChild(wrap);
// Restore focus to search box after re-render
if (radarSearchQ) setTimeout(()=>{ const si=document.getElementById('tvis-radar-search'); if(si){si.focus();si.setSelectionRange(si.value.length,si.value.length);} },10);
};
render();
});
// ── INTERACT HELPER DATA ──────────────────────────────────────────────────────
const INTERACT_DATA = {
1: {sub:'arkadaş', joy:'+2-3%', love:null, hate:'-1%', note:null},
3: {sub:'arkadaş', joy:'+2-3%', love:null, hate:null, note:null},
4: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
5: {sub:'arkadaş', joy:'+2-3%', love:null, hate:'-2%', note:null},
7: {sub:'romantik', joy:null, love:'+2%', hate:null, note:'Bar/Rest.'},
8: {sub:'arkadaş', joy:'+2-3%', love:null, hate:null, note:null},
9: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
10: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
11: {sub:'romantik', joy:'-1%', love:'+5%', hate:null, note:'Ev/Otel/Park'},
12: {sub:'romantik', joy:null, love:'+2-3%',hate:'-1%', note:null},
13: {sub:'romantik', joy:null, love:'+2-3%',hate:null, note:'Ev/Park'},
14: {sub:'romantik', joy:null, love:'+2-3%',hate:null, note:null},
15: {sub:'nefret', joy:'-1%', love:null, hate:'+5%', note:null},
16: {sub:'nefret', joy:null, love:null, hate:'+5%', note:null},
18: {sub:'arkadaş', joy:'+5%', love:null, hate:'-4%', note:null},
19: {sub:'romantik', joy:'-1%', love:'+5%', hate:null, note:'Ev/Otel'},
20: {sub:'romantik', joy:null, love:'+5%', hate:null, note:'Ev/Otel'},
21: {sub:'arkadaş', joy:'+2-3%', love:null, hate:'-2%', note:'Şarkı söyleme'},
29: {sub:'arkadaş', joy:'+2%', love:null, hate:null, note:null},
30: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
32: {sub:'arkadaş', joy:'+5%', love:null, hate:'-5%', note:'Tıp'},
33: {sub:'arkadaş', joy:'+2-8%', love:null, hate:'-6%', note:'İllüzyon'},
34: {sub:'arkadaş', joy:'+5%', love:null, hate:'-4%', note:null},
35: {sub:'romantik', joy:null, love:'+3-7%',hate:null, note:'Dans/Bar'},
36: {sub:'nefret', joy:'-4-6%', love:null, hate:'+5-7%', note:null},
39: {sub:'arkadaş', joy:'+1-2%', love:null, hate:null, note:'İbadet evi'},
41: {sub:'nefret', joy:'-2%', love:null, hate:'+2%', note:null},
42: {sub:'nefret', joy:null, love:null, hate:'+5%', note:null},
44: {sub:'arkadaş', joy:'+5%', love:null, hate:'-5%', note:null},
48: {sub:'nefret', joy:'-2%', love:null, hate:'+3%', note:null},
49: {sub:'nefret', joy:'-2-4%', love:'-2-3%',hate:'+3-5%', note:null},
51: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
54: {sub:'arkadaş', joy:'+0-1%', love:null, hate:'-1%', note:null},
55: {sub:'arkadaş', joy:'+2-3%', love:null, hate:null, note:null},
56: {sub:'arkadaş', joy:'+2-3%', love:null, hate:null, note:null},
57: {sub:'arkadaş', joy:'+2-3%', love:null, hate:null, note:null},
59: {sub:'arkadaş', joy:'+2-3%', love:null, hate:null, note:null},
60: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
62: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
63: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
65: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
66: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
67: {sub:'arkadaş', joy:'+5%', love:null, hate:null, note:null},
68: {sub:'arkadaş', joy:'+2%', love:null, hate:null, note:null},
69: {sub:'arkadaş', joy:'+5%', love:'-1%', hate:null, note:'En iyi arkadaş'},
70: {sub:'arkadaş', joy:'+5%', love:'-1%', hate:null, note:'En iyi arkadaş'},
71: {sub:'romantik', joy:null, love:'+2%', hate:null, note:null},
75: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
76: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
77: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
78: {sub:'romantik', joy:null, love:'+5%', hate:null, note:null},
79: {sub:'nefret', joy:null, love:null, hate:'+2%', note:null},
81: {sub:'nefret', joy:null, love:null, hate:'+5%', note:null},
82: {sub:'nefret', joy:null, love:null, hate:'+5%', note:null},
84: {sub:'nefret', joy:'-5%', love:null, hate:'+5%', note:null},
89: {sub:'romantik', joy:null, love:'+5%', hate:null, note:'Spor/Sahil'},
115:{sub:'nefret', joy:null, love:null, hate:'+3%', note:null},
129:{sub:'romantik', joy:null, love:'+5%', hate:'-3%', note:'Park'},
136:{sub:'arkadaş', joy:'+5%', love:null, hate:null, note:'Mezarlık'},
154:{sub:'özel', joy:null, love:'-15%', hate:null, note:'\u26a0\ufe0f Aşk -%15'},
155:{sub:'özel', joy:null, love:null, hate:'-15%', note:'\u26a0\ufe0f Nefret -%15'},
156:{sub:'özel', joy:'-15%', love:null, hate:null, note:'\u26a0\ufe0f Keyif -%15'},
161:{sub:'romantik', joy:null, love:'+2%', hate:null, note:null},
164:{sub:'romantik', joy:'-1%', love:'+5%', hate:null, note:null},
166:{sub:'arkadaş', joy:'+3%', love:null, hate:'-3%', note:null},
};
// Merge hardcoded data with user's custom entries (custom overrides on conflict)
const getInteractData = () => {
const custom = gmGet(DK.INTERACT_CUSTOM, {}) || {};
return Object.assign({}, INTERACT_DATA, custom);
};
const INTERACT_DEFAULTS = {
arkadaş: [34, 62, 1],
romantik: [35, 78, 10],
nefret: [15, 16, 84],
özel: [154, 155, 156],
};
const INTERACT_TYPES = ['standart','arkadaş','romantik','nefret','özel'];
// INLINE ACTIONS (Hızlı Bağlantılar)
const W = PM + '/';
const IA_DEF = {
character: [
{id:'phone', icon:'📞',label:'Telefon', path:`${W}Interact/Phone/`, idType:'character'},
{id:'msg', icon:'✉️',label:'Mesaj', path:`${W}Conversations/Conversation/`, idType:'character'},
{id:'iact', icon:'👋',label:'İlgilen', path:`${W}Interact/`, idType:'character'},
{id:'copyid',icon:'📋',label:'ID Kopyala',path:'', idType:'copyid'},
],
locale: [
{id:'goto', icon:'🗺️',label:'Git', path:`${W}Locale/MoveToLocale/`, idType:'locale'},
{id:'chars', icon:'👥',label:'Mekandakiler', path:`${W}Locale/CharactersPresent/`, idType:'locale'},
{id:'copyid',icon:'📋',label:'ID Kopyala', path:'', idType:'copyid'},
],
artist: [
{id:'upcoming',icon:'🎵',label:'Gelecek Konserler',path:`${W}Artist/UpcomingPerformances/`,idType:'artist'},
{id:'past', icon:'🎶',label:'Son Konserler', path:`${W}Artist/PastPerformances/`, idType:'artist'},
{id:'copyid', icon:'📋',label:'ID Kopyala', path:'', idType:'copyid'},
],
city: [
{id:'flight',icon:'✈️',label:'Uçuş', path:`${W}City/BookFlight/`, idType:'city'},
{id:'jet', icon:'🛩️',label:'VIP Jet', path:`${W}City/PrivateJet/`, idType:'city'},
{id:'road', icon:'🚗',label:'Kara', path:`${W}City/RoadTrip/`, idType:'city'},
],
};
// copyid removed from type dropdown — copyid buttons in IA_DEF still work
const IA_TYPES = ['character','locale','artist','city'];
const IA_LABELS = {
character: '🧑 Karakter',
locale: '📍 Mekan',
artist: '🎸 Sanatçı',
city: '🏙️ Şehir',
};
const getIA = sec => gmGet(DK.IA+sec, null) || IA_DEF[sec] || [];
const saveIA = (sec,v) => gmSet(DK.IA+sec,v);
const resetIA= sec => gmDel(DK.IA+sec);
const parseURL = url => { try { return new URL(url.includes('://')?url:'https://p.com'+url).pathname.replace(/\/\d+([?#].*)?$/,'').replace(/\/?$/,'/'); } catch { return url; } };
const openIAEdit = () => mkModal(s('iaEdit'), cont => {
let active='character';
const tabs=mk('div'); tabs.style.cssText='display:flex;gap:4px;margin-bottom:10px;flex-wrap:wrap';
const tBtns={}; cont.appendChild(tabs);
const secCont=mk('div'); cont.appendChild(secCont);
const renderSec=()=>{
secCont.innerHTML='';
const cfg=getIA(active).map(a=>({...a}));
const list=mk('div');
mkDraggable(list,ids=>{ const cur=getIA(active),map={}; cur.forEach(a=>{map[a.id]=a;}); saveIA(active,ids.map(id=>map[id]).filter(Boolean)); renderSec(); });
cfg.forEach(a=>{
const row=mk('div','tvis-iarow'); row.draggable=true; row.dataset.did=a.id;
const dr=mk('span','','⠿'); dr.style.cssText='color:#ccc;cursor:grab;flex-shrink:0';
const ic=mk('span','',a.icon+' ');
const lb=mk('span','',a.label); lb.style.flex='1';
const tp=mk('span','',a.idType); tp.style.cssText='font-size:10px;color:#aaa;flex-shrink:0';
const rm=mkB('🗑','btn-sm btn-r',()=>{saveIA(active,getIA(active).filter(x=>x.id!==a.id));renderSec();});
row.append(dr,ic,lb,tp,rm); list.appendChild(row);
});
secCont.appendChild(list);
const rst=mkB(s('iaDefaults'),'btn-sm btn-grey',()=>{if(confirm(s('iaResetConfirm'))){resetIA(active);renderSec();}});
rst.style.margin='6px 0'; secCont.appendChild(rst);
const form=mk('div'); form.style.cssText='border-top:1px solid #eee;padding-top:8px;margin-top:2px';
form.appendChild(mk('div','tvis-sec',s('iaAddSec')));
const urlI=mk('input'); urlI.placeholder=s('iaUrlPlh'); urlI.style.cssText='width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px;margin-bottom:3px';
const nfo=mk('div',''); nfo.style.cssText='font-size:10px;color:#17a2b8;margin-bottom:5px;min-height:13px';
urlI.addEventListener('input',()=>{const v=urlI.value.trim();nfo.textContent=v?s('iaUrlInfo')+' '+parseURL(v):''});
const lblI=mk('input'); lblI.placeholder=s('iaLblPlh'); lblI.style.cssText='width:100%;box-sizing:border-box;padding:4px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px;margin-bottom:6px';
const tRow=mk('div'); tRow.style.cssText='display:flex;align-items:center;gap:6px;margin-bottom:6px';
const tSel=mk('select'); tSel.style.cssText='flex:1;padding:3px;border:1px solid #ccc;border-radius:3px;font-size:11px';
IA_TYPES.forEach(tp=>{const o=mk('option','',tp);o.value=tp;tSel.appendChild(o);}); tSel.value=active;
tRow.append(mk('span','','ID Tipi:'),tSel);
form.append(urlI,nfo,lblI,tRow);
form.appendChild(mk('div','tvis-sec','Simge'));
const getSel=mkIconPicker(form,'🔗');
form.appendChild(mkB(s('iaSave'),'btn-sm btn-g',()=>{
const url=urlI.value.trim(),lbl=lblI.value.trim(); if(!url||!lbl) return;
const c=getIA(active); c.push({id:'c'+Date.now(),icon:getSel(),label:lbl,path:parseURL(url),idType:tSel.value});
saveIA(active,c); urlI.value=''; lblI.value=''; renderSec();
}));
secCont.appendChild(form);
};
Object.entries(IA_LABELS).forEach(([sec,lbl])=>{
const b=mkB(lbl,sec===active?'btn-sm btn-v':'btn-sm btn-grey',()=>{active=sec;Object.entries(tBtns).forEach(([k,b])=>b.className=k===sec?'btn-sm btn-v':'btn-sm btn-grey');renderSec();});
tBtns[sec]=b; tabs.appendChild(b);
});
renderSec();
});
const applyInlineActions = () => {
if (!isOnDef(K.ia, true)) return;
const clip=(id,name,type)=>{ navigator.clipboard?.writeText(id); addClip(id,name,type); };
const mkBtn=(a,id,name,sec)=>{
if (a.idType==='copyid'){const b=mk('button','tvis-ia',a.icon);b.title=a.label;b.onclick=e=>{e.preventDefault();e.stopPropagation();clip(id,name,sec);};return b;}
const el=mk('a','tvis-ia',a.icon); el.title=a.label; el.href=a.path+id; return el;
};
const SELS = {
character: {sel:'a[href*="/Character/"]', r:/\/Character\/(\d+)(?:[/?#]|$)/},
locale: {sel:'a[href*="/Locale/"]', r:/\/Locale\/(\d+)(?:[/?#]|$)/},
artist: {sel:'a[href*="/Artist/"]', r:/\/Artist\/(\d+)(?:[/?#]|$)/},
city: {sel:'a[href*="/City/"]', r:/\/City\/(\d+)(?:[/?#]|$)/},
};
Object.entries(SELS).forEach(([sec,cfg])=>{
const acts=getIA(sec);
document.querySelectorAll(cfg.sel).forEach(link=>{
if(link.dataset.tvisIa||link.closest('#tvis-bar,#tvip-bar,.tvis-ov,.tvip-ov,.tvis-chpop')) return;
const m=link.href?.match(cfg.r); if(!m) return;
link.dataset.tvisIa='1';
const span=mk('span','tvis-ia-wrap');
acts.forEach(a=>span.appendChild(mkBtn(a,m[1],link.textContent.trim(),sec)));
link.insertAdjacentElement('afterend',span);
});
});
};
// INTERACT CUSTOM DATA EDITOR
const openInteractDataEdit = () => mkModal(s('interactDataEdit'), cont => {
const getCustom = () => gmGet(DK.INTERACT_CUSTOM, {}) || {};
const saveCustom = d => gmSet(DK.INTERACT_CUSTOM, d);
const render = () => {
cont.innerHTML = '';
const custom = getCustom();
const keys = Object.keys(custom);
if (!keys.length) {
const em = mk('div','',s('interactDataEmpty'));
em.style.cssText = 'color:#999;font-size:12px;margin-bottom:10px';
cont.appendChild(em);
} else {
const tbl = mk('table','tvis-ih-guide-tbl'); tbl.style.marginBottom='10px';
const thead = mk('thead');
const hr = mk('tr');
['ID', s('interactType'), s('interactColJoy'), s('interactColLove'), s('interactColHate'), s('interactColNote'), ''].forEach(h => {
const th = mk('th','',h); hr.appendChild(th);
});
thead.appendChild(hr); tbl.appendChild(thead);
const tbody = mk('tbody');
keys.sort((a,b)=>Number(a)-Number(b)).forEach(k => {
const d = custom[k];
const tr = mk('tr');
const subBadge = d.sub ? `<span class="tvis-ih-badge tvis-ih-badge-${d.sub?.[0]}">${d.sub}</span>` : '—';
[
`<b>${k}</b>`,
subBadge,
d.joy ? `<span class="tvis-ih-joy">${d.joy}</span>` : '—',
d.love ? `<span class="tvis-ih-love">${d.love}</span>`: '—',
d.hate ? `<span class="tvis-ih-hate">${d.hate}</span>`: '—',
d.note ? `<span class="tvis-ih-note">${d.note}</span>`: '—',
].forEach(html => { const td=mk('td'); td.innerHTML=html; tr.appendChild(td); });
const delTd = mk('td');
delTd.appendChild(mkB('🗑','btn-sm btn-r',()=>{
const c=getCustom(); delete c[k]; saveCustom(c); render();
}));
tr.appendChild(delTd);
tbody.appendChild(tr);
});
tbl.appendChild(tbody);
cont.appendChild(tbl);
}
// ── ADD FORM ──
const form = mk('div'); form.style.cssText='border-top:1px solid #eee;padding-top:8px';
const row1 = mk('div'); row1.style.cssText='display:flex;gap:5px;flex-wrap:wrap;align-items:center;margin-bottom:6px';
const inp = (ph, w) => { const i=mk('input'); i.placeholder=ph; i.style.cssText=`width:${w};padding:3px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px`; return i; };
const idI = inp(s('interactDataId'), '64px'); idI.type='number'; idI.min='1';
const subS = mk('select'); subS.style.cssText='padding:3px 5px;border:1px solid #ccc;border-radius:3px;font-size:11px';
['arkadaş','romantik','nefret'].forEach(v=>{ const o=mk('option','',v); o.value=v; subS.appendChild(o); });
const joyI = inp('😊 Joy', '72px');
const lovI = inp('❤️ Love', '72px');
const hatI = inp('😡 Hate', '72px');
const notI = inp(s('interactColNote'), '120px');
const dupW = mk('span',''); dupW.style.cssText='color:#e67e22;font-size:11px;display:none'; dupW.textContent=s('interactDataDup');
row1.append(idI, subS, joyI, lovI, hatI, notI);
const addBtn = mkB(s('interactDataAdd'), 'btn-sm btn-g', () => {
const id = idI.value.trim(); if (!id) return;
const c = getCustom();
if (c[id]) { dupW.style.display=''; return; }
dupW.style.display='none';
c[id] = {
sub: subS.value,
joy: joyI.value.trim() || null,
love: lovI.value.trim() || null,
hate: hatI.value.trim() || null,
note: notI.value.trim() || null,
};
saveCustom(c);
idI.value=''; joyI.value=''; lovI.value=''; hatI.value=''; notI.value='';
render();
});
const delAll = mkB(s('interactDataReset'), 'btn-sm btn-r', () => {
if (!confirm(s('interactDataResetQ'))) return;
saveCustom({}); render();
});
delAll.style.marginLeft='6px';
form.append(row1, dupW, addBtn, delAll);
cont.appendChild(form);
};
render();
});
// INTERACT HELPER
const applyInteractHelper = () => {
if (!isOnDef(K.interact, true)) return;
if (!location.href.includes('/Interact/')) return;
const sel = document.getElementById('ctl00_cphTopColumn_ctl00_ddlInteractionTypes');
// Karakter ID'sini URL'den al
const charIdMatch = location.href.match(/\/Interact\/(?:Details\/)?(\d+)/);
const charId = charIdMatch ? charIdMatch[1] : '0';
// Mevcut tipi yükle
let curType = gmGet(DK.INTERACT_TYPE + charId, 'standart');
// ── Seçeneğe ikon/değer yaz ──────────────────────────────────────────────
const decorateOptions = () => {
if (!sel) return;
[...sel.options].forEach(opt => {
if (!opt.value || opt.value === '0') return;
const id = parseInt(opt.value);
const data = getInteractData()[id];
if (!data) return;
const parts = [];
if (data.joy) parts.push(`😊${data.joy}`);
if (data.love) parts.push(`❤️${data.love}`);
if (data.hate) parts.push(`😡${data.hate}`);
const baseName = opt.dataset.ihBase || opt.textContent;
opt.dataset.ihBase = baseName;
opt.textContent = parts.length ? `${baseName} (${parts.join(' ')})` : baseName;
opt.dataset.ihSub = data.sub;
});
};
// ── Filtreye göre seçenekleri gizle/göster ───────────────────────────────
const applyFilter = (type) => {
curType = type;
gmSet(DK.INTERACT_TYPE + charId, type);
if (sel) {
[...sel.options].forEach(opt => {
if (!opt.value || opt.value === '0') { opt.style.display = ''; return; }
const sub = opt.dataset.ihSub;
if (type === 'standart' || !sub) {
opt.style.display = '';
} else {
opt.style.display = sub === type ? '' : 'none';
}
});
// Varsayılan seçenekleri sırayla dene
const defIds = INTERACT_DEFAULTS[type] || [];
let selected = false;
for (const defId of defIds) {
const defOpt = [...sel.options].find(o => o.value === String(defId) && o.style.display !== 'none');
if (defOpt) { sel.value = defOpt.value; selected = true; break; }
}
if (!selected) {
const first = [...sel.options].find(o => o.value !== '0' && o.style.display !== 'none');
if (first) sel.value = first.value;
}
}
// Buton stillerini güncelle
document.querySelectorAll('.tvis-ih-typebtn').forEach(b => {
b.classList.toggle('active', b.dataset.type === type);
});
};
// ── Kıskançlık dropdown'unu bul ve varsayılanı ayarla ───────────────────
const jealousSel = document.getElementById('ctl00_cphTopColumn_ctl00_ddlSexCausesJealousy');
if (jealousSel) jealousSel.value = '0'; // Kıskanmam = default
// ── Romantizm seviyesini sayfadan oku ───────────────────────────────────
const getLove = () => {
const rows = document.querySelectorAll('table.width100 tr');
for (const row of rows) {
const label = row.querySelector('td.nowrap');
if (!label) continue;
const txt = label.textContent.trim().toLowerCase();
if (txt.includes('aşk') || txt.includes('love') || txt.includes('amor')) {
const sk = row.querySelector('.sortkey');
if (sk) return parseInt(sk.textContent.trim()) || 0;
}
}
return -1;
};
// ── UI oluştur ───────────────────────────────────────────────────────────
const buildUI = () => {
if (document.getElementById('tvis-ih-wrap')) return;
const wrap = mk('div',''); wrap.id = 'tvis-ih-wrap';
// ROW 1: Arkadaş / Romantik / Nefret
const barTop = mk('div','tvis-ih-bar'); barTop.style.marginBottom='3px';
[
{ key:'arkadaş', label: s('interactFriend') },
{ key:'romantik', label: s('interactRomantic') },
{ key:'nefret', label: s('interactHate') },
].forEach(({ key, label }) => {
const cls = `tvis-ih-typebtn t-${key}${key === curType ? ' active' : ''}`;
const btn = mk('button', cls, label);
btn.dataset.type = key; btn.type = 'button';
btn.onclick = () => applyFilter(key);
barTop.appendChild(btn);
});
wrap.appendChild(barTop);
// ROW 2: Standart · 📋 Listele/Düzenle
const barBot = mk('div','tvis-ih-bar');
const stdBtn = mk('button', `tvis-ih-typebtn${curType === 'standart' ? ' active' : ''}`, s('interactAll'));
stdBtn.dataset.type = 'standart'; stdBtn.type = 'button';
stdBtn.onclick = () => applyFilter('standart');
barBot.appendChild(stdBtn);
const guideBtn = mk('button','btn-sm btn-grey','📋 Listele / Düzenle');
guideBtn.style.marginLeft='auto'; guideBtn.type='button';
guideBtn.onclick = () => openInteractGuide();
barBot.appendChild(guideBtn);
wrap.appendChild(barBot);
// Kıskançlık uyarısı
if (jealousSel && jealousSel.value === '1') {
const loveVal = getLove();
if (loveVal >= 70) wrap.appendChild(mk('div','tvis-ih-warn', s('interactWarn')));
}
// Dropdown mevcut ise onun ALTINA ekle (dropdown üstte, butonlar altta)
if (sel) {
sel.insertAdjacentElement('afterend', wrap);
} else {
// Seçenek yoksa (ilgilenme hakkı bitti) sayfada uygun bir yere yerleştir
const anchor = document.querySelector(
'#ctl00_cphTopColumn_ctl00_pnlInteract, .interaction-form, form[action*="Interact"], .box, #ppm-content'
);
if (anchor) anchor.insertAdjacentElement('afterbegin', wrap);
else document.body.appendChild(wrap);
}
};
// ── Rehber modal (İlgilen Verilerini Listele / Düzenle) ──────────────────
const openInteractGuide = () => {
mkModal('İlgilen Verilerini Listele / Düzenle', cont => {
cont.style.cssText = 'font-size:13px;padding-right:20px';
cont.parentElement.style.overflowX = 'auto';
cont.parentElement.style.width = 'auto';
cont.parentElement.style.maxWidth = 'min(98vw,960px)';
const pageMap = {};
if (sel) {
[...sel.options].forEach(opt => {
if (!opt.value || opt.value === '0') return;
pageMap[parseInt(opt.value)] = opt.dataset.ihBase || opt.textContent.split(' (')[0];
});
}
const renderGuide = () => {
cont.innerHTML = '';
const allData = getInteractData();
const custom = gmGet(DK.INTERACT_CUSTOM, {}) || {};
const allIds = [...new Set([...Object.keys(allData).map(Number), ...Object.keys(pageMap).map(Number)])].sort((a,b)=>a-b);
const tbl = mk('table','tvis-ih-guide-tbl'); tbl.style.cssText='width:100%;table-layout:auto';
const thead = mk('thead'); const hrow = mk('tr');
['ID', s('interactName'), s('interactType'), '😊', '❤️', '😡', s('interactColNote'), '✓', ''].forEach(h => {
thead.appendChild(hrow); hrow.appendChild(mk('th','',h));
});
thead.appendChild(hrow); tbl.appendChild(thead);
const tbody = mk('tbody');
allIds.forEach(id => {
const data = allData[id] || {};
const isCustom = !!custom[id];
const onPage = !!pageMap[id];
const name = pageMap[id] || `ID ${id}`;
const tr = mk('tr');
if (!onPage) tr.style.opacity = '0.45';
if (isCustom) tr.style.background = '#f0fff4';
const subBadge = data.sub
? `<span class="tvis-ih-badge tvis-ih-badge-${data.sub?.[0]}">${data.sub}</span>`
: '<span class="tvis-ih-note">—</span>';
[
`<span style="color:#aaa;font-size:10px">${id}</span>`,
`<span>${name}</span>`,
subBadge,
data.joy ? `<span class="tvis-ih-joy">${data.joy}</span>` : '—',
data.love ? `<span class="tvis-ih-love">${data.love}</span>` : '—',
data.hate ? `<span class="tvis-ih-hate">${data.hate}</span>` : '—',
data.note ? `<span class="tvis-ih-note">${data.note}</span>` : '—',
onPage ? `<span style="color:#28a745;font-weight:700">✓</span>` : '',
].forEach(html => { const td=mk('td'); td.innerHTML=html; tr.appendChild(td); });
// Action cell — edit/delete for custom entries
const actTd = mk('td'); actTd.style.whiteSpace='nowrap';
if (isCustom) {
const eBtn = mkB('✏','btn-sm btn-grey', () => {
if (document.getElementById('tvis-ih-editrow')) document.getElementById('tvis-ih-editrow').remove();
const editRow = mk('tr'); editRow.id='tvis-ih-editrow'; editRow.style.background='#fffde7';
const eTd = mk('td'); eTd.colSpan=9; eTd.style.padding='4px 6px';
const ef = mk('div'); ef.style.cssText='display:flex;gap:4px;align-items:center;flex-wrap:wrap';
const idL = mk('span','',`ID: ${id}`); idL.style.cssText='font-size:10px;color:#aaa;font-weight:600;min-width:40px';
const subS=mk('select'); subS.style.cssText='padding:2px 4px;border:1px solid #ccc;border-radius:3px;font-size:11px';
['arkadaş','romantik','nefret'].forEach(v=>{const o=mk('option','',v);o.value=v;if(data.sub===v)o.selected=true;subS.appendChild(o);});
const fi=(ph,val,w)=>{const i=mk('input');i.placeholder=ph;i.value=val||'';i.style.cssText=`width:${w};padding:2px 5px;border:1px solid #ccc;border-radius:3px;font-size:11px`;return i;};
const jI=fi('😊',data.joy,'64px'), lI=fi('❤️',data.love,'64px'), hI=fi('😡',data.hate,'64px'), nI=fi('Not',data.note,'110px');
const sv=mkB('✔','btn-sm btn-g',()=>{
const c=gmGet(DK.INTERACT_CUSTOM,{})||{};
c[id]={sub:subS.value,joy:jI.value.trim()||null,love:lI.value.trim()||null,hate:hI.value.trim()||null,note:nI.value.trim()||null};
gmSet(DK.INTERACT_CUSTOM,c); if(sel)decorateOptions(); renderGuide();
});
const cn=mkB('✕','btn-sm btn-grey',()=>editRow.remove());
ef.append(idL,subS,jI,lI,hI,nI,sv,cn);
eTd.appendChild(ef); editRow.appendChild(eTd);
tr.insertAdjacentElement('afterend',editRow);
});
const dBtn=mkB('🗑','btn-sm btn-r',()=>{
if(!confirm(s('deleteEntry')))return;
const c=gmGet(DK.INTERACT_CUSTOM,{})||{}; delete c[id]; gmSet(DK.INTERACT_CUSTOM,c);
if(sel)decorateOptions(); renderGuide();
});
actTd.append(eBtn, dBtn);
}
tr.appendChild(actTd);
tbody.appendChild(tr);
});
// ── INLINE ADD ROW ────────────────────────────────────────────
const addRow = mk('tr'); addRow.style.background='#f8f0ff';
const addTd = mk('td'); addTd.colSpan=9;
addTd.style.cssText='padding:8px 6px;border-top:2px solid #c9b8f0';
const wip = mk('div'); wip.style.cssText='font-size:9px;color:#e67e22;margin-bottom:5px';
wip.textContent=s('interactDataWip');
const af = mk('div'); af.style.cssText='display:flex;gap:4px;align-items:center;flex-wrap:wrap';
const fi2=(ph,w,tp='text')=>{const i=mk('input');i.placeholder=ph;i.type=tp;if(tp==='number'){i.min='1';i.max='9999';}i.style.cssText=`width:${w};padding:2px 5px;border:1px solid #c9b8f0;border-radius:3px;font-size:11px`;return i;};
const nId=fi2('ID','52px','number');
const nSub=mk('select'); nSub.style.cssText='padding:2px 4px;border:1px solid #c9b8f0;border-radius:3px;font-size:11px';
['arkadaş','romantik','nefret'].forEach(v=>{const o=mk('option','',v);o.value=v;nSub.appendChild(o);});
const nJoy=fi2('😊 Joy','64px'), nLov=fi2('❤️ Love','64px'), nHat=fi2('😡 Hate','64px'), nNot=fi2('Koşul/Not','110px');
const dupW=mk('span',''); dupW.style.cssText='color:#e67e22;font-size:10px;display:none'; dupW.textContent=s('interactDataDup');
const aBtn=mkB('+ Ekle','btn-sm btn-v',()=>{
const idv=nId.value.trim(); if(!idv)return;
const c=gmGet(DK.INTERACT_CUSTOM,{})||{};
if(c[idv]){dupW.style.display='';return;}
dupW.style.display='none';
c[idv]={sub:nSub.value,joy:nJoy.value.trim()||null,love:nLov.value.trim()||null,hate:nHat.value.trim()||null,note:nNot.value.trim()||null};
gmSet(DK.INTERACT_CUSTOM,c);
if(sel)decorateOptions();
nId.value=''; nJoy.value=''; nLov.value=''; nHat.value=''; nNot.value='';
renderGuide();
setTimeout(()=>cont.scrollTo({top:cont.scrollHeight,behavior:'smooth'}),50);
});
af.append(nId,nSub,nJoy,nLov,nHat,nNot,aBtn,dupW);
addTd.append(wip,af); addRow.appendChild(addTd);
tbody.appendChild(addRow);
tbl.appendChild(tbody);
cont.appendChild(tbl);
const leg=mk('div'); leg.style.cssText='font-size:10px;color:#aaa;margin-top:6px';
leg.textContent=s('interactDataLegend');
cont.appendChild(leg);
};
renderGuide();
});
};
// ── Başlat ───────────────────────────────────────────────────────────────
decorateOptions();
buildUI();
applyFilter(curType);
};
// CHARACTER NOTE
const applyCharNote = () => {
if (!isOnDef(K.note, true)) return;
const m=location.href.match(/\/Character\/(\d+)/); if (!m) return;
const charBox=document.querySelector('.charPresBox'); if(!charBox||document.getElementById('tvis-notebar')) return;
const allNotes=gmGet(DK.NOTES,{})||{};
const bar=mk('div','tvis-notebar'); bar.id='tvis-notebar';
const ta=mk('textarea'); ta.placeholder=s('notePlh'); ta.value=allNotes[m[1]]||'';
const btn=mkB('💾','btn-g btn-sm',()=>{
const notes=gmGet(DK.NOTES,{})||{}; notes[m[1]]=ta.value; gmSet(DK.NOTES,notes);
btn.textContent='✅'; setTimeout(()=>btn.textContent='💾',1500);
});
bar.append(mk('span','','📝 '),ta,btn);
charBox.insertAdjacentElement('afterend',bar);
};
// DIARY FILTER — searches all entries across all days
const applyDiaryFilter = () => {
if (!isOnDef(K.diary, true)||!location.href.includes('/Character/Diary')) return;
const nav=document.querySelector('[id$="_ddlNavigate"]')?.closest('div.box'); if(!nav||document.getElementById('tvis-df')) return;
const wrap=mk('div'); wrap.id='tvis-df'; wrap.style.marginBottom='10px';
wrap.innerHTML=`<h2>${s('dfTitle')}</h2><div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-top:6px"><input id="tvis-df-i" placeholder="${s('dfPlh')}" style="padding:5px 8px;border:1px solid #ccc;border-radius:4px;font-size:12px;width:220px"><button id="tvis-df-c" class="btn-grey btn-sm">${s('dfClear')}</button><span id="tvis-df-n" style="font-size:11px;color:#666"></span></div>`;
nav.insertAdjacentElement('beforebegin',wrap);
const inp=document.getElementById('tvis-df-i'), info=document.getElementById('tvis-df-n');
const allDays = () => document.querySelectorAll('#ppm-content .diaryExtraspace > li');
const allEntries = () => document.querySelectorAll('#ppm-content .diaryExtraspace > li > ul > li');
const filter=()=>{
const q=inp.value.trim().toLowerCase();
let cnt=0, total=0;
allDays().forEach(day=>{
let vis=false;
day.querySelectorAll(':scope > ul > li').forEach(e=>{
total++;
const ok=!q||e.textContent.toLowerCase().includes(q);
e.style.display=ok?'':'none';
if(ok){vis=true;cnt++;}
});
day.style.display=(!q||vis)?'':'none';
// show/hide day header (first non-ul element)
[...day.children].forEach(ch=>{ if(ch.tagName!=='UL') ch.style.display=(!q||vis)?'':'none'; });
});
if(q){
info.textContent=`${cnt} / ${total} ${s('dfCount')}`;
} else {
info.textContent=`${total} ${s('dfTotal')}`;
}
};
document.getElementById('tvis-df-c').onclick=()=>{
inp.value='';
allDays().forEach(day=>{
day.style.display='';
[...day.children].forEach(ch=>ch.style.display='');
});
allEntries().forEach(e=>e.style.display='');
filter(); // recalculate total
};
inp.addEventListener('input',filter);
inp.addEventListener('keyup',e=>{if(e.key==='Enter')filter();});
filter(); // show total on load
};
// ICON MENU
const openCustomIconsModal = () => mkModal(s('customIconTitle'), cont => {
const renderCustom = () => {
chipArea.innerHTML='';
getCustomIcons().forEach((ico,i) => {
const chip=mk('span','tvis-custom-chip',ico);
const del=mk('button','','×'); del.type='button'; del.title='Sil';
del.onclick=()=>{ const c=getCustomIcons(); c.splice(i,1); gmSet(DK.CUSTOM_ICONS,c); renderCustom(); };
chip.appendChild(del); chipArea.appendChild(chip);
});
};
const chipArea = mk('div','tvis-custom-icons'); cont.appendChild(chipArea);
renderCustom();
const icoInput=mk('input'); icoInput.placeholder=s('customIconPlh');
icoInput.style.cssText='width:100%;box-sizing:border-box;padding:3px 6px;border:1px solid #ccc;border-radius:3px;font-size:11px;margin:6px 0 3px';
cont.appendChild(icoInput);
const btnRow = mk('div'); btnRow.style.cssText='display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px';
btnRow.append(
mkB(s('addIcon'),'btn-sm btn-v',()=>{
const val=icoInput.value.trim(); if(!val) return;
const c=getCustomIcons();
[...val].forEach(ico=>{ if(ico.trim()&&!c.includes(ico)) c.push(ico); });
gmSet(DK.CUSTOM_ICONS,c); icoInput.value=''; renderCustom();
}),
mkB(s('delAllIcons'),'btn-sm btn-r',()=>{
if(!confirm(s('delAllIconsConfirm'))) return;
gmSet(DK.CUSTOM_ICONS,[]); renderCustom();
}),
mkB(s('exportIcons'),'btn-sm btn-b',()=>{
const icons=getCustomIcons();
if(!icons.length){ showToast(s('ciNoIcons')); return; }
navigator.clipboard?.writeText(icons.join('')).then(()=>showToast(s('ciCopied')));
})
);
cont.appendChild(btnRow);
const geteEl=mk('a','',s('emojiRef'));
geteEl.href='https://getemoji.com/'; geteEl.target='_blank';
geteEl.style.cssText='font-size:10px;color:#17a2b8;display:block;text-decoration:none';
cont.appendChild(geteEl);
//ShareMD-Emoji deposu
const archiveEl=mk('a','','🗂️ Emoji Archive');
archiveEl.href='https://share-md.com/view?id=3dc5b188-c715-4829-9270-59be829d2de3';
archiveEl.target='_blank';
archiveEl.style.cssText='font-size:10px;color:#17a2b8;display:block;text-decoration:none;margin-top:2px';
cont.appendChild(archiveEl);
});
// ── MODÜL: Para Giriş Formatlayıcı ──────────────────────────────────────────
// Etiket: applyMoneyFormatter
// Sayfalara uygula: her sayfada çalışır, input yoksa sessizce geçer
const applyMoneyFormatter = () => {
if (!isOnDef(K.moneyFmt, true)) return;
// Para input'larını tanımlayan seçiciler — genişletilebilir
const SEL = [
'input[id*="txtPriceTag"]',
'input[id*="txtAmount"]',
'input[id*="txtPrice"]',
'input[id*="txtMoney"]',
'input[id*="txtCash"]',
'input[id*="txtGive"]',
'input[id*="txtPT"]',
'input[id*="txtWithdrawAmount"]',
'input[id*="txtDepositAmount"]',
].join(',');
const fmt = v => v.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
const raw = v => v.replace(/\./g, '');
const applyTo = inp => {
if (inp.dataset.mfmt) return;
inp.dataset.mfmt = '1';
// Cursor pozisyonunu koruyarak formatla
inp.addEventListener('input', () => {
const start = inp.selectionStart;
const oldLen = inp.value.length;
const digits = raw(inp.value).replace(/\D/g, '');
const newVal = fmt(digits);
inp.value = newVal;
const shift = newVal.length - oldLen;
inp.setSelectionRange(start + shift, start + shift);
});
// Form gönderilmeden önce noktaları temizle
const strip = () => { inp.value = raw(inp.value); };
inp.closest('form')?.addEventListener('submit', strip);
// ASP.NET postback butonları için de temizle
inp.closest('form')?.querySelectorAll(
'input[type=submit], input[type=image], button[type=submit]'
).forEach(btn => btn.addEventListener('click', strip));
// Mevcut değeri formatla
const init = raw(inp.value || '').replace(/\D/g, '');
if (init) inp.value = fmt(init);
};
document.querySelectorAll(SEL).forEach(applyTo);
// ASP.NET UpdatePanel sonrası yeniden tara
const obs = new MutationObserver(() =>
document.querySelectorAll(SEL).forEach(applyTo)
);
obs.observe(document.body, { childList: true, subtree: true });
};
// ── MODÜL: Forum Ekleme Butonları (Raf entegreli) ────────────────────────────
const applyForumList = () => {
if (!isOnDef(K.pins, true)) return;
if (!location.href.includes('/Forum/') && !location.href.includes('/Thread/')) return;
const parseId = url => url.match(/\/Thread\/(\d+)/)?.[1];
const injectAddButtons = () => {
document.querySelectorAll('table.data td a[href*="/Thread/"]').forEach(link => {
if (link.dataset.frlIa) return;
link.dataset.frlIa='1';
const id=parseId(link.href); if(!id) return;
const tracked=()=>isForumInRaf(link.href);
const btn=mk('button','frl-addbtn'+(tracked()?' on':''),tracked()?'📋':'⊕');
btn.type='button';
btn.onclick=()=>{
if(tracked()){openRaf();return;}
// Show lane picker popup
const shelf=getShelf(); const fd=shelf.folders['forum'];
const pop=mk('div');
pop.style.cssText='position:fixed;z-index:999999;background:#fff;border:1px solid #c9b8f0;border-radius:6px;padding:6px;box-shadow:0 4px 12px rgba(0,0,0,.2);display:flex;flex-direction:column;gap:3px;font-size:11px';
const rect=btn.getBoundingClientRect();
pop.style.top=(rect.bottom+4)+'px'; pop.style.left=rect.left+'px';
fd.columns.forEach((col,ci)=>{
const row=mkB(`${col.name} (${col.items.length})`,'btn-sm btn-grey',()=>{
addForumToRaf(id,link.textContent.trim(),link.href,ci);
btn.textContent='📋'; btn.className='frl-addbtn on'; pop.remove();
});
row.style.cssText='width:100%;text-align:left'; pop.appendChild(row);
});
document.body.appendChild(pop);
setTimeout(()=>{const close=e=>{if(!pop.contains(e.target)){pop.remove();document.removeEventListener('click',close);}};document.addEventListener('click',close);},50);
};
link.insertAdjacentElement('afterend',btn);
});
};
injectAddButtons();
// MutationObserver for dynamic content
const obs=new MutationObserver(injectAddButtons);
obs.observe(document.body,{childList:true,subtree:true});
setTimeout(()=>obs.disconnect(),5000);
return {};
};
// ────────────────────────────────────────────────────────────────────────────
// GENRE POPULARITY POPUP
const applyGenrePopup = () => {
if (!guard(K.genrePopup)) return;
if (!location.href.includes('/Character/SongToArtist/')) return;
const content = document.getElementById('ppm-content') || document.querySelector('.content');
if (!content) return;
const h1 = content.querySelector('h1');
if (!h1) return;
const link = mk('a', '', s('genrePopupBtn'));
link.href = '#';
link.style.cssText = 'display:inline-block;margin:6px 0 10px;font-size:12px;color:#6f42c1;font-weight:bold;text-decoration:none;padding:3px 10px;background:#f5f0ff;border:1px solid #c9b8f0;border-radius:4px';
link.onclick = e => {
e.preventDefault();
mkModal(s('genrePopupTitle'), cont => {
cont.style.cssText = 'padding:4px 0 0';
const iframe = document.createElement('iframe');
iframe.className = 'tvis-gpop-iframe';
iframe.src = `${PM}/Charts/GenrePopularity`;
cont.appendChild(iframe);
// widen box after paint
requestAnimationFrame(() => {
const box = cont.closest('.tvis-box');
if (box) box.style.maxWidth = 'min(95vw, 1100px)';
});
});
};
h1.insertAdjacentElement('afterend', link);
};
// SERENADE HELPER
const applySerenadeHelper = () => {
if (!guard(K.serenadeHelper)) return;
if (!location.href.includes('/Locale/RestaurantOrderSerenade/')) return;
const RADIO_STATIONS = [
{ id: 3, prefix: '[ROCK]' }, { id: 4, prefix: '[MR]' },
{ id: 5, prefix: '[HM]' }, { id: 6, prefix: '[PUNK]' },
{ id: 7, prefix: '[ELEC]' }, { id: 8, prefix: '[POP]' },
{ id: 9, prefix: '[HH]' }, { id: 10, prefix: '[R&B]' },
{ id: 11, prefix: '[REG]' }, { id: 12, prefix: '[WM]' },
{ id: 13, prefix: '[C&W]' }, { id: 14, prefix: '[JAZZ]' },
{ id: 15, prefix: '[BLU]' }, { id: 16, prefix: '[CLAS]' },
{ id: 17, prefix: '[LAT]' }, { id: 18, prefix: '[AM]' },
{ id: 19, prefix: '[FLA]' },
];
// Next Wednesday 10:00 CET (= 09:00 UTC)
const nextWedCET = fromMs => {
const d = new Date(fromMs);
const dow = d.getUTCDay(); // 0=Sun, 3=Wed
let days = (3 - dow + 7) % 7;
if (days === 0 && d.getUTCHours() >= 9) days = 7;
const nw = new Date(d);
nw.setUTCDate(d.getUTCDate() + days);
nw.setUTCHours(9, 0, 0, 0);
return nw.getTime();
};
const isCacheValid = ts => !!ts && Date.now() < nextWedCET(ts);
const fmtDt = ms => new Date(ms).toLocaleString(dateLocale, {
weekday:'short', month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'
});
// Inject prefixes into serenade dropdown
const injectPrefixes = () => {
const ddl = document.getElementById('ctl00_cphLeftColumn_ctl00_ddlSerenade');
if (!ddl) return;
const cache = gmGet(DK.RADIO_CACHE, null);
if (!cache?.tracks) return;
[...ddl.options].forEach(opt => {
if (!opt.value || opt.value === '0') return;
const prefix = cache.tracks[String(opt.value)];
if (prefix && !opt.text.startsWith('[')) opt.text = `${prefix} ${opt.text}`;
});
};
// Fetch all 17 stations and build cache — sequential, radar-style
const doFetch = (onDone) => {
if (radioRunning) return;
radioRunning = true; radioCancelled = false;
const total = RADIO_STATIONS.length;
let done = 0;
showRadioProgress(0, total);
(async () => {
try {
// Base GET — extract ViewState once
await waitGap(); lastFetch = Date.now();
const baseResp = await fetch(`${PM}/Charts/Radio/`);
const baseDoc = new DOMParser().parseFromString(await baseResp.text(), 'text/html');
const gf = name => baseDoc.querySelector(`[name="${name}"]`)?.value || '';
const vs = gf('__VIEWSTATE'), vsg = gf('__VIEWSTATEGENERATOR'), ev = gf('__EVENTVALIDATION');
const tracks = {};
for (let i = 0; i < RADIO_STATIONS.length; i++) {
if (radioCancelled) break;
// Random delay 2–6s between requests (same as radar bulk)
if (i > 0) await new Promise(r => setTimeout(r, 2100 + Math.random() * 2900));
if (radioCancelled) break;
showRadioProgress(i + 1, total);
await waitGap(); lastFetch = Date.now();
try {
const { id, prefix } = RADIO_STATIONS[i];
const body = new URLSearchParams({
'__EVENTTARGET': 'ctl00$cphLeftColumn$ctl00$ddlRadioStations',
'__EVENTARGUMENT': '',
'__VIEWSTATE': vs, '__VIEWSTATEGENERATOR': vsg, '__EVENTVALIDATION': ev,
'ctl00$cphLeftColumn$ctl00$ddlRadioStations': String(id),
'ctl00$cphLeftColumn$ctl00$ddlWeeksIntoThePast': '0',
'ctl00$cphLeftColumn$ctl00$ddlCountries': '2',
});
const resp = await fetch(`${PM}/Charts/Radio/`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
const doc = new DOMParser().parseFromString(await resp.text(), 'text/html');
const tid = doc.querySelector('#tablechart tbody tr:first-child td:nth-child(2) a')
?.href?.match(/Track\/(\d+)/)?.[1];
if (tid) tracks[String(tid)] = prefix;
done++;
} catch { /* skip station */ }
}
if (!radioCancelled) gmSet(DK.RADIO_CACHE, { ts: Date.now(), tracks });
} catch { /* base fetch failed */ }
radioRunning = false;
hideRadioProgress(radioCancelled, done, total, () => {
injectPrefixes();
// Re-render panel if open
const modal = document.getElementById('tvis-modal');
if (modal) onDone?.();
});
})();
};
// Open helper modal
let warnState = 0;
const openHelper = () => {
mkModal(s('shTitle'), cont => {
cont.style.minWidth = '320px';
warnState = 0;
const render = () => {
cont.innerHTML = '';
const cache = gmGet(DK.RADIO_CACHE, null);
const ddl = document.getElementById('ctl00_cphLeftColumn_ctl00_ddlSerenade');
const tracks = cache?.tracks || {};
// Build matched list from serenade dropdown
const matched = [];
if (ddl) {
[...ddl.options].forEach(opt => {
if (!opt.value || opt.value === '0') return;
const prefix = tracks[String(opt.value)];
if (prefix) {
const txt = opt.text.replace(/^\[.*?\]\s*/, '');
matched.push({ prefix, txt });
}
});
} else {
// Fallback: show all cached prefixes without labels
RADIO_STATIONS.forEach(({ prefix }) => {
const tid = Object.keys(tracks).find(k => tracks[k] === prefix);
if (tid) matched.push({ prefix, txt: tid });
});
}
// List
const list = mk('div', 'tvis-sh-list');
if (!matched.length) {
list.appendChild(mk('div', 'tvis-sh-empty',
Object.keys(tracks).length ? s('shNoneMatched') : s('shNoCache')));
} else {
matched.forEach(({ prefix, txt }) => {
const row = mk('div', 'tvis-sh-row');
row.append(mk('span', 'tvis-sh-prefix', prefix), mk('span', 'tvis-sh-track', txt));
list.appendChild(row);
});
}
cont.appendChild(list);
// Footer
const footer = mk('div', 'tvis-sh-footer');
if (cache?.ts) {
footer.appendChild(mk('div', '', `${s('shLastUpd')} ${fmtDt(cache.ts)}`));
footer.appendChild(mk('div', '', `${s('shNextUpd')} ${fmtDt(nextWedCET(cache.ts))}`));
}
const warnEl = mk('div', 'tvis-sh-warn');
footer.append(warnEl);
// Buttons
const btnRow = mk('div'); btnRow.style.cssText = 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:2px';
const updBtn = mkB(s('shUpdateBtn'), 'btn-sm btn-v', async () => {
if (radioRunning) return;
if (isCacheValid(cache?.ts)) {
if (warnState === 0) {
warnState = 1;
warnEl.textContent = s('shWarn1').replace('{date}', fmtDt(cache.ts));
return;
}
if (warnState === 1) {
warnState = 2;
warnEl.textContent = s('shWarn2');
return;
}
}
warnEl.textContent = '';
doFetch(render);
});
const radioLink = mk('a', '', s('shRadioLink'));
radioLink.href = `${PM}/Charts/Radio/`;
radioLink.style.cssText = 'font-size:11px;color:#6f42c1;text-decoration:none';
radioLink.target = '_blank';
btnRow.append(updBtn, radioLink);
footer.appendChild(btnRow);
cont.appendChild(footer);
};
render();
});
};
// Insert button before the .box
const box = document.querySelector('.box');
if (box) {
const btn = mkB(s('shBtn'), 'btn-sm btn-v', openHelper);
btn.style.cssText = 'display:block;margin:6px 0 8px';
box.insertAdjacentElement('beforebegin', btn);
}
injectPrefixes();
};
// ────────────────────────────────────────────────────────────────────────────
// ────────────────────────────────────────────────────────────────────────────
// SPEED CALLING
const SC_DEFAULT_CFG = {
interval: 3,
phoneIds: {
arkadaş: [24, 61, 121],
romantik: [25, 73, 74],
}
};
const getScCfg = () => Object.assign({}, SC_DEFAULT_CFG, gmGet(DK.SPEEDCALL_CFG, {}));
const getScQueue = () => gmGet(DK.SPEEDCALL_Q, []) || [];
const setScQueue = arr => gmSet(DK.SPEEDCALL_Q, arr);
const getScState = () => gmGet(DK.SPEEDCALL_STATE, {idx:0, running:false, failStreak:0}) || {idx:0,running:false,failStreak:0};
const setScState = st => gmSet(DK.SPEEDCALL_STATE, st);
// Build queue from saved interact types (arkadaş + romantik)
const buildScQueue = (includeTypes) => {
const queue = [];
const seen = new Set();
// Scan all interact type keys stored in GM
// We iterate known radar chars + try to find any saved interact type
// Build from: all tracked chars + any charId key in GM storage
const allTracked = getAllTracked().map(e => String(e.charId));
// Also read interact custom list if present
const custom = gmGet(DK.INTERACT_CUSTOM, {}) || {};
// Collect unique charIds from radar
const candidates = [...new Set(allTracked)];
candidates.forEach(cid => {
const t = gmGet(DK.INTERACT_TYPE + cid, null);
if (t && includeTypes.includes(t) && !seen.has(cid)) {
seen.add(cid);
// Try to get name from char cache
const cache = gmGet(DK.CACHE, {}) || {};
const name = cache[cid]?.name || cid;
queue.push({ charId: cid, type: t, name });
}
});
return queue;
};
// Modal: configure & start
const openSpeedCallModal = () => {
mkModal(s('scTitle'), (cont, close) => {
const cfg = getScCfg();
const queue = getScQueue();
const state = getScState();
// ── WHO TO CALL ──
cont.appendChild(mk('div','tvis-sec',s('scWho')));
const chkFriend = Object.assign(mk('input'),{type:'checkbox',checked:true,id:'sc-chk-friend'});
const chkRom = Object.assign(mk('input'),{type:'checkbox',checked:true,id:'sc-chk-rom'});
const lF = mk('label','tvis-chk'); lF.append(chkFriend, mk('span','',s('scWhoFriend'))); cont.appendChild(lF);
const lR = mk('label','tvis-chk'); lR.append(chkRom, mk('span','',s('scWhoRom'))); cont.appendChild(lR);
// Preview list
const listWrap = mk('div','tvis-sc-charlist');
const refreshList = () => {
listWrap.innerHTML = '';
const inc = [];
if (chkFriend.checked) inc.push('arkadaş');
if (chkRom.checked) inc.push('romantik');
const chars = buildScQueue(inc);
if (!chars.length) {
listWrap.appendChild(mk('div','',s('scNoChars'))).style.cssText='font-size:11px;color:#999;padding:8px';
return;
}
chars.forEach(c => {
const row = mk('div','tvis-sc-charrow');
const chk = Object.assign(mk('input'),{type:'checkbox',checked:true});
chk.dataset.cid = c.charId;
chk.dataset.type = c.type;
chk.dataset.name = c.name;
const badge = mk('span','tvis-sc-badge '+(c.type==='romantik'?'r':'a'), c.type==='romantik'?'❤️':'👥');
const nm = mk('span','',c.name);
row.append(chk, badge, nm);
listWrap.appendChild(row);
});
};
chkFriend.onchange = refreshList;
chkRom.onchange = refreshList;
refreshList();
cont.appendChild(listWrap);
cont.appendChild(mk('hr','tvis-hr'));
// ── INTERVAL ──
cont.appendChild(mk('div','tvis-sec',s('scInterval')));
const intRow = mk('div'); intRow.style.cssText='display:flex;align-items:center;gap:8px;margin-bottom:4px';
const slider = Object.assign(mk('input'),{type:'range',min:1,max:10,value:cfg.interval});
slider.style.cssText='flex:1';
const intLbl = mk('span','',cfg.interval+'s');
intLbl.style.cssText='min-width:24px;font-weight:700;color:#6f42c1';
slider.oninput = () => { intLbl.textContent = slider.value+'s'; };
intRow.append(slider, intLbl);
cont.appendChild(intRow);
cont.appendChild(mk('div','',s('scIntervalNote'))).style.cssText='font-size:10px;color:#aaa;margin-bottom:8px';
cont.appendChild(mk('hr','tvis-hr'));
// ── PHONE IDs ──
cont.appendChild(mk('div','tvis-sec',s('scPhoneIds')));
cont.appendChild(mk('div','',s('scPhoneNote'))).style.cssText='font-size:10px;color:#aaa;margin-bottom:6px';
const makePhoneRow = (labelKey, typeKey) => {
const row = mk('div','tvis-sc-phonerow');
row.appendChild(mk('span','',s(labelKey))).style.cssText='min-width:110px;flex-shrink:0;font-size:11px';
const inp = mk('input','tvis-sc-phoneinp');
inp.value = (cfg.phoneIds[typeKey] || []).join(', ');
inp.dataset.sctype = typeKey;
row.appendChild(inp);
cont.appendChild(row);
return inp;
};
const inpFriend = makePhoneRow('scFriendIds','arkadaş');
const inpRom = makePhoneRow('scRomIds','romantik');
cont.appendChild(mk('hr','tvis-hr'));
// ── BUTTONS ──
const btnRow = mk('div'); btnRow.style.cssText='display:flex;gap:6px;flex-wrap:wrap;margin-top:4px';
const doStart = (resume) => {
// Save config
const newCfg = {
interval: parseInt(slider.value)||3,
phoneIds: {
arkadaş: inpFriend.value.split(',').map(x=>parseInt(x.trim())).filter(n=>!isNaN(n)),
romantik: inpRom.value.split(',').map(x=>parseInt(x.trim())).filter(n=>!isNaN(n)),
}
};
gmSet(DK.SPEEDCALL_CFG, newCfg);
if (!resume) {
// Build queue from checked chars
const checked = [...listWrap.querySelectorAll('input[type=checkbox]:checked')];
if (!checked.length) return;
const q = checked.map(c=>({charId:c.dataset.cid, type:c.dataset.type, name:c.dataset.name}));
setScQueue(q);
setScState({idx:0, running:true, failStreak:0});
} else {
const st = getScState();
setScState({...st, running:true, failStreak:0});
}
close();
const q = getScQueue();
const st = getScState();
const first = q[st.idx];
if (!first) return;
window.open(`${PM}/Interact/Phone/${first.charId}#ppmCall`, '_blank');
};
btnRow.appendChild(mkB(s('scStart'),'btn-g',()=>doStart(false)));
if (queue.length && state.idx > 0 && state.idx < queue.length) {
const resumeBtn = mkB(`${s('scResume')} (${state.idx}/${queue.length})`,'btn-v',()=>doStart(true));
btnRow.appendChild(resumeBtn);
}
btnRow.appendChild(mkB(s('scReset'),'btn-grey btn-sm',()=>{
if (!confirm(s('scReset')+'?')) return;
gmDel(DK.SPEEDCALL_Q); gmDel(DK.SPEEDCALL_STATE);
close();
}));
cont.appendChild(btnRow);
});
};
// Injected into every Interact/Phone page when #ppmCall is in URL
const applySpeedCall = () => {
if (!location.href.includes('/Interact/Phone/') && !location.href.includes('/Interact/Details/')) return;
if (!location.hash.includes('ppmCall')) return;
const state = getScState();
if (!state.running) return;
const queue = getScQueue();
const cfg = getScCfg();
const idx = state.idx;
const total = queue.length;
const current = queue[idx];
if (!current) {
// Done
setScState({...state, running:false});
injectScBar(null, idx, total, cfg, false, true);
return;
}
// ── BAR ──
let timerHandle = null;
let paused = false;
let timerLeft = 0;
const bar = mk('div','tvis-sc-bar');
bar.id = 'tvis-sc-bar';
const statEl = mk('span','tvis-sc-stat');
const timerEl = mk('span','tvis-sc-timer','');
const stopBtn = mkB(s('scBarStop'),'tvis-sc-btn stop',()=>{
paused = true;
clearTimeout(timerHandle);
setScState({...getScState(), running:false});
stopBtn.style.display='none';
resumeBtn.style.display='';
warnEl.textContent='';
statEl.innerHTML=`<span style="color:#ffd700">⏸ Duraklatıldı</span>`;
timerEl.textContent='';
});
const resumeBtn = mkB(s('scBarResume'),'tvis-sc-btn resume',()=>{
setScState({...getScState(), running:true, failStreak:0});
resumeBtn.style.display='none';
stopBtn.style.display='';
warnEl.textContent='';
proceedToNext();
});
resumeBtn.style.display='none';
const warnEl = mk('span','tvis-sc-warn','');
warnEl.style.display='none';
bar.append(statEl, timerEl, warnEl, stopBtn, resumeBtn);
document.body.prepend(bar);
// Push page content down
document.body.style.paddingTop = '34px';
const updateStat = (name, nextName) => {
statEl.innerHTML = `📞 <b>${idx+1}${s('scBarOf')}${total}</b> `
+ `<span class="tvis-sc-name">${name||current.charId}</span>`
+ (nextName ? ` › ${s('scBarNext')} <span class="tvis-sc-name">${nextName}</span>` : '');
};
const showWarn = (msg) => {
warnEl.textContent = msg;
warnEl.style.display='';
};
const hideWarn = () => { warnEl.style.display='none'; warnEl.textContent=''; };
const playAlarm = () => {
try {
const ctx = new (window.AudioContext||window.webkitAudioContext)();
[0,300,600].forEach(delay => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain); gain.connect(ctx.destination);
osc.frequency.value = 880;
gain.gain.setValueAtTime(0.4, ctx.currentTime + delay/1000);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + delay/1000 + 0.4);
osc.start(ctx.currentTime + delay/1000);
osc.stop(ctx.currentTime + delay/1000 + 0.4);
});
} catch(e) {}
};
const runCountdown = (secs, onDone) => {
timerLeft = secs;
timerEl.textContent = timerLeft + 's';
const tick = () => {
timerLeft--;
timerEl.textContent = timerLeft + 's';
if (timerLeft <= 0) { timerEl.textContent=''; onDone(); }
else timerHandle = setTimeout(tick, 1000);
};
timerHandle = setTimeout(tick, 1000);
};
const proceedToNext = () => {
const nextIdx = getScState().idx + 1;
if (nextIdx >= total) {
setScState({...getScState(), running:false, idx:nextIdx});
statEl.innerHTML = `✅ <b>${s('scBarDone')}</b>`;
timerEl.textContent='';
stopBtn.style.display='none';
return;
}
setScState({...getScState(), idx:nextIdx});
const next = queue[nextIdx];
const interval = cfg.interval + Math.random()*2;
const nextAfter = queue[nextIdx+1];
updateStat(next.name, nextAfter?.name);
runCountdown(Math.round(interval), () => {
location.assign(`${PM}/Interact/Phone/${next.charId}#ppmCall`);
});
};
const doFailStreak = () => {
const st = getScState();
const streak = (st.failStreak||0) + 1;
setScState({...st, failStreak:streak});
if (streak >= 2) {
clearTimeout(timerHandle);
paused = true;
playAlarm();
stopBtn.style.display='none';
resumeBtn.style.display='';
showWarn(`${s('scBarFail')} — ${s('scBarFailNote')}`);
timerEl.textContent='';
} else {
proceedToNext();
}
};
// ── ATTEMPT CALL ──
const sel = document.querySelector('#ctl00_cphTopColumn_ctl00_ddlInteractionTypes')
|| document.querySelector('select[id*="ddlInteraction"]');
const phoneIds = (cfg.phoneIds[current.type] || []).map(String);
const btn = document.querySelector('#ctl00_cphTopColumn_ctl00_btnInteract')
|| document.querySelector('input[id*="btnInteract"],button[id*="btnInteract"]');
const next1 = queue[idx+1];
updateStat(current.name, next1?.name);
let called = false;
if (sel && phoneIds.length) {
for (const pid of phoneIds) {
const opt = [...sel.options].find(o=>o.value===pid&&o.style.display!=='none');
if (opt) {
sel.value = pid;
sel.dispatchEvent(new Event('change',{bubbles:true}));
if (btn) {
setTimeout(()=>{
btn.click();
called = true;
setScState({...getScState(), failStreak:0});
const interval = cfg.interval + Math.random()*2;
runCountdown(Math.round(interval), proceedToNext);
}, 400);
}
break;
}
}
}
if (!called) {
if (!sel) {
// No interact dropdown found — might be wrong page
showWarn(s('scBarNoId'));
} else if (!phoneIds.length) {
showWarn(s('scBarNoId'));
} else {
showWarn(s('scBarNoId'));
}
setTimeout(() => doFailStreak(), 600);
}
};
// ── SETTINGS MODAL (used by bar link and PopControl button) ──────────────────
function _openSocialModal(pan) {
const existing = document.getElementById('tvis-set-ov');
if (existing) { existing.remove(); return; }
const ov = document.createElement('div'); ov.id = 'tvis-set-ov';
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99997;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box';
const origStyle = pan.getAttribute('data-orig-style') || '';
pan.style.cssText = 'display:block;position:relative;top:auto;right:auto;left:auto;max-height:85vh;overflow-y:auto;border-radius:10px;min-width:260px;max-width:500px;width:92%;';
ov.appendChild(pan);
ov.onclick = function(e) {
if (e.target === ov) {
document.body.appendChild(pan);
pan.style.display = 'none';
ov.remove();
}
};
document.body.appendChild(ov);
}
function _openSocialSettingsModal() {
const pan = document.getElementById('tvis-hpanel');
if (!pan) return;
const ov = document.getElementById('tvis-set-ov');
if (ov) { document.body.appendChild(pan); pan.style.display = 'none'; ov.remove(); return; }
_openSocialModal(pan);
}
// EN strings for PopControl export
window.__ppcStrSocial = {"menuTitle": "🌐 Social", "menuClip": "📋 Clipboard", "menuRadar": "📍 Radar", "save": "✔ Save", "close": "Close", "langLabel": "Language", "backup": "📤 Backup", "restore": "📥 Restore", "restoreErr": "Invalid file.", "restoreQ": "What to do with current data?", "mergeLbl": "Merge", "replaceLbl": "Replace", "cancelLbl": "Cancel", "pins": "📌 Shelf — Store artist, locale, city, forum & favourite links in organized shelves", "radar": "📍 Radar — Track 200 characters on 10 tabs", "interactfilter": "👋 Interaction Guide — Show joy/romance/hatred values, filter by type", "quickLinks": "🔗 Quick Links — Add message/go/send money buttons to character & locale pages", "note": "📝 Character Note — Persistent personal note field on every profile", "diary": "🔍 Diary Filter — Real-time search and filter for diary entries", "moneyFmt": "💰 Money Formatter — Automatically format large amounts: 1,500,000", "addIcon": "+ Add Icon", "customIconPlh": "Paste emoji...", "customIconTitle": "Custom Icons", "delAllIcons": "🗑 Delete All", "delAllIconsConfirm": "Delete all custom icons?", "exportIcons": "📤 Export", "emojiRef": "🔗 Find icons: getemoji.com", "pinBtn": "📌 Shelf", "rafForum": "Forum", "rafArtists": "Artists", "rafLocales": "Locales", "rafCities": "Cities", "rafFav": "Favorites", "rafChars": "Characters", "rafWork": "Work & Studio", "rafGoals": "Goals", "rafColDefault": "Lane", "rafEmpty": "Folder is empty.", "rafEditFolder": "Edit Folder", "rafEditCol": "Edit Column", "rafItemNote": "Note...", "rafDup": "This URL already added!", "colorPicker": "Color Picker", "scAskSC": "Add to Speed Calling list?", "yes": "Yes", "ppEmpty": "No pins yet.", "ppDup": "This page is already pinned!", "pinAdd": "📌 Add New Pin", "pinSave": "📌 Pin", "pinName": "Name:", "pinNote": "Note:", "pinIcon": "Icon", "chTitle": "📋 Clipboard History", "chEmpty": "No IDs copied yet.", "chClear": "Clear All", "chCopy": "Copy", "iaEdit": "🔗 Edit Quick Links", "iaDefaults": "↩ Reset to Defaults", "iaAddSec": "+ New Link", "iaUrlPlh": "Paste URL...", "iaUrlInfo": "Path to save:", "iaLblPlh": "Button name...", "iaSave": "Save", "dfTitle": "🔍 Diary Filter", "dfPlh": "Search...", "dfClear": "Clear", "dfCount": "matches", "dfTotal": "entries", "notePlh": "Note for this character...", "cpLoading": "⏳ Loading...", "cpTracked": "⭐ On Radar", "cpOffline": "Offline", "cpAttitude": "Attitude", "cpState": "State", "cpRefresh": "🔄 Update Manually", "tkTitle": "📍 Tracking Station", "tkEmpty": "Radar is empty.", "tkConfirmRm": "Remove from radar?", "tkLastSeen": "Last Seen:", "tkLastUpd": "", "tkConfirmOk": "✔ Confirm", "tkNote": "Note...", "tkBgDone": "📍 Radar updated", "tkBgStopped": "⛔ Update stopped", "tkBgRunning": "📍 Updating radar...", "tkBgCancel": "Stop", "tkUpdate": "🔄 Update Page", "tkTabEdit": "Edit Tab", "tkGoTo": "Go To", "tkPageSelect": "Which page to add to?", "tkPageMove": "Which page to move to?", "tkPageFull": "This page is full!", "tkRemove": "⭐ Remove from Radar", "tkMove": "↕ Move to Page", "plDesc": "What to do?", "plDescPlh": "Enter task...", "plSchedule": "SCHEDULE", "plTabDef": "Tab", "plNoTasks": "No tasks.", "interactFilter": "Relationship Type:", "interactAll": "🌐 Standard", "interactFriend": "👥 Friendship", "interactRomantic": "💕 Romance", "interactHate": "😡 Hatred", "interactGuide": "ℹ️ Guide", "interactGuideTitle": "Interaction Options Guide", "interactJealous": "💘 Jealousy:", "interactJealousYes": "Will be jealous", "interactWarn": "⚠️ Romance 70+ — High jealousy risk!", "interactColJoy": "Joy", "interactColLove": "Romance", "interactColHate": "Hatred", "interactColNote": "Condition", "interactSave": "Save", "interactType": "Type", "interactName": "Option", "interactDataEdit": "📊 Edit Interact Data", "interactDataId": "Option ID", "interactDataAdd": "+ Add", "interactDataEmpty": "No custom data.", "interactDataDup": "This ID already exists!", "interactDataReset": "🗑 Delete All", "interactDataResetQ": "Delete all custom data?", "badgeMoved": "📍 MOVED", "badgeUpdated": "✎ Updated", "badgeOnline": "● Online", "tcWarnMoved": "⚠️ Previous:", "tcWarnUpdated": "✎ Attitude/status changed", "tcFooterHint": "⠿ Drag & sort · ✏️ Edit tab", "tcPage": "Page", "tcTotal": "Total", "cash": "Cash", "ciNoIcons": "No icons!", "ciCopied": "📋 Icons copied!", "iaResetConfirm": "Reset to defaults?", "genrePopupBtn": "🎼 View Genre Popularity", "genrePopupTitle": "🎼 Genre Popularity", "shBtn": "🎵 Serenade Helper", "shTitle": "🎵 Serenade Helper", "shNoCache": "No cache — press update.", "shLastUpd": "Last update:", "shNextUpd": "Next update:", "shUpdateBtn": "🔄 Update", "shRadioLink": "📻 Radio Charts", "shWarn2": "Is an update really necessary? Press again.", "shFetching": "📻 Fetching station: {n}/17", "shDone": "✅ Update complete!", "shErr": "❌ An error occurred.", "shNoneMatched": "No songs matched the radio list at this restaurant.", "serenadeHelper": "🎵 Serenade Helper — Radio prefixes & cache on serenade page", "speedcall": "📞 Speed Calling — Auto-call friends & romantics in sequence; adjustable interval and call options", "folderTitle": "Title", "recentTab": "🕐 Recently Added", "recentEmpty": "No saved items yet.", "recentCount": "Last {n} added", "goToFolder": "Go to folder", "colLabel": "Column:", "addWhere": "Where to add?", "deleteEntry": "Delete this entry?", "interactDataLegend": "✓ = On this page · Faded = Guide only · 🟢 Background = User added", "scBtn": "📞 Call", "scTitle": "📞 Speed Calling", "scInterval": "Interval (sec):", "scIntervalNote": "+ 0-2s random added", "scPhoneNote": "Comma separated · Tried left to right · Leave empty if unknown", "scFriendIds": "Friendship calls:", "scRomIds": "Romantic calls:", "scWho": "Who to call?", "scWhoFriend": "Friends", "scWhoRom": "Romantics", "scStart": "▶ Start", "scResume": "▶ Resume", "scReset": "↺ Reset", "scNoChars": "No saved friend/romantic characters found.", "scBarCalling": "📞 Calling:", "scBarDone": "✅ All calls done!", "scBarStop": "Stop", "scBarResume": "Resume", "scBarSkip": "Skip", "scBarFail": "⚠️ 2 consecutive errors — Script cannot call!", "scBarFailNote": "Continue trying?", "scBarOf": "/", "scBarNext": "Next:", "scBarNoId": "No ID found — skipped"};
// PopControl hazır olana kadar bekler
function _waitPC(cb,n=0){if(unsafeWindow.PopControl){cb();return;}if(n<20)setTimeout(()=>_waitPC(cb,n+1),300);}
// ── POPCONTROL HELPER ────────────────────────────────────────────────────────
function _registerSocial(bar, pan) {
unsafeWindow.PopControl.register({
id: 'social', icon: '🌐', label: 'Social',
strings: window.__ppcStrSocial || {},
buttons: [
{ icon: '➕', label: s('pinBtn').replace('📌 ','').slice(0,5), onClick: () => quickRaf() },
{ icon: '📌', label: s('pinBtn').replace('📌 ','').slice(0,4), onClick: openRaf },
{ icon: '📋', label: s('menuClip').replace('📋 ','').slice(0,4), onClick: openClip },
{ icon: '📍', label: s('menuRadar').replace('📍 ','').slice(0,5), onClick: openTrackModal },
{ icon: '📞', label: s('scBtn').replace('📞 ','').slice(0,3), onClick: openSpeedCallModal },
{ icon: '🌐', label: 'Social', onClick: () => _openSocialSettingsModal() },
],
onUndo: () => { document.getElementById('tvis-set-ov')?.remove(); document.body.appendChild(pan); pan.style.display='none'; bar.style.display=''; },
});
bar.style.display = 'none';
}
// MENU
const injectMenu = () => {
if (document.getElementById('tvis-bar')) return;
const bar = mk('div','tvis-bar'); bar.id='tvis-bar';
const pan = mk('div','tvis-hpanel'); pan.id='tvis-hpanel';
const posBar = () => {
let offset = 4;
const helperBar = document.getElementById('tvip-bar');
if (helperBar) offset += helperBar.offsetWidth + 4;
bar.style.right = offset + 'px';
pan.style.right = offset + 'px';
};
const lnk = (txt,fn)=>{ const a=mk('a','',txt); a.href='#'; a.onclick=e=>{e.preventDefault();fn();}; return a; };
if (isOnDef(K.pins, true)) {
const pinW=mk('span'); pinW.style.cssText='display:inline-flex;align-items:center;gap:3px';
const addBtn=mk('button','','+ ');
addBtn.style.cssText='background:#6f42c1;color:#fff;border:none;border-radius:3px;cursor:pointer;font-size:10px;padding:1px 5px;font-weight:bold;line-height:1.4';
addBtn.onclick=e=>{e.preventDefault();quickRaf();};
pinW.append(addBtn, lnk(s('pinBtn'),openRaf));
bar.appendChild(pinW);
}
bar.appendChild(lnk(s('menuClip'), openClip));
if (isOnDef(K.tracking, true)) bar.appendChild(lnk(s('menuRadar'), openTrackModal));
if (isOnDef(K.speedcall, true)) bar.appendChild(lnk(s('scBtn'), openSpeedCallModal));
bar.appendChild(lnk(s('menuTitle'), _openSocialSettingsModal));
const checks = {};
const mkChk = (ck,lbl,defVal=true) => { const l=mk('label','tvis-chk'); const c=Object.assign(mk('input'),{type:'checkbox',checked:isOnDef(ck,defVal)}); checks[ck]=c; l.append(c,mk('span','',lbl)); pan.appendChild(l); };
const mkHr = () => pan.appendChild(mk('hr','tvis-hr'));
const mkSec = txt => pan.appendChild(mk('div','tvis-sec',txt));
mkHr();
mkSec('SOSYAL');
mkChk(K.pins, s('pins'), true);
mkChk(K.charPopup, s('charCard'), true);
mkChk(K.tracking, s('radar'), true);
mkChk(K.interact, s('interactfilter'), true);
mkChk(K.ia, s('quickLinks'), true);
mkChk(K.note, s('note'), true);
mkChk(K.speedcall, s('speedcall'), true);
mkHr();
mkSec('ARAÇLAR');
mkChk(K.diary, s('diary'), true);
mkChk(K.moneyFmt, s('moneyFmt'), true);
mkChk(K.genrePopup, s('genrePopup'), true);
mkChk(K.serenadeHelper, s('serenadeHelper'), true);
mkHr();
mkSec(s('langLabel'));
const langRow=mk('div','tvis-lang-row');
[['TR','🇹🇷 Türkçe'],['EN','🇬🇧 English'],['PT','🇧🇷 Português']].forEach(([code,label])=>{
const b=mk('button','tvis-lang-btn'+(LANG===code?' active':''),label);
b.onclick=()=>{CK.set('ppm_lang',code);location.reload();};
langRow.appendChild(b);
});
pan.appendChild(langRow);
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 readMeBtn = mk('a','btn-b btn-sm','📖 Beni Oku');
readMeBtn.href='https://rentry.org/SocialOku'; readMeBtn.target='_blank';
readMeBtn.style.textDecoration='none';
row1.append(
mkB(s('save'),'btn-g',()=>{ Object.entries(checks).forEach(([k,c])=>CK.set(k,c.checked?'1':'0')); location.reload(); }),
readMeBtn,
mkB(s('backup'),'btn-b btn-sm',()=>dbExport()),
mkB(s('restore'),'btn-grey btn-sm',()=>dbImport())
);
row2.append(
mkB(s('iaEdit'),'btn-sm btn-v',()=>openIAEdit()),
mkB(s('customIconTitle'),'btn-sm btn-grey',()=>openCustomIconsModal())
);
pan.append(row1,row2);
mkHr();
document.body.append(bar, pan);
posBar();
const _posObs = new MutationObserver(posBar);
_posObs.observe(document.body, { childList: true });
setTimeout(() => _posObs.disconnect(), 3000);
document.addEventListener('click', e => {
if (!bar.contains(e.target) && !pan.contains(e.target)) {
if (!document.getElementById('tvis-set-ov')) pan.style.display = 'none';
}
});
// Auto-connect to PopControl if available
_waitPC(() => _registerSocial(bar, pan));
};
// INIT
applyCharPopup();
applyForumList();
applyMoneyFormatter();
applyCharNote();
applyDiaryFilter();
applyInlineActions();
applyInteractHelper();
applyGenrePopup();
applySerenadeHelper();
applySpeedCall();
injectMenu();
} catch(e) { if (!e.message || !e.message.includes('__ppsm_device_block__')) throw e; }
})();