1/** 2 * Klon SavaÅları â panel (web) yüzü: liste, ayrıntı ve "Yeni savaÅ" modalı 3 * (mobil KlonSavasiView.swift'in panel karÅılıÄı; OYUN-MOTORU.md §10). 4 * 5 * İki klon bir konuda yarıÅır; turları sunucu kuyrukta üretir (LLM), hakem 6 * puanlar, izleyici oy verir, kapanıÅta sahiplere sezon puanı yazılır. Bu 7 * dosya yalnız GÃSTERİR ve üç eylemi (kabul / reddet / oy) sunucuya iletir; 8 * hiçbir kural burada hesaplanmaz â hakem aÄırlıÄı, oy kapıları, puan 9 * daÄılımı sunucudadır (services/oyun/klon_savasi.py). 10 * 11 * NEDEN AYRI DOSYA: lobi (oyun-view.js) bu modülü DİNAMİK ithal eder; buradaki 12 * bir parse hatası lobiyi â dolayısıyla paneli â düÅürmesin (panel-bos-ekran 13 * tuzaÄı). Aynı sebeple oyun-puan.js ile stil paylaÅılmaz: iki modülün 14 * yaÅam döngüsü baÄımsızdır, ortak yardımcı yalnız oyun-api.js'tedir. 15 * 16 * Sunucu sözleÅmesi (routes/oyun_routes.py; alanlar klon_savasi.gorunum): 17 * GET /savas/liste?durum= {battles[gorunum], open_votes[gorunum]} 18 * durum â pending|running|voting|judged|cancelled|expired (tek deÄer) 19 * GET /savas/{id} gorunum 20 * POST /savas/{id}/kabul gorunum (yalnız B'nin sahibi) 21 * POST /savas/{id}/reddet {ok} (iki sahip de; yalnız pending) 22 * POST /savas/{id}/oy {side} gorunum (sahip deÄil, status=voting, tek oy) 23 * GET /savas/formatlar?assistant_id=&rakip_assistant_id= {formats[{key,ad,aciklama,acik,neden}]} 24 * POST /savas {format, topic?, assistant_a?, assistant_b?|rakip_user_id?, is_public} gorunum 25 * GET /tercih {assistant_id,...} (varsayılan klonum) 26 * GET /api/assistants?scope=mine|directory klon seçenekleri (avatar_url çözümlü) 27 * 28 * gorunum: {id, format, format_ad, status, topic, lang, rounds, current_round, 29 * a|b: {assistant{id,name,avatar_url}, owner{id,name,handle,avatar_tone,avatar_url}}, 30 * transcript[{round, side, q?, text}], judge{a,b,reason,tutarsiz,hakem_yok?}|null (yalnız judged), 31 * votes{a,b}|null (yalnız judged), winner_side a|b|draw|null, points_awarded 0|1 (BAYRAK, miktar 32 * deÄil), my_vote, can_vote, is_mine, is_public, public_key, voting_ends_at, accept_deadline_at} 33 * 34 * Canlı hat: notification-center.js panel WS çerçevelerini window 'morfoz:game' 35 * olayı olarak yayar; burada game.battle / game.battle_round / game.battle_close 36 * dinlenir (sunucu _yay iki sahibe game.battle basar). Ãerçevede gövde olsa da 37 * REST ile tazelenir: gövde sahibe göre üretilir, izleyici için can_vote gibi 38 * alanlar yalnız kendi isteÄinde doÄrudur. 39 * 40 * KURALLAR: tarayıcı kutusu yok (confirm/prompt panelde susturulmuÅ â sayfa içi 41 * onay çipi); e-posta hiçbir yüzeyde görünmez (kullaniciAdi/kullaniciHandle); 42 * i18n anahtarları hep sabit literal; sunucu metni escapeHtml'den geçer; 43 * arayüz ikonları inline SVG (emoji CSS rengini dinlemez). 44 */ 45import { T, oyunApi, hataMetni, sayi } from './oyun-api.js'; 46import { api, escapeHtml, toast, kullaniciAdi, kullaniciHandle } from './api-client.js'; 47import { currentLang, onLanguageChange } from './i18n.js'; 48import { openUserSearchModal } from './user-picker.js'; 49 50// ---------------------------------------------------------------------- ortak 51 52/** Tek aktif ekran: dil deÄiÅince yeniden çizilir; yeni ekran açılınca öncekinin 53 * dinleyicisi kapatılır (oyun-puan.js ile aynı desen, ayrı kayıt). */ 54let _aktif = null; 55let _dilBagli = false; 56 57function _ekranAc(host, ciz, kapat) { 58 if (_aktif && _aktif.kapat) { try { _aktif.kapat(); } catch { /* sessiz */ } } 59 _aktif = { host, ciz, kapat }; 60 if (!_dilBagli) { 61 _dilBagli = true; 62 onLanguageChange(() => { if (_aktif && _aktif.host.isConnected) _aktif.ciz(); }); 63 } 64} 65 66function yerelAd() { return currentLang() === 'en' ? 'en-US' : 'tr-TR'; } 67 68/** Sunucu 'YYYY-MM-DD HH:MM:SS' (UTC) ya da ISO verir; ekran yerel saat basar. */ 69function tarihCoz(s) { 70 if (!s) return null; 71 const m = String(s).trim(); 72 const d = new Date(/[TZ]/.test(m) ? m : m.replace(' ', 'T') + 'Z'); 73 return isNaN(d.getTime()) ? null : d; 74} 75function tarih(s) { 76 const d = tarihCoz(s); 77 if (!d) return 'â'; 78 const o = { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }; 79 if (d.getFullYear() !== new Date().getFullYear()) o.year = 'numeric';
80 try { return d.toLocaleString(yerelAd(), o); } catch { return String(s); } 81} 82 83/** Panelin oturum sahibi (panel-app.js açılıÅta window.morfozMe yazar). Sahibi 84 * olduÄum tarafı (A mı B mi) bununla ayırt ederiz; gorunum.is_mine tarafı söylemez. */ 85function benimId() { 86 const v = window.morfozMe && window.morfozMe.id; 87 return v == null ? null : Number(v); 88} 89 90/** Sunucunun kiÅi kartı {id, name, handle, avatar_tone, avatar_url}; kullaniciAdi 91 * full_name bekliyor â name oraya taÅınır ki e-posta kuralı tek yerde kalsın. */ 92function kisi(u) { 93 if (!u) return null; 94 return { ...u, full_name: u.full_name || u.display_name || u.name || '' }; 95} 96function kisiAdi(u) { return u ? kullaniciAdi(kisi(u)) : 'â'; } 97 98/** Arayüz ikonları inline SVG. */ 99const IKON = { 100 geri: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>', 101 yenile: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/></svg>', 102 simsek: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></svg>', 103 simsekBuyuk: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 4 14h7l-1 8 9-12h-7z"/></svg>', 104 arti: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"/></svg>', 105 kapat: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>', 106 onay: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7"/></svg>', 107 paylas: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8h16v-8M12 3v13M7 8l5-5 5 5"/></svg>', 108 kupa: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 21h8M12 17v4M7 4h10v5a5 5 0 0 1-10 0z"/><path d="M7 6H4v2a3 3 0 0 0 3 3M17 6h3v2a3 3 0 0 1-3 3"/></svg>', 109 kisi: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>', 110 arama: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.35-4.35"/></svg>', 111 balon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 21l1.9-5.4A8 8 0 1 1 21 12z"/></svg>', 112 ok: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></svg>', 113}; 114 115/** Klon görseli: sunucu avatar_url çözer (call_avatar_ref â URL); yoksa baÅ harf. */ 116function klonAvatarHtml(a, boy) { 117 const ad = (a && a.name) || ''; 118 const bas = escapeHtml(String(ad).trim().slice(0, 1).toUpperCase() || '?'); 119 const st = `width:${boy}px;height:${boy}px;font-size:${Math.round(boy * 0.42)}px`; 120 if (a && a.avatar_url) { 121 return `<span class="os-avatar" style="${st}"><img src="${escapeHtml(a.avatar_url)}" alt="" loading="lazy"></span>`; 122 } 123 return `<span class="os-avatar" style="${st}">${bas}</span>`; 124} 125 126function basHtml(baslik, alt, ekHtml) { 127 return `<div class="os-head"> 128 <button type="button" class="os-geri" data-geri aria-label="${escapeHtml(T('action.back', 'Ger
128i'))}">${IKON.geri}</button> 129 <div class="os-head-metin"> 130 <h2 class="os-title">${escapeHtml(baslik)}</h2> 131 ${alt ? `<p class="os-sub">${escapeHtml(alt)}</p>` : ''} 132 </div> 133 ${ekHtml || ''} 134 </div>`; 135} 136 137function bosHtml(baslik, aciklama) { 138 return `<div class="os-bos">${IKON.simsekBuyuk}<div class="os-bos-baslik">${escapeHtml(baslik)}</div>` + 139 (aciklama ? `<div class="os-bos-alt">${escapeHtml(aciklama)}</div>` : '') + '</div>'; 140} 141 142function yukleniyorHtml() { 143 return `<div class="os-yukleniyor"><span class="os-spinner"></span>${escapeHtml(T('common.loading', 'Yükleniyorâ¦'))}</div>`; 144} 145 146function hataHtml(metin) { 147 return `<div class="os-hata"><div>${escapeHtml(metin)}</div> 148 <button type="button" class="os-btn os-btn-ghost os-btn-sm" data-yeniden>${IKON.yenile} ${escapeHtml(T('ui.oyun.savas.yeniden', 'Yeniden dene'))}</button></div>`; 149} 150 151/** Uzun süren eylem: düÄme kilitlenir ve dönen halka çıkar (metin yerinde kalır). */ 152function kilitle(btn, kilitli) { 153 if (!btn) return; 154 btn.disabled = !!kilitli; 155 btn.classList.toggle('os-busy', !!kilitli); 156} 157 158/** Sayfa içi onay çipi â tarayıcının onay kutusu panelde susturulmuÅ, akıŠ159 * sessizce ölürdü. Promise<boolean>; aynı kapta ikinci çip öncekini düÅürür. */ 160function onayIste(kap, metin, { tehlike = false } = {}) { 161 return new Promise((resolve) => { 162 const eski = kap.querySelector(':scope > .os-onay'); 163 if (eski) eski.remove(); 164 const cip = document.createElement('div'); 165 cip.className = 'os-onay'; 166 cip.innerHTML = `<span class="os-onay-metin">${escapeHtml(metin)}</span> 167 <button type="button" class="os-btn os-btn-sm ${tehlike ? 'os-btn-danger' : 'os-btn-primary'}" data-evet>${escapeHtml(T('action.yes', 'Evet'))}</button> 168 <button type="button" class="os-btn os-btn-sm os-btn-ghost" data-hayir>${escapeHtml(T('action.cancel', 'Vazgeç'))}</button>`; 169 kap.appendChild(cip); 170 // Ãip tıklanabilir bir satırın İÃİNDE olabilir (liste): Evet/Vazgeç satırı açmasın. 171 cip.addEventListener('click', (e) => e.stopPropagation()); 172 cip.addEventListener('keydown', (e) => e.stopPropagation()); 173 const bitir = (v) => { cip.remove(); resolve(v); }; 174 cip.querySelector('[data-evet]').addEventListener('click', () => bitir(true)); 175 cip.querySelector('[data-hayir]').addEventListener('click', () => bitir(false)); 176 setTimeout(() => cip.querySelector('[data-evet]').focus(), 20); 177 }); 178} 179
180function geriBagla(kok, geri) { 181 const b = kok.querySelector('[data-geri]'); 182 if (b) b.addEventListener('click', () => { if (typeof geri === 'function') geri(); }); 183} 184 185// Sabit eÅlemeler â anahtar dizesi HER ZAMAN literal (dinamik anahtar yasak). 186// Durum adları clone_battles.status (klon_savasi.py): pending|running|voting|judged|cancelled|expired. 187function savasDurumu(v) { 188 switch (v || '') { 189 case 'pending': return T('ui.oyun.savas.durum_pending', 'Davet bekliyor'); 190 case 'running': return T('ui.oyun.savas.durum_running', 'Sürüyor'); 191 case 'voting': return T('ui.oyun.savas.durum_voting', 'Oylamada'); 192 case 'judged': return T('ui.oyun.savas.durum_judged', 'Bitti'); 193 case 'cancelled': return T('ui.oyun.savas.durum_cancelled', 'İptal'); 194 case 'expired': return T('ui.oyun.savas.durum_expired', 'Süresi doldu'); 195 default: return v || ''; 196 } 197} 198function durumSinifi(v) { 199 switch (v || '') { 200 case 'running': return 'os-rozet-bilgi'; 201 case 'voting': return 'os-rozet-oy'; 202 case 'judged': return 'os-rozet-ok'; 203 case 'cancelled': case 'expired': return 'os-rozet-kotu'; 204 default: return 'os-rozet-bekle'; 205 } 206} 207 208/** Format adı BAKANIN dilinde: sunucunun format_ad alanı savaÅın diline göre 209 * üretilir (İngilizce panelde Türkçe savaÅ "TartıÅma" derdi). */ 210function formatAdi(f, yedek) { 211 switch (f || '') { 212 case 'debate': return T('ui.oyun.savas.format_debate', 'TartıÅma'); 213 case 'knowledge': return T('ui.oyun.savas.format_knowledge', 'Bilgi'); 214 case 'sales': return T('ui.oyun.savas.format_sales', 'SatıÅ'); 215 default: return yedek || f || ''; 216 } 217} 218 219/** Kapalı format sebebi (klon_savasi.formatlar â neden). */ 220function nedenMetni(v) { 221 switch (v || '') { 222 case 'egitim_yok': return T('ui.oyun.savas.neden_egitim_yok', 'İki klonun da yeterli herkese açık eÄitimi olmalı.'); 223 case 'kapali': return T('ui.oyun.savas.neden_kapali', 'Åu an kapalı.'); 224 default: return T('ui.oyun.savas.neden_kapali', 'Åu an kapalı.'); 225 } 226} 227 228/** Bakanın tarafı: 'a' | 'b' | '' (sahip deÄil). */ 229function benimTaraf(s) { 230 const me = benimId(); 231 if (!s || me == null) return ''; 232 if (s.a && s.a.owner && Number(s.a.owner.id) === me) return 'a'; 233 if (s.b && s.b.owner && Number(s.b.owner.id) === me) return 'b'; 234 return ''; 235} 236 237function klonAdi(s, taraf) { 238 const t = s && s[taraf]; 239 return (t && t.assistant && t.assistant.name) || taraf.toUpperCase(); 240} 241 242/** BitmiÅ savaÅta sahibe göre sonuç çipi. */ 243function sonucCipiHtml(s) { 244 if (!s || s.status !== 'judged') return ''; 245 const taraf = benimTaraf(s); 246 if (!taraf) return ''; 247 const w = s.winner_side || 'draw'; 248 if (w === 'draw') return `<span class="os-rozet os-rozet-bekle">${escapeHtml(T('ui.oyun.savas.berabere', 'Berabere'))}</span>`; 249 if (w === taraf) return `<span class="os-rozet os-rozet-ok">${escapeHtml(T('ui.oyun.savas.kazandin', 'Kazandın'))}</span>`; 250 return `<span class="os-rozet os-rozet-kotu">${escapeHtml(T('ui.oyun.savas.kaybettin', 'Kaybettin'))}</span>`; 251} 252 253function rozetlerHtml(s) { 254 const p = [ 255 `<span class="os-rozet os-rozet-soft">${escapeHtml(formatAdi(s.format, s.format_ad))}</span>`, 256 `<span class="os-rozet ${durumSinifi(s.status)}">${escapeHtml(savasDurumu(s.status))}</span>`, 257 ]; 258 if (s.status === 'running' && s.rounds) { 259 p.push(`<span class="os-rozet os-rozet-bilgi">${escapeHtml(T('ui.oyun.savas.tur_n_m', 'Tur {n}/{m}', { n: sayi(s.current_round || 0), m: sayi(s.rounds) }))}</span>`); 260 } 261 if (s.can_vote) p.push(`<span class="os-rozet os-rozet-oy os-rozet-dolu">${escapeHtml(T('ui.oyun.savas.oy_ver_cip', 'oy ver'))}</span>`); 262 if (s.is_public && s.public_key) p.push(`<span class="os-rozet os-rozet-soft">${escapeHtml(T('ui.oyun.savas.acik_cip', 'Herkese açık'))}</span>`); 263 p.push(sonucCipiHtml(s)); 264 return p.join(''); 265} 266 267// ====================================================================== LİSTE 268 269const DURUMLAR = () => ([ 270 ['', T('ui.oyun.savas.suzgec_tumu', 'Tümü')], 271 ['pending', T('ui.oyun.savas.suzgec_pending', 'Bekleyen')], 272 ['running', T('ui.oyun.savas.suzgec_running', 'Süren')], 273 ['voting', T('ui.oyun.savas.suzgec_voting', 'Oylama')], 274 ['judged', T('ui.oyun.savas.suzgec_judged', 'Bitti')], 275]); 276 277function satirHtml(s, { davetli = false } = {}) { 278 const a = klonAdi(s, 'a'), b = klonAdi(s, 'b'); 279 const sahipler = [kisiAdi(s.a && s.a.owner), kisiAdi(s.b && s.b.owner)].join(' · '); 280 const eylem = davetli ? `<div class="os-satir-eylem"> 281 <button type="button" class="os-btn os-btn-sm os-btn-ghost os-btn-kotu" data-reddet="${s.id}">${escapeHtml(T('ui.oyun.savas.reddet', 'Reddet'))}</button> 282 <button type="button" class="os-btn os-btn-sm os-btn-primary" data-kabul="${s.id}">${escapeHtml(T('ui.oyun.savas.kabul', 'Kabul et'))}</button> 283 </div>` : ''; 284 return `<div class="os-satir" role="button" tabindex="0" data-ac="${s.id}"> 285 <span class="os-satir-ikon ${durumSinifi(s.status)}">${IKON.simsek}</span> 286 <span class="os-satir-metin">
287 <span class="os-satir-konu">${escapeHtml(s.topic || '')}</span> 288 <span class="os-satir-taraflar">${escapeHtml(a)} <span class="os-vs">vs</span> ${escapeHtml(b)}</span> 289 <span class="os-satir-alt">${escapeHtml(sahipler)} · ${escapeHtml(tarih(s.created_at))}</span> 290 <span class="os-cipler">${rozetlerHtml(s)}</span> 291 ${eylem} 292 </span> 293 <span class="os-satir-ok">${IKON.ok}</span> 294 </div>`; 295} 296 297function acikOyKartiHtml(s) { 298 return `<div class="os-oy-kart" role="button" tabindex="0" data-ac="${s.id}"> 299 <div class="os-oy-kart-konu">${escapeHtml(s.topic || '')}</div> 300 <div class="os-oy-kart-taraflar">${klonAvatarHtml(s.a && s.a.assistant, 24)}<span>${escapeHtml(klonAdi(s, 'a'))}</span> 301 <span class="os-vs">vs</span>${klonAvatarHtml(s.b && s.b.assistant, 24)}<span>${escapeHtml(klonAdi(s, 'b'))}</span></div> 302 <div class="os-oy-kart-alt"><span class="os-rozet os-rozet-soft">${escapeHtml(formatAdi(s.format, s.format_ad))}</span> 303 <span class="os-oy-kart-sure">${escapeHtml(s.voting_ends_at ? T('ui.oyun.savas.oylama_bitis', 'Oylama bitiÅi: {t}', { t: tarih(s.voting_ends_at) }) : '')}</span></div> 304 <button type="button" class="os-btn os-btn-sm os-btn-primary" data-ac="${s.id}">${escapeHtml(T('ui.oyun.savas.oy_ver', 'Oy ver'))}</button> 305 </div>`; 306} 307 308/** 309 * Klon SavaÅları listesi. `ac(id)` ayrıntıya geçirir (lobi adres çubuÄunu 310 * yazar), `geri` lobiye döner. 311 */
312export async function renderSavaslar(host, { geri, ac } = {}) { 313 stilleriEkle(); 314 const d = { durum: '', battles: [], acikOylar: [], yukleniyor: true, yuklendi: false, hata: '', istek: 0 }; 315 const kok = document.createElement('div'); 316 kok.className = 'os-host'; 317 host.innerHTML = ''; 318 host.appendChild(kok); 319 320 const acGit = (id) => { 321 if (typeof ac === 'function') { ac(id); return; } 322 // Kablosuz kullanım: ayrıntıdan geri = bu liste. 323 renderSavas(host, id, { geri: () => renderSavaslar(host, { geri, ac }) }).catch(e => toast(hataMetni(e), 'error')); 324 }; 325 326 const ciz = () => { 327 const me = benimId(); 328 const sekmeler = DURUMLAR().map(([k, ad]) => 329 `<button type="button" class="os-sekme ${d.durum === k ? 'on' : ''}" data-durum="${k}">${escapeHtml(ad)}</button>`).join(''); 330 // Åerit "oy verebileceklerim": sunucu acik_oylamalar SAHİBİN kendi savaÅını 331 // ve zaten oy verdiklerimi de döndürür (yalnız status/is_public süzer); 332 // can_vote bakana göre hesaplanır â sahip ya da oy vermiÅ ise düÅer. 333 // Listede zaten görünen satır ikinci kez Åeride girmez. 334 const listeIdler = new Set(d.battles.map(s => Number(s.id))); 335 const acik = d.acikOylar.filter(s => s.can_vote && !listeIdler.has(Number(s.id))); 336 let govde = ''; 337 if (d.yukleniyor && !d.yuklendi) govde = yukleniyorHtml(); 338 else if (d.hata && !d.yuklendi) govde = hataHtml(d.hata); 339 else if (!d.battles.length) { 340 govde = bosHtml( 341 d.durum ? T('ui.oyun.savas.bos_suzgec', 'Bu durumda savaÅ yok') : T('ui.oyun.savas.bos', 'Henüz savaÅın yok'), 342 d.durum ? '' : T('ui.oyun.savas.bos_alt', '"Yeni savaÅ" ile klonunu bir konuda yarıÅtır; karÅı klonun sahibi kabul edince baÅlar.')); 343 } else { 344 govde = `<div class="os-liste ${d.yukleniyor ? 'os-soluk' : ''}">${d.battles.map(s => 345 satirHtml(s, { davetli: s.status === 'pending' && me != null && s.b && s.b.owner && Number(s.b.owner.id) === me })).join('')}</div> 346 ${d.battles.length >= 30 ? `<p class="os-not">${escapeHtml(T('ui.oyun.savas.liste_siniri', 'Son 30 savaÅ gösteriliyor.'))}</p>` : ''}`; 347 } 348 const serit = acik.length ? `<div class="os-bolum">${escapeHtml(T('ui.oyun.savas.acik_oylamalar', 'Açık oylamalar'))}</div> 349 <p class="os-not">${escapeHtml(T('ui.oyun.savas.acik_oylamalar_notu', 'İzleyici olarak oy verebileceÄin herkese açık savaÅlar.'))}</p> 350 <div class="os-serit">${acik.map(acikOyKartiHtml).join('')}</div>` : ''; 351 kok.innerHTML = basHtml( 352 T('ui.oyun.savas.baslik', 'Klon SavaÅları'), 353 T('ui.oyun.savas.alt', 'İki klon bir konuda yarıÅır; hakem puanlar, izleyici oy verir.'), 354 `<div class="os-head-eylem"> 355 <button type="button" class="os-btn os-btn-ghost os-btn-sm ${d.yukleniyor ? 'os-busy' : ''}" data-yenile ${d.yukleniyor ? 'disabled' : ''} aria-label="${escapeHtml(T('ui.oyun.savas.yenile', 'Yenile'))}">${IKON.yenile}</button> 356 <button type="button" class="os-btn os-btn-primary" data-yeni>${IKON.arti} ${escapeHtml(T('ui.oyun.savas.yeni', 'Yeni savaÅ'))}</button> 357 </div>`) + 358 `${d.hata && d.yuklendi ? `<div class="os-uyari">${escapeHtml(d.hata)}</div>` : ''} 359 ${serit} 360 <div class="os-bolum">${escapeHtml(T('ui.oyun.savas.savaslarim', 'SavaÅlarım'))}</div> 361 <div class="os-sekmeler">${sekmeler}</div> 362 ${govde}`; 363 bagla(); 364 }; 365 366 function bagla() { 367 geriBagla(kok, geri); 368 const yeniden = kok.querySelector('[data-yeniden]'); 369 if (yeniden) yeniden.addEventListener('click', () => yukle()); 370 const yenile = kok.querySelector('[data-yenile]'); 371 if (yenile) yenile.addEventListener('click', () => yukle()); 372 const yeni = kok.querySelector('[data-yeni]'); 373 if (yeni) yeni.addEventListener('click', () => yeniSavasModali({ onBaslatildi: (s) => acGit(s.id) })); 374 kok.querySelectorAll('[data-durum]').forEach(b => b.addEventListener('click', () => { 375 if (d.durum === b.dataset.durum) return; 376 d.durum = b.dataset.durum; 377 yukle(); 378 })); 379 // Satır: tıkla ya da Enter â ayrıntı. İç düÄmeler (kabul/reddet) kendi iÅini görür.
380 kok.querySelectorAll('[data-ac]').forEach(el => { 381 const git = (e) => { e.stopPropagation(); acGit(parseInt(el.dataset.ac, 10)); }; 382 el.addEventListener('click', git); 383 if (el.tagName !== 'BUTTON') { 384 // Yalnız satırın kendisi odaktayken: içindeki Kabul/Reddet düÄmesinde basılan Enter satırı açmasın. 385 el.addEventListener('keydown', (e) => { 386 if (e.target !== el) return; 387 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); git(e); } 388 }); 389 } 390 }); 391 kok.querySelectorAll('[data-kabul]').forEach(b => b.addEventListener('click', async (e) => { 392 e.stopPropagation(); 393 kilitle(b, true); 394 try { 395 await oyunApi.post(`/savas/${encodeURIComponent(b.dataset.kabul)}/kabul`, {}); 396 toast(T('ui.oyun.savas.kabul_edildi', 'Kabul edildi; turlar iÅleniyor.'), 'success'); 397 yukle(); 398 } catch (err) { 399 kilitle(b, false); 400 toast(hataMetni(err), 'error'); 401 } 402 })); 403 kok.querySelectorAll('[data-reddet]').forEach(b => b.addEventListener('click', async (e) => { 404 e.stopPropagation(); 405 const kap = b.closest('.os-satir-metin'); 406 const evet = await onayIste(kap, T('ui.oyun.savas.reddet_onay', 'Davet reddedilsin mi?'), { tehlike: true }); 407 if (!evet) return; 408 kilitle(b, true);
409 try { 410 await oyunApi.post(`/savas/${encodeURIComponent(b.dataset.reddet)}/reddet`, {}); 411 toast(T('ui.oyun.savas.reddedildi', 'Davet reddedildi.'), 'success'); 412 yukle(); 413 } catch (err) { 414 kilitle(b, false); 415 toast(hataMetni(err), 'error'); 416 } 417 })); 418 } 419 420 async function yukle() { 421 const id = ++d.istek; 422 d.yukleniyor = true; d.hata = ''; 423 ciz(); 424 try { 425 const r = await oyunApi.get('/savas/liste' + (d.durum ? '?durum=' + encodeURIComponent(d.durum) : '')); 426 if (id !== d.istek) return; 427 d.battles = (r && Array.isArray(r.battles)) ? r.battles.filter(Boolean) : []; 428 d.acikOylar = (r && Array.isArray(r.open_votes)) ? r.open_votes.filter(Boolean) : []; 429 d.yuklendi = true; 430 } catch (e) { 431 if (id !== d.istek) return; 432 d.hata = hataMetni(e); 433 } 434 d.yukleniyor = false; 435 ciz(); 436 } 437 438 // Sahibi olduÄum savaÅın durumu deÄiÅince (game.battle) liste tazelenir; 439 // 400 ms toplama: bir tur iki çerçeve doÄurabilir. 440 let saat = null; 441 const onOlay = (e) => { 442 if (!host.contains(kok)) { kapat(); return; } 443 const f = e && e.detail; 444 if (!f || !/^game\.battle(_round|_close)?$/.test(String(f.type || ''))) return; 445 clearTimeout(saat); 446 saat = setTimeout(() => { if (host.contains(kok)) yukle(); }, 400); 447 }; 448 function kapat() { clearTimeout(saat); window.removeEventListener('morfoz:game', onOlay); } 449 window.addEventListener('morfoz:game', onOlay); 450 _ekranAc(host, ciz, kapat); 451 await yukle(); 452} 453 454// ====================================================================== AYRINTI 455 456function tarafHtml(s, taraf, kazandi) { 457 const t = s[taraf] || {}; 458 const rozet = kazandi 459 ? `<span class="os-rozet os-rozet-ok os-rozet-dolu">${IKON.kupa} ${escapeHtml(T('ui.oyun.savas.kazandi', 'Kazandı'))}</span>` 460 : `<span class="os-rozet ${taraf === 'a' ? 'os-rozet-a' : 'os-rozet-b'}">${taraf.toUpperCase()}</span>`; 461 return `<div class="os-taraf os-taraf-${taraf}"> 462 ${klonAvatarHtml(t.assistant, 56)} 463 <div class="os-taraf-ad">${escapeHtml((t.assistant && t.assistant.name) || T('ui.oyun.savas.klon', 'Klon'))}</div> 464 <div class="os-taraf-sahip">${escapeHtml(kisiAdi(t.owner))}${kullaniciHandle(t.owner) && kullaniciHandle(t.owner) !== kisiAdi(t.owner) ? ' · ' + escapeHtml(kullaniciHandle(t.owner)) : ''}</div> 465 ${rozet} 466 </div>`; 467} 468 469function balonHtml(s, t) { 470 const bMi = (t.side || 'a') === 'b'; 471 return `<div class="os-balon-satir ${bMi ? 'b' : 'a'}"> 472 <div class="os-balon"> 473 <div class="os-balon-ust"><span class="os-balon-ad">${escapeHtml(klonAdi(s, bMi ? 'b' : 'a'))}</span> 474 ${t.round ? `<span class="os-balon-tur">${escapeHtml(T('ui.oyun.savas.tur_n', 'Tur {n}', { n: sayi(t.round) }))}</span>` : ''}</div> 475 ${t.q ? `<div class="os-balon-soru">${escapeHtml(T('ui.oyun.savas.soru', 'Soru: {q}', { q: t.q }))}</div>` : ''} 476 <div class="os-balon-metin">${escapeHtml(t.text || '')}</div> 477 </div> 478 </div>`; 479} 480 481/** İki taraflı oran çubuÄu (hakem yüzdesi ya da oy sayısı). */ 482function oranHtml(aEtiket, bEtiket, aDeger, bDeger, { yuzde = false } = {}) { 483 const a = Number(aDeger) || 0, b = Number(bDeger) || 0; 484 const top = a + b; 485 const pa = top > 0 ? Math.round(100 * a / top) : 50; 486 const goster = (v) => yuzde ? escapeHtml(sayi(Math.round(v))) + '%' : escapeHtml(sayi(v)); 487 return `<div class="os-oran"> 488 <div class="os-oran-ust"><span class="os-oran-a">${escapeHtml(aEtiket)} <b>${goster(a)}</b></span> 489 <span class="os-oran-b"><b>${goster(b)}</b> ${escapeHtml(bEtiket)}</span></div> 490 <div class="os-oran-cubuk"><span class="os-oran-dolu-a" style="width:${pa}%"></span><span class="os-oran-dolu-b" style="width:${100 - pa}%"></span></div> 491 </div>`; 492} 493 494/** 495 * Tek savaÅın ayrıntısı: taraflar, döküm, hakem, oy, paylaÅım; davet eylemleri. 496 */
497export async function renderSavas(host, id, { geri } = {}) { 498 stilleriEkle(); 499 const d = { id: Number(id), s: null, yukleniyor: true, hata: '', istek: 0, not: '' }; 500 const kok = document.createElement('div'); 501 kok.className = 'os-host'; 502 host.innerHTML = ''; 503 host.appendChild(kok); 504 505 const ciz = () => { 506 const s = d.s; 507 if (!s) { 508 kok.innerHTML = basHtml(T('ui.oyun.savas.tek_baslik', 'Klon savaÅı'), '') + 509 (d.yukleniyor ? yukleniyorHtml() : hataHtml(d.hata || T('ui.oyun.savas.bulunamadi', 'SavaÅ bulunamadı'))); 510 bagla(); 511 return; 512 } 513 const taraf = benimTaraf(s); 514 const bitti = s.status === 'judged'; 515 const bekliyor = s.status === 'pending'; 516 const aAd = klonAdi(s, 'a'), bAd = klonAdi(s, 'b'); 517 518 // BaÅlık altı: son tarih notu. 519 let sure = ''; 520 if (s.status === 'voting' && s.voting_ends_at) sure = T('ui.oyun.savas.oylama_bitis', 'Oylama bitiÅi: {t}', { t: tarih(s.voting_ends_at) }); 521 else if (bekliyor && s.accept_deadline_at) sure = T('ui.oyun.savas.kabul_son', 'Kabul için son: {t}', { t: tarih(s.accept_deadline_at) }); 522 else if (bitti && s.ended_at) sure = T('ui.oyun.savas.bitis', 'BitiÅ: {t}', { t: tarih(s.ended_at) }); 523 524 // Davet eylemleri (yalnız pending): B'nin sahibi kabul/reddeder, A'nın sahibi geri çeker. 525 let eylem = ''; 526 if (bekliyor && taraf === 'b') { 527 eylem = `<div class="os-kart os-kart-eylem"> 528 <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.davet_metni', '{ad}, klonunu bir savaÅa davet etti.', { ad: kisiAdi(s.a && s.a.owner) }))}</div> 529 <p class="os-not">${escapeHtml(T('ui.oyun.savas.kabul_notu', 'Kabul edince turlar kuyrukta iÅlenir; sonuç bildirimle gelir.'))}</p> 530 <div class="os-eylemler"> 531 <button type="button" class="os-btn os-btn-ghost os-btn-kotu" data-reddet>${IKON.kapat} ${escapeHtml(T('ui.oyun.savas.reddet', 'Reddet'))}</button> 532 <button type="button" class="os-btn os-btn-primary" data-kabul>${IKON.onay} ${escapeHtml(T('ui.oyun.savas.kabul', 'Kabul et'))}</button> 533 </div> 534 </div>`; 535 } else if (bekliyor && taraf === 'a') { 536 eylem = `<div class="os-kart os-kart-eylem"> 537 <p class="os-not">${escapeHtml(T('ui.oyun.savas.bekleme_notu', 'KarÅı klonun sahibi kabul edince savaÅ baÅlar.'))}</p> 538 <div class="os-eylemler"> 539 <button type="button" class="os-btn os-btn-ghost os-btn-kotu os-btn-sm" data-geri-cek>${escapeHtml(T('ui.oyun.savas.geri_cek', 'Daveti geri çek'))}</button> 540 </div> 541 </div>`; 542 } 543 544 const taraflar = `<div class="os-kart os-taraflar"> 545 ${tarafHtml(s, 'a', bitti && s.winner_side === 'a')} 546 <div class="os-taraflar-orta">${IKON.simsekBuyuk}${bitti && s.winner_side === 'draw' ? `<span class="os-rozet os-rozet-bekle">${escapeHtml(T('ui.oyun.savas.berabere', 'Berabere'))}</span>` : ''}</div> 547 ${tarafHtml(s, 'b', bitti && s.winner_side === 'b')} 548 </div>`; 549 550 const tr = Array.isArray(s.transcript) ? s.transcript : []; 551 let dokum; 552 if (!tr.length) { 553 let m; 554 if (bekliyor) m = T('ui.oyun.savas.baslamadi', 'SavaÅ henüz baÅlamadı.'); 555 else if (s.status === 'running') m = T('ui.oyun.savas.turlar_isleniyor', 'Turlar iÅleniyorâ¦'); 556 else if (s.status === 'cancelled' || s.status === 'expired') m = T('ui.oyun.savas.yapilmadi', 'SavaÅ yapılmadı.'); 557 else m = T('ui.oyun.savas.dokum_yok', 'Döküm yok.'); 558 dokum = `<div class="os-kart os-dokum-bos">${IKON.balon}<span>${escapeHtml(m)}</span></div>`; 559 } else { 560 dokum = `<div class="os-bolum">${escapeHtml(T('ui.oyun.savas.karsilasma', 'KarÅılaÅma'))}</div> 561 <div class="os-dokum">${tr.map(t => balonHtml(s, t)).join('')} 562 ${s.status === 'running' ? `<div class="os-dokum-devam"><span class="os-spinner"></span>${escapeHtml(T('ui.oyun.savas.turlar_isleniyor', 'Turlar iÅleniyorâ¦'))}</div>` : ''}</div>`; 563 } 564 565 let hakem = ''; 566 if (bitti && s.judge) { 567 const h = s.judge; 568 hakem = `<div class="os-kart"> 569 <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.hakem', 'Hakem'))}</div> 570 ${h.hakem_yok ? `<p class="os-not">${escapeHtml(T('ui.oyun.savas.hakem_yok', 'Hakem koÅmadı; sonuç yalnız oylardan.'))}</p>` : ''} 571 ${oranHtml(aAd, bAd, h.a, h.b, { yuzde: true })} 572 ${h.reason ? `<p class="os-hakem-neden">${escapeHtml(h.reason)}</p>` : ''} 573 ${h.tutarsiz ? `<p class="os-not">${escapeHtml(T('ui.oyun.savas.hakem_tutarsiz', 'İki hakem koÅusu ayrıÅtı; berabere sayıldı.'))}</p>` : ''} 574 </div>`; 575 } 576 577 let oy = ''; 578 if (s.can_vote) { 579 oy = `<div class="os-kart"> 580 <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.kim_kazandi', 'Kim kazandı?'))}</div> 581 <p class="os-not">${escapeHtml(T('ui.oyun.savas.oy_notu', 'Tek oy; daÄılım oylama kapanana kadar gizli.'))}</p> 582 <div class="os-oy-dugmeler"> 583 <button type="button" class="os-btn os-btn-a" data-oy="a">${escapeHtml(aAd)}</button> 584 <button type="button" class="os-btn os-btn-b" data-oy="b">${escapeHtml(bAd)}</button> 585 </div> 586 </div>`; 587 } else if (s.my_vote) { 588 oy = `<div class="os-kart os-oy-verildi">${IKON.onay} ${escapeHtml(T('ui.oyun.savas.oyun', 'Oyun: {ad}', { ad: s.my_vote === 'a' ? aAd : bAd }))}</div>`; 589 } else if (s.status === 'voting' && taraf) { 590 oy = `<div class="os-kart"><p class="os-not">
590${escapeHtml(T('ui.oyun.savas.oylama_suruyor', 'Oylama sürüyor; sahipler oy veremez, daÄılım kapanınca görünür.'))}</p></div>`; 591 } 592 if (bitti && s.votes) { 593 oy += `<div class="os-kart"> 594 <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.izleyici_oyu', 'İzleyici oyu'))}</div> 595 ${oranHtml(aAd, bAd, s.votes.a, s.votes.b)} 596 ${taraf ? `<p class="os-not">${escapeHtml(s.points_awarded 597 ? T('ui.oyun.savas.puan_yazildi', 'Sahiplere sezon puanı yazıldı; cüzdanında görünür.') 598 : T('ui.oyun.savas.puan_yok', 'Bu savaÅ puan kazandırmadı (aynı çiftle sezon sınırı ya da bütçe).'))}</p>` : ''} 599 </div>`; 600 } 601 602 let paylas = ''; 603 if (s.is_public && s.public_key) { 604 const url = paylasimAdresi(s.public_key); 605 paylas = `<div class="os-kart"> 606 <div class="os-kart-baslik">${escapeHtml(T('ui.oyun.savas.paylas', 'PaylaÅ'))}</div> 607 <p class="os-not">${escapeHtml(T('ui.oyun.savas.paylas_notu', 'Oy toplamak için baÄlantıyı gönder.'))}</p> 608 <div class="os-paylas-satir"> 609 <input type="text" readonly value="${escapeHtml(url)}" data-paylas-adres> 610 <button type="button" class="os-btn os-btn-ghost" data-paylas>${IKON.paylas} ${escapeHtml(T('ui.oyun.savas.paylas_dugme', 'BaÄlantıyı paylaÅ'))}</button> 611 </div> 612 </div>`; 613 } 614 615 kok.innerHTML = basHtml(s.topic || T('ui.oyun.savas.tek_baslik', 'Klon savaÅı'), 616 `${formatAdi(s.format, s.format_ad)} · ${savasDurumu(s.status)}${sure ? ' · ' + sure : ''}`, 617 `<button type="button" class="os-btn os-btn-ghost os-btn-sm" data-yenile aria-label="${escapeHtml(T('ui.oyun.savas.yenile', 'Yenile'))}">${IKON.yenile}</button>`) + 618 `<div class="os-cipler os-cipler-bas">${rozetlerHtml(s)}</div> 619 ${d.hata ? `<div class="os-uyari">${escapeHtml(d.hata)}</div>` : ''} 620 ${d.not ? `<div class="os-bilgi-not">${IKON.onay} ${escapeHtml(d.not)}</div>` : ''} 621 ${eylem} 622 ${taraflar} 623 ${dokum} 624 ${hakem} 625 ${oy} 626 ${paylas}`; 627 bagla(); 628 }; 629 630 function bagla() { 631 geriBagla(kok, geri); 632 const yeniden = kok.querySelector('[data-yeniden]'); 633 if (yeniden) yeniden.addEventListener('click', () => yukle()); 634 const yenile = kok.querySelector('[data-yenile]'); 635 if (yenile) yenile.addEventListener('click', () => yukle()); 636 637 const kabul = kok.querySelector('[data-kabul]'); 638 if (kabul) kabul.addEventListener('click', async () => { 639 const reddet = kok.querySelector('[data-reddet]'); 640 kilitle(kabul, true); kilitle(reddet, true); 641 d.hata = ''; d.not = ''; 642 try { 643 const r = await oyunApi.post(`/savas/${encodeURIComponent(d.id)}/kabul`, {}); 644 if (r && r.id) d.s = r; 645 d.not = T('ui.oyun.savas.kabul_edildi', 'Kabul edildi; turlar iÅleniyor.'); 646 toast(d.not, 'success'); 647 ciz(); 648 } catch (e) { 649 kilitle(kabul, false); kilitle(reddet, false); 650 toast(hataMetni(e), 'error'); 651 } 652 }); 653 const reddet = kok.querySelector('[data-reddet]') || kok.querySelector('[data-geri-cek]'); 654 if (reddet) reddet.addEventListener('click', async () => { 655 const kap = reddet.closest('.os-kart'); 656 const geriCek = reddet.hasAttribute('data-geri-cek'); 657 const evet = await onayIste(kap, geriCek 658 ? T('ui.oyun.savas.geri_cek_onay', 'Davet geri çekilsin mi?') 659 : T('ui.oyun.savas.reddet_onay', 'Davet reddedilsin mi?'), { tehlike: true }); 660 if (!evet) return; 661 kilitle(reddet, true); kilitle(kok.querySelector('[data-kabul]'), true);
662 try { 663 await oyunApi.post(`/savas/${encodeURIComponent(d.id)}/reddet`, {}); 664 toast(geriCek ? T('ui.oyun.savas.geri_cekildi', 'Davet geri çekildi.') : T('ui.oyun.savas.reddedildi', 'Davet reddedildi.'), 'success'); 665 yukle(); 666 } catch (e) { 667 kilitle(reddet, false); kilitle(kok.querySelector('[data-kabul]'), false); 668 toast(hataMetni(e), 'error'); 669 } 670 }); 671 672 kok.querySelectorAll('[data-oy]').forEach(b => b.addEventListener('click', async () => { 673 kok.querySelectorAll('[data-oy]').forEach(x => kilitle(x, true)); 674 d.hata = ''; d.not = ''; 675 try { 676 const r = await oyunApi.post(`/savas/${encodeURIComponent(d.id)}/oy`, { side: b.dataset.oy }); 677 if (r && r.id) d.s = r; 678 d.not = T('ui.oyun.savas.oy_kaydedildi', 'Oyun kaydedildi.'); 679 toast(d.not, 'success'); 680 ciz(); 681 } catch (e) { 682 kok.querySelectorAll('[data-oy]').forEach(x => kilitle(x, false)); 683 toast(hataMetni(e), 'error'); 684 } 685 })); 686 687 const paylas = kok.querySelector('[data-paylas]'); 688 if (paylas) paylas.addEventListener('click', async () => { 689 const s = d.s; 690 const url = paylasimAdresi(s.public_key); 691 const baslik = `${klonAdi(s, 'a')} vs ${klonAdi(s, 'b')} · ${T('ui.oyun.savas.tek_baslik', 'Klon savaÅı')}`; 692 kilitle(paylas, true);
693 try { 694 let paylasildi = false; 695 if (navigator.share) { 696 // Kullanıcı vazgeçtiyse (AbortError) sessiz; paylaÅım desteklenmiyorsa panoya düÅ. 697 try { await navigator.share({ title: baslik, text: s.topic || baslik, url }); paylasildi = true; } 698 catch (e) { if (e && e.name === 'AbortError') paylasildi = true; } 699 } 700 if (!paylasildi) { 701 await navigator.clipboard.writeText(url); 702 toast(T('ui.oyun.savas.kopyalandi', 'BaÄlantı kopyalandı.'), 'success'); 703 } 704 } catch { 705 // Pano kapalıysa (izin yok, http) adres kutusunu seçili bırak: elle kopyalanır. 706 const giris = kok.querySelector('[data-paylas-adres]'); 707 if (giris) { giris.focus(); giris.select(); } 708 toast(T('ui.oyun.savas.kopyalanamadi', 'Kopyalanamadı; adresi seçip kopyala.'), 'error'); 709 } 710 kilitle(paylas, false); 711 }); 712 const adres = kok.querySelector('[data-paylas-adres]'); 713 if (adres) adres.addEventListener('focus', () => adres.select()); 714 } 715 716 async function yukle() { 717 const id = ++d.istek; 718 d.yukleniyor = true; d.hata = ''; 719 // Gövde varken yeniden çizilmez (ekran zıplamasın); yalnız yenile düÄmesi 720 // kilitlenir â ciz() bittiÄinde DOM yenilendiÄi için kilit kendiliÄinden kalkar. 721 if (!d.s) ciz(); else kilitle(kok.querySelector('[data-yenile]'), true); 722 try { 723 const r = await oyunApi.get(`/savas/${encodeURIComponent(d.id)}`); 724 if (id !== d.istek) return; 725 if (r && r.id) d.s = r; 726 else d.hata = T('ui.oyun.savas.bulunamadi', 'SavaÅ bulunamadı'); 727 } catch (e) { 728 if (id !== d.istek) return; 729 d.hata = hataMetni(e); 730 } 731 d.yukleniyor = false; 732 ciz(); 733 } 734 735 // Canlı: BU savaÅın çerçevesi (tur eklendi, hakem koÅtu, kapandı) â 400 ms 736 // toplayıp yeniden yükle. Dinleyici bir kez baÄlanır; kök host'tan kalkınca 737 // (baÅka ekran çizilince) kendini söker. 738 let saat = null; 739 const onOlay = (e) => { 740 if (!host.contains(kok)) { kapat(); return; } 741 const f = e && e.detail; 742 if (!f || !/^game\.battle(_round|_close)?$/.test(String(f.type || ''))) return; 743 if (f.battle_id != null && Number(f.battle_id) !== d.id) return; 744 clearTimeout(saat); 745 saat = setTimeout(() => { if (host.contains(kok)) yukle(); }, 400); 746 }; 747 function kapat() { clearTimeout(saat); window.removeEventListener('morfoz:game', onOlay); } 748 window.addEventListener('morfoz:game', onOlay); 749 _ekranAc(host, ciz, kapat); 750 await yukle(); 751} 752 753/** Herkese açık sayfa: routes/oyun_public_routes.py â GET /kp/savas/{public_key}. */ 754function paylasimAdresi(anahtar) { 755 return `${location.origin}/kp/savas/${encodeURIComponent(String(anahtar || ''))}`; 756} 757 758// ====================================================================== YENİ SAVAÅ 759 760/** Sahibi olduÄum klonlar. `scope=mine` yanıtı yalnız {id, name, description, 761 * avatar_tone, avatar_url, visibility, my_role, modules} taÅır (assistants_routes 762 * list_assistants) â seed_source/klon_kind GELMEZ; savaÅa uygunluÄu (uygun_klon) 763 * sunucu POST /savas'ta denetler, burada yalnız sahiplik süzülür. */ 764async function klonlarimiYukle() { 765 try { 766 const rows = await api.get('/api/assistants?scope=mine', { sessiz: true }); 767 const liste = Array.isArray(rows) ? rows : (rows && rows.items) || []; 768 return liste.filter(a => a && (!a.my_role || a.my_role === 'owner')); 769 } catch { 770 return []; 771 } 772} 773 774/** 775 * "Yeni savaÅ" modalı: klonum â format (sunucudan; konu istemesi formata göre) 776 * â rakip (kiÅi ya da rehberden klon) â herkese açık â POST /savas. 777 * Sunucu rakip KİÅİ verilince onun klonunu kendisi seçer (motor.klon_bul). 778 */ 779function yeniSavasModali({ onBaslatildi }) { 780 const d = { 781 klonlar: [], assistant_a: null, formatlar: [], formatHata: '', formatIstek: 0, format: 'debate', topic: '', 782 rakipTur: 'kisi', rakip: null, rakipKlon: null, is_public: true, 783 arama: '', aramaSonuc: [], aramaIstek: 0, calisiyor: false, yukleniyor: true, 784 }; 785 const ov = document.createElement('div'); 786 ov.className = 'os-overlay'; 787 ov.innerHTML = `<div class="os-modal" role="dialog" aria-modal="true" aria-label="${escapeHtml(T('ui.oyun.savas.yeni', 'Yeni savaÅ'))}"> 788 <div class="os-modal-head"> 789 <div class="os-modal-title">${escapeHtml(T('ui.oyun.savas.yeni', 'Yeni savaÅ'))}</div> 790 <button type="button" class="os-modal-x" data-kapat aria-label="${escapeHtml(T('action.close', 'Kapat'))}">${IKON.kapat}</button> 791 </div> 792 <div class="os-modal-govde"> 793 <div class="os-form-bolum" data-klon></div> 794 <div class="os-form-bolum" data-formatlar></div> 795 <div class="os-form-bolum" data-konu></div> 796 <div class="os-form-bolum" data-rakip></div> 797 <div class="os-form-bolum" data-acik></div> 798 <div class="os-form-hata" data-hata hidden></div> 799 </div> 800 <div class="os-modal-alt"> 801 <button type="button" class="os-btn os-btn-ghost" data-kapat>${escapeHtml(T('action.cancel', 'Vazgeç'))}</button> 802 <button type="button" class="os-btn os-btn-primary" data-gonder disabled>${IKON.simsek} ${escapeHtml(T('ui.oyun.savas.davet_et', 'SavaÅa davet et'))}</button> 803 </div> 804 </div>`; 805 document.body.appendChild(ov); 806 const kapat = () => { ov.remove(); document.removeEventListener('keydown', esc); }; 807 function esc(e) { if (e.key === 'Escape' && !document.querySelector('.up-modal-overlay')) kapat(); } 808 document.addEventListener('keydown', esc); 809 ov.addEventListener('click', (e) => { if (e.target === ov) kapat(); }); 810 ov.querySelectorAll('[data-kapat]').forEach(b => b.addEventListener('click', kapat)); 811 812 const el = (sec) => ov.querySelector(sec); 813 const hataKutu = el('[data-hata]'); 814 const gonder = el('[data-gonder]'); 815 const hataGoster = (m) => { hataKutu.textContent = m; hataKutu.hidden = !m; }; 816 817 function gecerliMi() { 818 const f = d.formatlar.find(x => x.key === d.format); 819 return !d.calisiyor && !!d.assistant_a && !!f && !!f.acik && 820 (d.rakipTur === 'kisi' ? !!d.rakip : !!d.rakipKlon); 821 } 822 function dugmeGuncelle() { gonder.disabled = !gecerliMi(); } 823 824 // --- klonum
825 function klonCiz() { 826 const kap = el('[data-klon]'); 827 if (d.yukleniyor) { kap.innerHTML = yukleniyorHtml(); return; } 828 if (!d.klonlar.length) { 829 kap.innerHTML = `<div class="os-uyari">${escapeHtml(T('ui.oyun.savas.klon_yok', 'SavaÅa sokacak bir klonun yok. Ãnce bir asistan kur (otomatik üretilen klonlar savaÅa giremez).'))}</div>`; 830 return; 831 } 832 kap.innerHTML = `<label class="os-alan"><span>${escapeHtml(T('ui.oyun.savas.klonum', 'Klonum'))}</span> 833 <select data-klon-sec>${d.klonlar.map(a => 834 `<option value="${escapeHtml(String(a.id))}" ${Number(a.id) === Number(d.assistant_a) ? 'selected' : ''}>${escapeHtml(a.name || ('#' + a.id))}</option>`).join('')}</select></label>`; 835 kap.querySelector('[data-klon-sec]').addEventListener('change', (e) => { 836 d.assistant_a = parseInt(e.target.value, 10) || null; 837 formatlariYukle(); 838 }); 839 } 840 841 // --- format (sunucudan; kapalı olanlar sebebiyle gri) 842 function formatlarCiz() { 843 const kap = el('[data-formatlar]'); 844 if (!d.formatlar.length) { 845 // Hata: "yükleniyor"da takılı kalmasın â sebep + yeniden dene (çıkmaz yok). 846 kap.innerHTML = `<div class="os-bolum-kucuk">${escapeHtml(T('ui.oyun.savas.format', 'Format'))}</div>` + 847 (d.formatHata ? hataHtml(d.formatHata) 848 : `<p class="os-not">${escapeHtml(T('ui.oyun.savas.formatlar_yukleniyor', 'Formatlar yükleniyorâ¦'))}</p>`); 849 const yeniden = kap.querySelector('[data-yeniden]'); 850 if (yeniden) yeniden.addEventListener('click', () => formatlariYukle()); 851 return; 852 } 853 kap.innerHTML = `<div class="os-bolum-kucuk">${escapeHtml(T('ui.oyun.savas.format', 'Format'))}</div> 854 <div class="os-secenekler">${d.formatlar.map(f => ` 855 <button type="button" class="os-secenek ${d.format === f.key ? 'on' : ''} ${f.acik ? '' : 'kapali'}" data-format="${escapeHtml(f.key)}" ${f.acik ? '' : 'disabled'}> 856 <span class="os-secenek-nokta"></span> 857 <span class="os-secenek-metin"> 858 <span class="os-secenek-ad">${escapeHtml(formatAdi(f.key, f.ad))}</span> 859 <span class="os-secenek-alt">${escapeHtml(f.aciklama || '')}</span> 860 ${f.acik ? '' : `<span class="os-secenek-neden">${escapeHtml(nedenMetni(f.neden))}</span>`} 861 </span> 862 </button>`).join('')}</div>`; 863 kap.querySelectorAll('[data-format]').forEach(b => b.addEventListener('click', () => { 864 d.format = b.dataset.format; 865 if (d.format === 'sales') d.is_public = false; 866 formatlarCiz(); konuCiz(); acikCiz(); dugmeGuncelle(); 867 })); 868 } 869 870 async function formatlariYukle() { 871 // Klon/rakip art arda deÄiÅince istekler yarıÅır: yalnız SON isteÄin yanıtı 872 // iÅlenir, önceki geç gelirse atılır (aksi hâlde eski klonun kapalı formatı kalır). 873 const istek = ++d.formatIstek; 874 // BoÅ parametre gönderilmez: sunucu int|None bekler, '' 422 döndürür. 875 const q = []; 876 if (d.assistant_a) q.push('assistant_id=' + encodeURIComponent(String(d.assistant_a))); 877 if (d.rakipTur === 'klon' && d.rakipKlon) q.push('rakip_assistant_id=' + encodeURIComponent(String(d.rakipKlon.id))); 878 d.formatHata = ''; 879 if (!d.formatlar.length) formatlarCiz(); 880 try { 881 const r = await oyunApi.get('/savas/formatlar' + (q.length ? '?' + q.join('&') : ''), { sessiz: true }); 882 if (istek !== d.formatIstek || !ov.isConnected) return; 883 d.formatlar = (r && Array.isArray(r.formats)) ? r.formats : []; 884 } catch (e) { 885 if (istek !== d.formatIstek || !ov.isConnected) return; 886 d.formatlar = []; 887 d.formatHata = hataMetni(e); 888 } 889 const secili = d.formatlar.find(f => f.key === d.format && f.acik); 890 if (!secili) { 891 const ilk = d.formatlar.find(f => f.acik); 892 if (ilk) d.format = ilk.key; 893 } 894 if (d.format === 'sales') d.is_public = false; 895 formatlarCiz(); konuCiz(); acikCiz(); dugmeGuncelle(); 896 } 897 898 // --- konu: tartıÅma "Konu", satıŠ"Senaryo"; bilgi formatı konu istemez 899 // (sorular klonların eÄitiminden üretilir). 900 function konuCiz() { 901 const kap = el('[data-konu]'); 902 if (d.format === 'knowledge') { kap.innerHTML = ''; return; } 903 const etiket = d.format === 'sales' 904 ? T('ui.oyun.savas.senaryo', 'Senaryo (isteÄe baÄlı)') 905 : T('ui.oyun.savas.konu', 'Konu (isteÄe baÄlı)'); 906 kap.innerHTML = `<label class="os-alan"><span>${escapeHtml(etiket)}</span> 907 <textarea rows="2" maxlength="200" data-konu-giris placeholder="${escapeHtml(T('ui.oyun.savas.konu_yer', 'BoÅ bırakırsan konu havuzdan seçilir'))}">${escapeHtml(d.topic)}</textarea></label>`; 908 kap.querySelector('[data-konu-giris]').addEventListener('input', (e) => { d.topic = e.target.value.slice(0, 200); }); 909 } 910 911 // --- rakip: kiÅi (sunucu klonunu bulur) ya da rehberden klon 912 function rakipCiz() {
913 const kap = el('[data-rakip]'); 914 const sekme = (k, ad) => `<button type="button" class="os-sekme ${d.rakipTur === k ? 'on' : ''}" data-rakip-tur="${k}">${escapeHtml(ad)}</button>`; 915 let icerik; 916 if (d.rakipTur === 'kisi') { 917 icerik = d.rakip 918 ? `<div class="os-secili"><span class="os-secili-ikon">${IKON.kisi}</span> 919 <span class="os-secili-ad">${escapeHtml(kisiAdi(d.rakip))}</span> 920 <button type="button" class="os-cip-x" data-rakip-sil aria-label="${escapeHtml(T('action.close', 'Kapat'))}">${IKON.kapat}</button></div>` 921 : `<div class="os-secili-yok"><span>${escapeHtml(T('ui.oyun.savas.rakip_yok', 'Henüz kimse seçilmedi'))}</span> 922 <button type="button" class="os-btn os-btn-ghost os-btn-sm" data-rehber>${IKON.kisi} ${escapeHtml(T('ui.oyun.savas.kisi_sec', 'KiÅi seç'))}</button></div>`; 923 icerik += `<p class="os-not">${escapeHtml(T('ui.oyun.savas.kisi_notu', 'KiÅiyi seç; sunucu onun klonunu bulur.'))}</p>`; 924 } else { 925 icerik = d.rakipKlon 926 ? `<div class="os-secili">${klonAvatarHtml(d.rakipKlon, 26)} 927 <span class="os-secili-ad">${escapeHtml(d.rakipKlon.name || '')}${d.rakipKlon.owner_name ? ` <span class="os-secili-alt">· ${escapeHtml(d.rakipKlon.owner_name)}</span>` : ''}</span> 928 <button type="button" class="os-cip-x" data-rakip-sil aria-label="${escapeHtml(T('action.close', 'Kapat'))}">${IKON.kapat}</button></div>` 929 : `<div class="os-arama">${IKON.arama}<input type="text" data-klon-arama autocomplete="off" value="${escapeHtml(d.arama)}" placeholder="${escapeHtml(T('ui.oyun.savas.klon_ara', 'Rehberde klon ara (ad)'))}"></div> 930 <div class="os-arama-sonuc" data-klon-sonuc></div>`; 931 icerik += `<p class="os-not">${escapeHtml(T('ui.oyun.savas.klon_notu', 'Herkese açık rehberdeki bir klonu seç; sahibi kabul edince baÅlar.'))}</p>`; 932 } 933 kap.innerHTML = `<div class="os-bolum-kucuk">${escapeHtml(T('ui.oyun.savas.rakip', 'Rakip'))}</div> 934 <div class="os-sekmeler os-sekmeler-kucuk">${sekme('kisi', T('ui.oyun.savas.rakip_kisi', 'KiÅi'))}${sekme('klon', T('ui.oyun.savas.rakip_klon', 'Rehberden klon'))}</div> 935 ${icerik}`; 936 kap.querySelectorAll('[data-rakip-tur]').forEach(b => b.addEventListener('click', () => { 937 if (d.rakipTur === b.dataset.rakipTur) return; 938 d.rakipTur = b.dataset.rakipTur; 939 rakipCiz(); dugmeGuncelle(); 940 // Bilgi formatı iki klonun eÄitimine bakar: rakip klon deÄiÅince yeniden sor. 941 formatlariYukle(); 942 })); 943 const rehber = kap.querySelector('[data-rehber]'); 944 if (rehber) rehber.addEventListener('click', () => { 945 openUserSearchModal({ 946 onPick: (id, u) => { 947 d.rakip = u || { id }; 948 // Tek rakip seçiliyor: kiÅi seçici "ekle" modalı açık bırakır, biz kapatırız. 949 const uo = document.querySelector('.up-modal-overlay'); 950 if (uo) uo.remove(); 951 rakipCiz(); dugmeGuncelle(); 952 }, 953 }); 954 }); 955 const sil = kap.querySelector('[data-rakip-sil]'); 956 if (sil) sil.addEventListener('click', () => { 957 d.rakip = null; d.rakipKlon = null; 958 rakipCiz(); dugmeGuncelle(); 959 formatlariYukle(); 960 }); 961 const arama = kap.querySelector('[data-klon-arama]'); 962 if (arama) { 963 let saat = null; 964 arama.addEventListener('input', () => { 965 d.arama = arama.value; 966 clearTimeout(saat); 967 saat = setTimeout(klonAra, 250); 968 }); 969 setTimeout(() => arama.focus(), 20); 970 if (d.arama.trim().length >= 2) klonAra(); 971 } 972 } 973 974 /** Rehber araması: sunucu sayfalar (limit 20); tüm liste asla yüklenmez. */ 975 async function klonAra() { 976 const kap = el('[data-klon-sonuc]'); 977 if (!kap) return; 978 const q = d.arama.trim(); 979 if (q.length < 2) { kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(T('ui.oyun.savas.ara_ipucu', 'En az 2 harf yaz'))}</div>`; return; } 980 const istek = ++d.aramaIstek; 981 kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(T('ui.oyun.savas.araniyor', 'Aranıyorâ¦'))}</div>`; 982 let rows = []; 983 try { 984 rows = await api.get('/api/assistants?scope=directory&q=' + encodeURIComponent(q) + '&limit=20&sort=top', { sessiz: true }); 985 } catch (e) { 986 if (istek !== d.aramaIstek) return; 987 kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(hataMetni(e))}</div>`; 988 return; 989 } 990 if (istek !== d.aramaIstek) return; 991 const me = benimId(); 992 // Kendi klonum ve otomatik üretilmiÅ (seed) klonlar savaÅa giremez (uygun_klon). 993 d.aramaSonuc = (Array.isArray(rows) ? rows : []).filter(a =>
994 a && Number(a.owner_user_id) !== me && !a.seed_source && !Number(a.seed_unofficial || 0)); 995 if (!d.aramaSonuc.length) { kap.innerHTML = `<div class="os-arama-ipucu">${escapeHtml(T('ui.oyun.savas.klon_bulunamadi', 'Uygun klon bulunamadı'))}</div>`; return; } 996 kap.innerHTML = d.aramaSonuc.map(a => `<button type="button" class="os-arama-satir" data-klon-id="${escapeHtml(String(a.id))}"> 997 ${klonAvatarHtml(a, 28)} 998 <span class="os-arama-metin"><span class="os-arama-ad">${escapeHtml(a.name || '')}</span> 999 ${a.owner_name ? `<span class="os-arama-alt">${escapeHtml(a.owner_name)}</span>` : ''}</span> 1000 </button>`).join(''); 1001 kap.querySelectorAll('[data-klon-id]').forEach(b => b.addEventListener('click', () => { 1002 const a = d.aramaSonuc.find(x => String(x.id) === b.dataset.klonId); 1003 if (!a) return; 1004 d.rakipKlon = { id: a.id, name: a.name || '', owner_name: a.owner_name || '', avatar_url: a.avatar_url || '' }; 1005 rakipCiz(); dugmeGuncelle(); 1006 formatlariYukle(); 1007 })); 1008 } 1009 1010 // --- herkese açık (satıŠdüellosu hep kapalı) 1011 function acikCiz() { 1012 const kap = el('[data-acik]'); 1013 const satis = d.format === 'sales'; 1014 kap.innerHTML = `<label class="os-anahtar ${satis ? 'kapali' : ''}"> 1015 <input type="checkbox" data-acik-kutu ${d.is_public && !satis ? 'checked' : ''} ${satis ? 'disabled' : ''}> 1016 <span class="os-anahtar-metin"> 1017 <span class="os-anahtar-ad">${escapeHtml(T('ui.oyun.savas.acik', 'Herkese açık (izleyici oyu)'))}</span> 1018 <span class="os-anahtar-alt">${escapeHtml(T('ui.oyun.savas.acik_notu', 'Açık savaÅlar paylaÅılabilir ve izleyiciler oy verir. SatıŠdüellosu hep kapalı.'))}</span> 1019 </span></label>`; 1020 kap.querySelector('[data-acik-kutu]').addEventListener('change', (e) => { d.is_public = !!e.target.checked; }); 1021 } 1022 1023 gonder.addEventListener('click', async () => { 1024 hataGoster(''); 1025 if (!gecerliMi()) return; 1026 d.calisiyor = true; 1027 kilitle(gonder, true); 1028 const govde = { 1029 format: d.format, 1030 topic: d.format === 'knowledge' ? null : (d.topic.trim() || null), 1031 assistant_a: d.assistant_a, 1032 is_public: !!d.is_public && d.format !== 'sales', 1033 }; 1034 if (d.rakipTur === 'klon') govde.assistant_b = Number(d.rakipKlon.id); 1035 else govde.rakip_user_id = Number(d.rakip.id); 1036 try { 1037 const s = await oyunApi.post('/savas', govde); 1038 kapat(); 1039 toast(s && s.status === 'running' 1040 ? T('ui.oyun.savas.basladi', 'SavaÅ baÅladı; turlar iÅleniyor.') 1041 : T('ui.oyun.savas.davet_gonderildi', 'Davet gönderildi; karÅı sahip kabul edince baÅlar.'), 'success'); 1042 if (typeof onBaslatildi === 'function' && s && s.id) onBaslatildi(s); 1043 } catch (e) { 1044 d.calisiyor = false; 1045 kilitle(gonder, false); 1046 hataGoster(hataMetni(e)); 1047 } 1048 }); 1049 1050 // İlk yükleme: klonlarım + oyun tercihi (varsayılan klon) paralel; sonra formatlar. 1051 klonCiz(); formatlarCiz(); konuCiz(); rakipCiz(); acikCiz(); 1052 (async () => { 1053 const [klonlar, tercih] = await Promise.all([ 1054 klonlarimiYukle(), 1055 oyunApi.get('/tercih', { sessiz: true }).catch(() => null), 1056 ]); 1057 if (!ov.isConnected) return; 1058 d.klonlar = klonlar; 1059 // Varsayılan: oyun tercihindeki klon (GET /tercih â assistant_id), yoksa ilki. 1060 // klon_kind'e göre seçim YOK: scope=mine yanıtında o alan gelmiyor. 1061 const tercihId = tercih && tercih.assistant_id ? Number(tercih.assistant_id) : null; 1062 const sec = klonlar.find(a => Number(a.id) === tercihId) || klonlar[0]; 1063 d.assistant_a = sec ? Number(sec.id) : null; 1064 d.yukleniyor = false; 1065 klonCiz(); 1066 if (d.assistant_a) await formatlariYukle(); 1067 else dugmeGuncelle(); 1068 })(); 1069} 1070 1071// ---------------------------------------------------------------------- stil 1072 1073function stilleriEkle() { 1074 if (document.getElementById('oyunSavasStyles')) return; 1075 const s = document.createElement('style'); 1076 s.id = 'oyunSavasStyles'; 1077 s.textContent = ` 1078 .os-host, .os-overlay { --os-radius: 10px; } 1079 .os-host { padding: 24px 28px 56px; max-width: 900px; color: var(--text, #2A1E35); } 1080 .os-head { display:flex; align-items:flex-start; gap:12px; margin-bottom:10px; } 1081 .os-geri { flex:none; width:34px; height:34px; border-radius:50%; border:1px solid var(--border,#E6DFEC); background:var(--surface,#fff); color:var(--text-secondary,#6F6280); cursor:pointer; display:inline-flex; align-items:center; justify-content:center; } 1082 .os-geri:hover { color:var(--brand,#6B2387); border-color:var(--brand,#6B2387); } 1083 .os-head-metin { flex:1; min-width:0; } 1084 .os-head-eylem { display:flex; gap:6px; align-items:center; flex:none; } 1085 .os-title { font-family:var(--serif,'Fraunces',serif); font-size:24px; margin:2px 0 2px; color:var(--text,#2A1E35); overflow-wrap:anywhere; } 1086 .os-sub { margin:0; font-size:13px; color:var(--text-tertiary,#A89DB5); max-width:64ch; } 1087 .os-bolum { font-size:14px; font-weight:700; margin:20px 0 8px; color:var(--text,#2A1E35); } 1088 .os-bolum-kucuk { font-size:12px; font-weight:700; color:var(--text-secondary,#6F6280); margin-bottom:6px; text-transform:uppercase; letter-spacing:.03em; } 1089 .os-not { margin:4px 0 8px; font-size:12px; color:var(--text-tertiary,#A89DB5); line-height:1.45; } 1090 .os-uyari { background:var(--warning-soft,rgba(214,150,30,.12)); color:var(--text,#2A1E35); border-radius:var(--os-radius); padding:8px 12px; font-size:12px; margin:0 0 10px; }
1091 .os-bilgi-not { display:flex; align-items:center; gap:6px; color:var(--success,#2A9D7C); font-size:12.5px; font-weight:600; margin:0 0 10px; } 1092 .os-yukleniyor { display:flex; align-items:center; gap:10px; padding:24px 0; color:var(--text-tertiary,#A89DB5); font-size:13px; } 1093 .os-spinner { width:16px; height:16px; border-radius:50%; border:2px solid var(--border,#E6DFEC); border-top-color:var(--brand,#6B2387); animation:os-don .8s linear infinite; display:inline-block; flex:none; } 1094 @keyframes os-don { to { transform:rotate(360deg); } } 1095 .os-hata { display:flex; align-items:center; gap:12px; flex-wrap:wrap; padding:12px 14px; border-radius:var(--os-radius); background:var(--danger-soft,rgba(184,56,56,.1)); color:var(--text,#2A1E35); font-size:13px; } 1096 .os-soluk { opacity:.55; } 1097 1098 .os-btn { border:none; border-radius:9px; padding:9px 15px; font-size:13px; font-weight:600; font-family:inherit; cursor:pointer; display:inline-flex; align-items:center; gap:6px; position:relative; } 1099 .os-btn:disabled { opacity:.5; cursor:default; } 1100 .os-btn-sm { padding:6px 11px; font-size:12px; } 1101 .os-btn-primary { background:var(--brand,#6B2387); color:#fff; } 1102 .os-btn-ghost { background:var(--accent-soft,rgba(107,35,135,.09)); color:var(--brand,#6B2387); } 1103 .os-btn-danger { background:var(--danger,#B83838); color:#fff; } 1104 .os-btn-kotu { color:var(--danger,#B83838); background:var(--danger-soft,rgba(184,56,56,.1)); } 1105 .os-btn-a { flex:1; justify-content:center; background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); border:1px solid var(--brand,#6B2387); } 1106 .os-btn-b { flex:1; justify-content:center; background:var(--soft-info-bg,rgba(46,110,200,.1)); color:var(--soft-info-fg,#25579E); border:1px solid var(--soft-info-fg,#25579E); } 1107 .os-btn.os-busy { color:transparent !important; } 1108 .os-btn.os-busy::after { content:''; position:absolute; left:50%; top:50%; width:14px; height:14px; margin:-7px 0 0 -7px; border-radius:50%; border:2px solid rgba(255,255,255,.4); border-top-color:#fff; animation:os-don .8s linear infinite; } 1109 .os-btn-ghost.os-busy::after, .os-btn-kotu.os-busy::after, .os-btn-a.os-busy::after, .os-btn-b.os-busy::after { border-color:var(--accent-soft,rgba(107,35,135,.2)); border-top-color:var(--brand,#6B2387); } 1110 1111 .os-sekmeler { display:flex; gap:4px; background:var(--surface-2,#F4F0F7); border-radius:10px; padding:3px; margin-bottom:10px; } 1112 .os-sekmeler-kucuk { display:inline-flex; margin-bottom:8px; } 1113 .os-sekme { flex:1; border:none; background:transparent; border-radius:8px; padding:7px 10px; font-size:12px; font-weight:600; color:var(--text-secondary,#6F6280); cursor:pointer; font-family:inherit; white-space:nowrap; } 1114 .os-sekme.on { background:var(--surface,#fff); color:var(--brand,#6B2387); box-shadow:0 1px 3px rgba(0,0,0,.08); } 1115 1116 .os-rozet { display:inline-flex; align-items:center; gap:4px; border-radius:999px; padding:2px 8px; font-size:11px; font-weight:600; white-space:nowrap; } 1117 .os-rozet-soft { background:var(--surface-2,#F4F0F7); color:var(--text-secondary,#6F6280); } 1118 .os-rozet-ok { background:var(--success-soft,rgba(42,157,124,.14)); color:var(--success,#2A9D7C); } 1119 .os-rozet-kotu { background:var(--danger-soft,rgba(184,56,56,.12)); color:var(--danger,#B83838); } 1120 .os-rozet-bekle { background:var(--warning-soft,rgba(214,150,30,.16)); color:#9A6A0E; } 1121 .os-rozet-bilgi { background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); } 1122 .os-rozet-oy { background:var(--soft-info-bg,rgba(46,110,200,.12)); color:var(--soft-info-fg,#25579E); } 1123 .os-rozet-a { background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); } 1124 .os-rozet-b { background:var(--soft-info-bg,rgba(46,110,200,.12)); color:var(--soft-info-fg,#25579E); } 1125 .os-rozet-dolu.os-rozet-oy { background:var(--soft-info-fg,#25579E); color:#fff; } 1126 .os-rozet-dolu.os-rozet-ok { background:var(--success,#2A9D7C); color:#fff; } 1127 .os-cipler { display:flex; gap:5px; flex-wrap:wrap; align-items:center; } 1128 .os-cipler-bas { margin:0 0 12px; } 1129 1130 .os-liste { display:flex; flex-direction:column; gap:6px; } 1131 .os-satir { display:flex; align-items:flex-start; gap:10px; padding:10px 12px; border-radius:var(--os-radius); background:var(--surface,#fff); border:1px solid var(--border,#E6DFEC); cursor:pointer; text-align:left; } 1132 .os-satir:hover, .os-satir:focus-visible { border-color:var(--brand,#6B2387); outline:none; } 1133 .os-satir-ikon { flex:none; width:32px; height:32px; border-radius:10px; display:inline-flex; align-items:center; justify-content:center; } 1134 .os-satir-metin { flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; } 1135 .os-satir-konu { font-size:13.5px; font-weight:600; color:var(--text,#2A1E35); overflow-wrap:anywhere; } 1136 .os-satir-taraflar { font-size:12.5px; color:var(--text-secondary,#6F6280); } 1137 .os-vs { font-size:10.5px; font-weight:700; color:var(--text-tertiary,#A89DB5); text-transform:uppercase; margin:0 4px; } 1138 .os-satir-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 1139 .os-satir-eylem { display:flex; gap:6px; margin-top:6px; } 1140 .os-satir-ok { flex:none; color:var(--text-tertiary,#A89DB5); align-self:center; } 1141 1142 .os-serit { display:flex; gap:10px; overflow-x:auto; padding-bottom:6px; scrollbar-width:thin; } 1143 .os-oy-kart { flex:none; width:240px; display:flex; flex-direction:column; gap:6px; padding:12px; border-radius:var(--os-radius); background:var(--surface,#fff); border:1px solid var(--border,#E6DFEC); cursor:pointer; } 1144 .os-oy-kart:hover, .os-oy-kart:focus-visible { border-color:var(--brand,#6B2387); outline:none; } 1145 .os-oy-kart-konu { font-size:13px; font-weight:600; color:var(--text,#2A1E35); display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; } 1146 .os-oy-kart-taraflar { display:flex; align-items:center; gap:5px; font-size:12px; color:var(--text-secondary,#6F6280); min-width:0; } 1147 .os-oy-kart-taraflar > span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 1148 .os-oy-kart-alt { display:flex; align-items:center; gap:6px; justify-content:space-between; } 1149 .os-oy-kart-sure { font-size:11px; color:var(--text-tertiary,#A89DB5); } 1150 .os-oy-kart .os-btn { align-self:flex-start; margin-top:2px; } 1151 1152 .os-avatar { flex:none; display:inline-flex; align-items:center; justify-content:center; border-radius:50%; overflow:hidden; background:var(--accent-soft,rgba(107,35,135,.12)); color:var(--brand,#6B2387); font-weight:700; } 1153 .os-avatar img { width:100%; height:100%; object-fit:cover; display:block; } 1154 1155 .os-bos { display:flex; flex-direction:column; align-items:center; gap:6px; padding:28px 12px; color:var(--text-tertiary,#A89DB5); text-align:center; border:1px dashed var(--border,#E6DFEC); border-radius:var(--os-radius); } 1156 .os-bos-baslik { font-size:13px; font-weight:600; color:var(--text-secondary,#6F6280); } 1157 .os-bos-alt { font-size:12px; max-width:48ch; } 1158 1159 .os-kart { padding:14px 16px; border-radius:var(--os-radius); background:var(--surface,#fff); border:1px solid var(--border,#E6DFEC); margin:10px 0; } 1160 .os-kart-baslik { font-size:14px; font-weight:700; margin-bottom:4px; color:var(--text,#2A1E35); } 1161 .os-kart-eylem { border-color:var(--brand,#6B2387); } 1162 .os-eylemler { display:flex; gap:8px; justify-content:flex-end; flex-wrap:wrap; margin-top:6px; } 1163 .os-onay { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-top:10px; padding:8px 10px; border-radius:8px; background:var(--surface-2,#F4F0F7); border:1px solid var(--border,#E6DFEC); font-size:12.5px; color:var(--text,#2A1E35); } 1164 .os-onay-metin { flex:1; min-width:120px; } 1165 1166 .os-taraflar { display:flex; align-items:flex-start; gap:8px; } 1167 .os-taraf { flex:1; min-width:0; display:flex; flex-direction:column; align-items:center; gap:5px; text-align:center; } 1168 .os-taraf-ad { font-size:14px; font-weight:700; color:var(--text,#2A1E35); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; } 1169 .os-taraf-sahip { font-size:11.5px; color:var(--text-tertiary,#A89DB5); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:100%; } 1170 .os-taraflar-orta { flex:none; display:flex; flex-direction:column; align-items:center; gap:6px; padding-top:14px; color:var(--warning,#D6961E); } 1171 1172 .os-dokum { display:flex; flex-direction:column; gap:8px; } 1173 .os-dokum-bos { display:flex; align-items:center; gap:8px; color:var(--text-tertiary,#A89DB5); font-size:13px; } 1174 .os-dokum-devam { display:flex; align-items:center; gap:8px; color:var(--text-tertiary,#A89DB5); font-size:12px; padding:4px 0; } 1175 .os-balon-satir { display:flex; } 1176 .os-balon-satir.a { justify-content:flex-start; padding-right:40px; } 1177 .os-balon-satir.b { justify-content:flex-end; padding-left:40px; } 1178 .os-balon { max-width:100%; padding:10px 12px; border-radius:var(--os-radius); background:var(--accent-soft,rgba(107,35,135,.08)); } 1179 .os-balon-satir.b .os-balon { background:var(--soft-info-bg,rgba(46,110,200,.1)); } 1180 .os-balon-ust { display:flex; align-items:center; gap:6px; margin-bottom:3px; } 1181 .os-balon-ad { font-size:11.5px; font-weight:700; color:var(--brand,#6B2387); } 1182 .os-balon-satir.b .os-balon-ad { color:var(--soft-info-fg,#25579E); } 1183 .os-balon-tur { font-size:11px; color:var(--text-tertiary,#A89DB5); } 1184 .os-balon-soru { font-size:12px; color:var(--text-secondary,#6F6280); margin-bottom:4px; } 1185 .os-balon-metin { font-size:13.5px; line-height:1.5; color:var(--text,#2A1E35); white-space:pre-wrap; overflow-wrap:anywhere; } 1186 1187 .os-oran { margin:6px 0 2px; } 1188 .os-oran-ust { display:flex; justify-content:space-between; gap:8px; font-size:12.5px; color:var(--text-secondary,#6F6280); margin-bottom:5px; } 1189 .os-oran-a b { color:var(--brand,#6B2387); font-size:15px; } 1190 .os-oran-b b { color:var(--soft-info-fg,#25579E); font-size:15px; } 1191 .os-oran-cubuk { display:flex; height:10px; border-radius:999px; overflow:hidden; background:var(--surface-2,#F4F0F7); } 1192 .os-oran-dolu-a { background:var(--brand,#6B2387); transition:width .3s; } 1193 .os-oran-dolu-b { background:var(--soft-info-fg,#25579E); transition:width .3s; } 1194 .os-hakem-neden { margin:8px 0 0; font-size:13px; color:var(--text-secondary,#6F6280); line-height:1.45; } 1195 .os-oy-dugmeler { display:flex; gap:10px; margin-top:6px; } 1196 .os-oy-verildi { display:flex; align-items:center; gap:6px; color:var(--success,#2A9D7C); font-size:13px; font-weight:600; } 1197 1198 .os-paylas-satir { display:flex; gap:8px; align-items:center; flex-wrap:wrap; } 1199 .os-paylas-satir input { flex:1; min-width:180px; border:1px solid var(--border,#E6DFEC); border-radius:8px; padding:8px 10px; font-size:12.5px; font-family:inherit; background:var(--surface-2,#F4F0F7); color:var(--text,#2A1E35); } 1200 1201 .os-overlay { position:fixed; inset:0; background:rgba(20,10,30,.45); z-index:300; display:flex; align-items:center; justify-content:center; padding:16px; } 1202 .os-modal { width:min(560px, 100%); max-height:90vh; overflow:auto; background:var(--surface,#fff); color:var(--text,#2A1E35); border-radius:14px; box-shadow:0 18px 50px rgba(0,0,0,.25); display:flex; flex-direction:column; } 1203 .os-modal-head { display:flex; align-items:center; justify-content:space-between; padding:14px 16px 8px; } 1204 .os-modal-title { font-size:16px; font-weight:700; } 1205 .os-modal-x { border:none; background:transparent; color:var(--text-secondary,#6F6280); cursor:pointer; display:inline-flex; padding:4px; } 1206 .os-modal-govde { padding:0 16px 8px; display:flex; flex-direction:column; gap:14px; } 1207 .os-modal-alt { display:flex; justify-content:flex-end; gap:8px; padding:10px 16px 16px; } 1208 .os-form-bolum:empty { display:none; } 1209 .os-form-hata { padding:8px 10px; border-radius:8px; background:var(--danger-soft,rgba(184,56,56,.1)); color:var(--danger,#B83838); font-size:12px; } 1210 .os-alan { display:flex; flex-direction:column; gap:4px; font-size:12px; color:var(--text-secondary,#6F6280); } 1211 .os-alan select, .os-alan textarea { border:1px solid var(--border,#E6DFEC); border-radius:8px; padding:8px 10px; font-size:13px; font-family:inherit; background:var(--surface-2,#F4F0F7); color:var(--text,#2A1E35); width:100%; box-sizing:border-box; resize:vertical; } 1212 .os-alan select:focus, .os-alan textarea:focus { outline:none; border-color:var(--brand,#6B2387); } 1213 .os-secenekler { display:flex; flex-direction:column; gap:6px; } 1214 .os-secenek { display:flex; align-items:flex-start; gap:10px; padding:9px 10px; border-radius:var(--os-radius); border:1px solid var(--border,#E6DFEC); background:var(--surface,#fff); cursor:pointer; text-align:left; font-family:inherit; color:var(--text,#2A1E35); } 1215 .os-secenek.on { border-color:var(--brand,#6B2387); background:var(--accent-soft,rgba(107,35,135,.06)); } 1216 .os-secenek.kapali { opacity:.6; cursor:default; } 1217 .os-secenek-nokta { flex:none; width:14px; height:14px; border-radius:50%; border:2px solid var(--border-strong,#C9BFD3); margin-top:2px; } 1218 .os-secenek.on .os-secenek-nokta { border-color:var(--brand,#6B2387); background:var(--brand,#6B2387); box-shadow:inset 0 0 0 2.5px var(--surface,#fff); } 1219 .os-secenek-metin { display:flex; flex-direction:column; gap:2px; min-width:0; } 1220 .os-secenek-ad { font-size:13px; font-weight:600; } 1221 .os-secenek-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); line-height:1.4; } 1222 .os-secenek-neden { font-size:11.5px; color:var(--warning,#D6961E); } 1223 .os-secili { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:999px; background:var(--accent-soft,rgba(107,35,135,.1)); color:var(--brand,#6B2387); font-size:13px; font-weight:600; width:fit-content; max-width:100%; } 1224 .os-secili-ikon { display:inline-flex; } 1225 .os-secili-ad { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 1226 .os-secili-alt { font-weight:500; color:var(--text-secondary,#6F6280); } 1227 .os-secili-yok { display:flex; align-items:center; gap:10px; flex-wrap:wrap; font-size:12px; color:var(--text-tertiary,#A89DB5); } 1228 .os-cip-x { border:none; background:transparent; color:inherit; cursor:pointer; display:inline-flex; padding:2px; } 1229 .os-arama { display:flex; align-items:center; gap:8px; border:1px solid var(--border,#E6DFEC); border-radius:8px; padding:0 10px; background:var(--surface-2,#F4F0F7); color:var(--text-tertiary,#A89DB5); } 1230 .os-arama input { flex:1; border:none; background:transparent; padding:8px 0; font-size:13px; font-family:inherit; color:var(--text,#2A1E35); outline:none; min-width:0; } 1231 .os-arama-sonuc { display:flex; flex-direction:column; gap:4px; margin-top:6px; max-height:220px; overflow:auto; } 1232 .os-arama-ipucu { font-size:12px; color:var(--text-tertiary,#A89DB5); padding:6px 2px; } 1233 .os-arama-satir { display:flex; align-items:center; gap:8px; padding:6px 8px; border-radius:8px; border:1px solid var(--border,#E6DFEC); background:var(--surface,#fff); cursor:pointer; text-align:left; font-family:inherit; color:var(--text,#2A1E35); } 1234 .os-arama-satir:hover { border-color:var(--brand,#6B2387); } 1235 .os-arama-metin { display:flex; flex-direction:column; min-width:0; } 1236 .os-arama-ad { font-size:13px; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 1237 .os-arama-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } 1238 .os-anahtar { display:flex; align-items:flex-start; gap:10px; cursor:pointer; } 1239 .os-anahtar.kapali { opacity:.6; cursor:default; } 1240 .os-anahtar input { margin-top:3px; accent-color:var(--brand,#6B2387); } 1241 .os-anahtar-metin { display:flex; flex-direction:column; gap:2px; } 1242 .os-anahtar-ad { font-size:13px; font-weight:600; color:var(--text,#2A1E35); } 1243 .os-anahtar-alt { font-size:11.5px; color:var(--text-tertiary,#A89DB5); line-height:1.4; } 1244 1245 @media (max-width: 640px) { 1246 .os-host { padding:16px 14px 48px; } 1247 .os-balon-satir.a { padding-right:16px; } 1248 .os-balon-satir.b { padding-left:16px; } 1249 .os-taraflar-orta { padding-top:8px; } 1250 .os-oy-dugmeler { flex-direction:column; } 1251 } 1252 `; 1253 document.head.appendChild(s); 1254}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.