Kul olmak, Allah'ı zikretmeyi gerektirir. İstediğim duaları ve zikirleri bir araya getirebilsem, istediğim zaman ekleme, çıkarma düzenleme yapabilsem, sayaç bilgisi olsa, hedefimi raporlayabilsem düşüncesiyle ortaya çıkan bir sayfa.
"Kayıt"; sekmesine tıkladığınızda, Sol taraftaki menüden başlık ve metin girebilirsiniz. Girdiğiniz dua ve zikirleri düzenleyebilir, sağ alanda oluşan zikirlerin solundaki oklara basılı tutarak sıralamayı değiştirebilirsiniz.
"Kayıt"; sekmesin en alt tarafında bulunan butonlarla, veriyi içeri ve dışarı aktarabilirsiniz.
"Seç", butonuna bastığınızda sayaç numaratorü orta alanda yer alır üzerine bastıkça artar.
Kayıt alanına girilen zikirleri "Seç" ekranında açılır, listeden seçebilirsiniz.
Hedef default 100 olarak sabitlendi. Bu hedefi manuel değiştirebilirsiniz. hedefe ulaşınca bip bip sesi duyulur. Sayacı sıfırla butonu bulunur.
"Raporla"; menüsünde tüm çekilen zikirler tarih aralığında listelenir. Sıfırla butonuna basarak bu alanı temizleyebilirsiniz. (Şifre: 9)
Aşağıdaki kodu kopyalayıp not defterine yapıştırıp "kul.html" uzantılı olarak kaydedin. Çift tıklayarak Chrome gibi bir bowser ile açmanız yeterlidir.
+ Projenin Android telefonlar için mobil uygulamaya dönüştürülmüş hali aşağıdaki paylaşımdadır. "KUL.apk" indir.
HTML Kod:
<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#103d39"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; connect-src 'none'; form-action 'none'; base-uri 'none'"><title>KUL · Zikir Sayacı</title><link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='16' fill='%23103d39'/%3E%3Ctext x='32' y='43' font-size='38' text-anchor='middle' fill='white'%3EK%3C/text%3E%3C/svg%3E"><style>
:root{font-family:system-ui,-apple-system,"Segoe UI",sans-serif;color:#173c38;background:#f1f5f4;font-synthesis:none;--green:#103d39;--muted:#59716d;--line:#d9e4e0}*{box-sizing:border-box}body{margin:0}button,input,textarea,select{font:inherit}button,a,input,textarea,select{-webkit-tap-highlight-color:transparent}button{cursor:pointer;touch-action:manipulation}button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:3px solid #b57a27;outline-offset:3px}header{background:var(--green);color:white;display:flex;justify-content:space-between;align-items:center;padding:18px clamp(14px,4vw,64px);gap:10px}a{color:inherit;text-decoration:none}.brand{font-family:Georgia,serif;font-size:30px;letter-spacing:5px;font-weight:bold}nav{display:flex;gap:6px}nav a{padding:11px 14px;border-radius:9px;font-size:14px}nav a[aria-current=page]{background:#ffffff21}nav a:hover{background:#ffffff16}main{width:100%;padding:26px clamp(16px,4vw,64px) 35px}section[hidden]{display:none!important}.heading{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:22px}h1{font-size:clamp(24px,3vw,32px);font-weight:600;letter-spacing:-.7px;margin:0 0 7px}h2{font-size:19px;margin:0 0 18px}p{line-height:1.6}.sub,.muted{color:var(--muted);font-size:14px;margin:0}.eyebrow{text-transform:uppercase;letter-spacing:2px;font-size:12px;color:var(--muted);margin-bottom:12px}.card{background:#fff;border:1px solid var(--line);border-radius:18px;padding:24px}.btn{min-height:44px;border:1px solid var(--line);border-radius:9px;padding:11px 17px;background:white;color:var(--green);font-weight:600}.btn.primary{background:var(--green);color:white;border-color:var(--green)}.btn.danger{color:#a03636;border-color:#e9cdcd}.btn:disabled{opacity:.4;cursor:default}.row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}label{display:block;font-size:14px;font-weight:600;margin-bottom:8px}input,textarea,select{width:100%;min-width:0;background:white;color:#173c38;border:1px solid #bccfc8;border-radius:9px;padding:12px;min-height:44px}textarea{resize:vertical;min-height:110px}small{font-size:13px;color:var(--muted)}.form-grid{display:grid;grid-template-columns: minmax(240px,1fr) minmax(0,2fr);gap:22px;align-items:start}.field{margin-bottom:17px}.record{display:flex;align-items:center;gap:15px;border-top:1px solid var(--line);padding:18px 0}.record:first-child{border:0}.record-info{min-width:0;flex:1}.record strong,.record p{overflow-wrap:anywhere}.record p{margin:5px 0 0;color:var(--muted);white-space:pre-wrap}.record .btn{padding:9px 12px}.empty{padding:30px 10px;text-align:center;color:var(--muted)}#counter-view{max-width:1100px;margin:auto}.counter-top{display:flex;justify-content:space-between;align-items:end;gap:16px;margin-bottom:18px}.picker{flex:1;max-width:460px}.counter-card{text-align:center;padding:24px;background:var(--green);color:#fff;border-radius:22px;box-shadow:0 15px 40px #103d3912}.counter-caption{font-size:12px;letter-spacing:2px;color:#bdcfc8;text-transform:uppercase}.tap{width:100%;border:0;color:white;background:transparent;display:flex;flex-direction:column;align-items:center;padding:8px;user-select:none;border-radius:14px}.tap:active{background:#ffffff0a}.number{font-size:clamp(64px,13vw,110px);font-weight:700;font-variant-numeric:tabular-nums;line-height:1.25;max-width:100%;overflow-wrap:anywhere}.tap-hint{font-size:14px;color:#c3d7d1;margin:6px 0 18px}.progress{height:5px;background:#ffffff20;border-radius:9px;overflow:hidden;max-width:520px;margin:0 auto 12px}.progress i{display:block;height:100%;width:0;background:#d8b875;transition:width .15s}.goal-status{font-size:14px;color:#e1d4b7;min-height:21px}.counter-controls{justify-content:center;margin:19px 0}.target{display:flex;align-items:center;gap:10px}.target label{margin:0;white-space:nowrap}.target input{width:100px}.text-card{position:relative;text-align:center;min-height:160px;display:flex;flex-direction:column;justify-content:center}.text-window{height:clamp(130px,25vh,250px);display:flex;align-items:center;justify-content:center;overflow:auto}.zikir-text{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.65;width:100%;margin:0;font-family:Georgia,"Times New Roman",serif;font-size:36px}.footer{display:flex;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-top:24px;font-size:12px;color:var(--muted)}.filters{display:flex;gap:16px;flex-wrap:wrap;align-items:end}.filters .field{margin:0;flex:1;min-width:170px}.stats{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin:20px 0}.stat strong{display:block;font-size:34px;margin-top:8px;font-variant-numeric:tabular-nums}.table-wrap{overflow:auto}table{width:100%;border-collapse:collapse;text-align:left}th,td{padding:16px 12px;border-bottom:1px solid var(--line);overflow-wrap:anywhere}th{color:var(--muted);font-size:13px;font-weight:500}td:last-child,th:last-child{text-align:right;white-space:nowrap}.notice{background:#fff0dc;color:#7b4615;padding:14px;border-radius:9px;margin:16px 0;font-size:14px}.backup{margin-top:24px;border-top:1px solid var(--line);padding-top:20px}dialog{border:1px solid var(--line);border-radius:18px;width:calc(100% - 32px);max-width:440px;padding:26px;color:var(--green)}dialog::backdrop{background:#082e2a90}dialog .row{justify-content:flex-end;margin-top:20px}#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#172e2b;color:white;border-radius:12px;padding:13px 20px;max-width:calc(100% - 30px);box-shadow:0 5px 30px #0002;z-index:10;font-size:14px}#toast:empty{display:none}@media(max-width:650px){header{padding:14px 16px}.brand{font-size:25px;letter-spacing:3px}nav{gap:0}nav a{padding:10px;font-size:13px}main{padding-top:21px}.form-grid{grid-template-columns:1fr}.card{padding:18px}.heading{align-items:start}.record{flex-wrap:wrap}.record-info{flex-basis:65%}.counter-card{padding:19px 12px}.counter-controls{gap:15px}.counter-top{display:block}.picker{max-width:none}.stats{gap:10px}.stat strong{font-size:30px}th,td{padding:13px 6px}.filters .field{min-width:120px}}@media print{header, .filters,.report-actions,.footer,#toast,.notice{display:none!important}main{padding:0}.card{box-shadow:none}body{background:white}}
.reorder-handle{flex:0 0 44px;width:44px;min-height:48px;border:1px solid var(--line);border-radius:9px;background:#f1f5f4;color:var(--green);font-size:24px;cursor:grab;touch-action:none;user-select:none}.reorder-handle:active{cursor:grabbing}.record.dragging{background:#edf5f1;outline:2px solid #7ca398;border-radius:10px;opacity:.85}.record.dragging *{user-select:none}body.reordering{cursor:grabbing}.record-info{flex-basis:0}.record-actions{display:flex;gap:8px;flex-wrap:wrap}@media(max-width:650px){.record-info{flex-basis:calc(100% - 70px)}.record-actions{margin-left:59px}}
/* Keep the mobile counter within the available viewport; long text scrolls inside its panel. */
html{width:100%;overflow-x:hidden}body{min-width:0}header,main,section,.card,.picker,.record-info{min-width:0}.text-window{min-height:0;overscroll-behavior:contain;-webkit-overflow-scrolling:touch}.zikir-text{flex-shrink:0}.counter-top .picker{min-width:0}#choose{max-width:100%;text-overflow:ellipsis}#selected-title{overflow-wrap:anywhere}.text-window:focus-visible{outline:2px solid #b57a27;outline-offset:2px}
@media(max-width:760px),(pointer:coarse){
header{padding-top:max(10px,env(safe-area-inset-top));padding-left:max(12px,env(safe-area-inset-left));padding-right:max(12px,env(safe-area-inset-right));flex-shrink:0}
body[data-view="index"]{height:100vh;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
body[data-view="index"] main{flex:1;min-height:0;display:flex;flex-direction:column;padding:12px max(12px,env(safe-area-inset-right)) max(10px,env(safe-area-inset-bottom)) max(12px,env(safe-area-inset-left));overflow:auto}
body[data-view="index"] #counter-view{width:100%;flex:1;min-height:0;display:flex;flex-direction:column;margin:0 auto}
.counter-top{flex:none;margin-bottom:10px}.counter-top .eyebrow{margin-bottom:6px}.picker{max-width:none}.picker label{margin-bottom:5px}.picker select{padding:9px;min-height:42px}
.counter-card{flex:none;padding:12px;border-radius:16px}.number{font-size:clamp(48px,12vw,76px);line-height:1.1}.tap{padding:5px}.tap-hint{margin:5px 0 8px}.progress{margin-bottom:7px}.counter-controls{flex:none;margin:10px 0;gap:8px}.target{gap:7px}.target input{width:90px;padding:8px}.counter-controls .btn{padding:9px 13px}
.text-card{flex:1;min-height:90px;padding:12px;overflow:hidden}.text-card .eyebrow{flex:none;margin-bottom:8px;font-size:12px;letter-spacing:1px}.text-window{flex:1;height:auto;min-height:0;width:100%;overflow:auto}.footer{flex:none;gap:3px;margin-top:8px;font-size:12px}.footer span{line-height:1.3}
}
@media(max-height:540px) and (orientation:landscape){
body[data-view="index"] #counter-view{display:grid;grid-template-columns:minmax(180px,.8fr) minmax(0,1.2fr);grid-template-rows:auto minmax(90px,1fr) auto;gap:8px}
.counter-top{grid-column:1 / -1;display:flex;align-items:center;gap:10px;margin:0}.counter-top .eyebrow{margin:0}.counter-top .picker{display:flex;align-items:center;gap:8px;max-width:none}.picker label{white-space:nowrap;margin:0}.counter-card{grid-column:1;grid-row:2;align-content:center}.text-card{grid-column:2;grid-row:2 / 4}.counter-controls{grid-column:1;grid-row:3;margin:0}.footer{display:none}.number{font-size:48px}.counter-controls small{display:none}
}
</style></head><body>
<header><a class="brand" href="#index" aria-label="KUL ana sayfa">KUL</a><nav aria-label="Ana menü"><a href="#kayit" data-route="kayit">Kayıt</a><a href="#index" data-route="index">Seç</a><a href="#rapor" data-route="rapor">Raporla</a></nav></header>
<main><div id="storage-warning" class="notice" hidden role="alert"></div>
<section id="counter-view"><div class="counter-top"><div><div class="eyebrow">Zikir vakti</div></div><div class="picker"><label for="choose">Zikir seçin</label><select id="choose"></select></div></div>
<div class="counter-card"><div class="counter-caption">Mevcut sayaç</div><button id="tap" class="tap" aria-label="Zikir sayısını bir artır"><span id="count" class="number">0</span><span class="tap-hint">Saymak için dokunun · +1</span></button><div class="progress" role="progressbar" aria-label="Hedef ilerlemesi" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><i id="progress-fill"></i></div><div id="goal-status" class="goal-status" aria-live="polite"></div></div>
<div class="row counter-controls"><div class="target"><label for="target">Hedef</label><input id="target" type="number" min="0" max="999999999" step="1" inputmode="numeric" aria-describedby="target-help"></div><button id="reset-counter" class="btn">Sıfırla</button><small id="target-help">0 = hedefsiz</small></div>
<div class="card text-card"><div class="eyebrow" id="selected-title">Seçilen zikir</div><div class="text-window" tabindex="0" role="region" aria-label="Seçilen zikir metni; uzun metinleri kaydırarak okuyabilirsiniz"><p id="selected-text" class="zikir-text" dir="auto"></p></div></div><div class="footer"><span>Her dokunuş rapora kaydedilir.</span><span>Sayaç sıfırlansa da geçmiş korunur.</span></div></section>
<section id="records-view" hidden><div class="heading"><div><div class="eyebrow">Kişisel listeniz</div><h1>Zikir kayıtları</h1><p class="sub">Metinlerinizi ekleyin, düzenleyin ve seçin. Sıralamak için satırın solundaki tutamacı yukarı veya aşağı sürükleyin.</p></div></div><div class="form-grid"><div class="card"><h2 id="form-title">Yeni kayıt</h2><form id="record-form"><div class="field"><label for="name">Başlık</label><input id="name" maxlength="100" required placeholder="Örn. Salavat"></div><div class="field"><label for="text">Zikir metni</label><textarea id="text" maxlength="5000" required dir="auto" placeholder="Okumak istediğiniz metni yazın…"></textarea></div><div class="row"><button class="btn primary" type="submit">Kaydet</button><button id="cancel-edit" type="button" class="btn" hidden>Vazgeç</button></div></form></div><div class="card"><h2>Kayıtlı zikirler <small id="record-total"></small></h2><div id="records"></div></div></div><div class="backup"><h2>Verilerinizi yedekleyin</h2><p class="sub">Kayıtlar bu cihaz ve tarayıcıda tutulur. Tarayıcı verileri silinirse kaybolabilir. Dosya adı veya konumu değişirse eski verilere erişim değişebilir.</p><div class="row" style="margin-top:14px"><button class="btn" id="export">Yedek indir</button><button class="btn" id="import">Yedek yükle</button><input type="file" id="backup-file" accept=".json,application/json,.xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" hidden></div><p class="sub" style="margin-top:12px">Yedekler telefonda ve tablette JSON, bilgisayarda Excel (zikir_yedek.xlsx) olarak indirilir. Her iki dosya türü de yüklenebilir. Yedekler zikirleri, sayaçları, hedefleri ve tüm geçmişi içerir. Sayfa adlarını ve sütun başlıklarını koruyun. Tek dosya, internet gerektirmez. Mobilde HTML dosyasını JavaScript destekleyen bir tarayıcıda açın; dosya önizleyicileri çalıştırmayabilir.</p></div></section>
<section id="report-view" hidden><div class="heading"><div><div class="eyebrow">Zikir günlüğünüz</div><h1>Rapor</h1><p class="sub">Seçtiğiniz tarihler dahil, zikir bazında toplam adetler.</p></div><button id="report-reset" class="btn danger">Sıfırla</button></div><form id="report-form" class="card filters"><div class="field"><label for="from">Başlangıç tarihi</label><input id="from" type="date" required></div><div class="field"><label for="to">Bitiş tarihi</label><input id="to" type="date" required></div><button class="btn primary">Raporu göster</button></form><p id="range-label" class="sub" style="margin-top:18px"></p><div class="stats"><div class="card stat"><span class="sub">Toplam zikir adedi</span><strong id="report-total">0</strong></div><div class="card stat"><span class="sub">Farklı zikir</span><strong id="report-kinds">0</strong></div></div><div class="card"><div class="table-wrap"><table><thead><tr><th>Zikir</th><th>Adet</th></tr></thead><tbody id="report-body"></tbody></table></div><p class="empty" id="report-empty">Bu tarih aralığında henüz sayım yok.</p></div><div class="footer"><span>Tarihler cihazınızın yerel saatine göre kaydedilir.</span><span>Silinen zikirlerin geçmişi raporda korunur.</span></div></section>
</main><dialog id="modal"><form id="modal-form"><h2 id="modal-title"></h2><p id="modal-text"></p><div id="password-field" hidden><label for="password">Şifre</label><input type="password" id="password" inputmode="numeric" autocomplete="off"><p id="password-error" role="alert"></p></div><div class="row"><button type="button" id="modal-cancel" class="btn">Vazgeç</button><button type="submit" class="btn primary" id="modal-ok">Onayla</button></div></form></dialog><div id="toast" role="status"></div>
<script>
'use strict';
const $=id=>document.getElementById(id),KEY='kul.offline.v1',fmt=n=>n.toLocaleString('tr-TR');
const day=()=>{const d=new Date();return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0')};
const uid=()=>Date.now().toString(36)+Math.random().toString(36).slice(2,10);
const EMBEDDED_ZIKIRS=[{"id": "mu5qwogtxe6qcxib", "name": "Zihin açıklığı ve Başarı duası", "text": "Rabbi zidnî ilmen ve fehmen ve el-hıknî bi's sâlihîn.\n(Rabbim! İlmimi ve anlayışımı artır ve beni salih (iyi) kullarının arasına kat.)", "count": 0, "target": 100}, {"id": "mu5qf19w80g193hb", "name": "İşin kolaylaşması, dildeki bağın çözülmesi", "text": "Rabbişrah lî sadrî. Ve yessir lî emrî. Vahlul ukdeten min lisânî. Yefkahû kavlî.\n(Rabbim! Göğsümü genişlet (içimi ferahlat), işimi bana kolaylaştır, dilimdeki bağı çöz ki sözümü iyi anlasınlar.)", "count": 0, "target": 100}, {"id": "mu5qp0sfstjmbdhw", "name": "Borçtan kurtulma ve Helal rızık duası", "text": "Allâhümmekfinî bihalâlike an harâmike ve ağninî bifadlike ammen sivâk.\n(Allah'ım! Bana helâl kıldığın nimetlerle yetinmemi sağla ve beni haram kıldıklarından uzak tut. Lütfun ve ihsanınla beni Senden başkasına muhtaç etme.)", "count": 0, "target": 100}, {"id": "mu5p4unyo4qizxm1", "name": "Zarardan Korunma Duası", "text": "Bismillâhillezi Lâ Yedurru Ma'asmihî Şey'ün Fil Ardi Ve Lâ Fissemâi ve Hüves-Semî'ul Alîm\n(İsmi sayesinde yerde ve gökte hiçbir şeyin zarar veremeyeceği Allah'ın adıyla (başlarım). O, her şeyi hakkıyla işiten ve her şeyi hakkıyla bilendir.)", "count": 0, "target": 100}, {"id": "mu5p8m7872owreqk", "name": "Tevekkül duası", "text": "Lâ ilâhe illallâhü vahdehû lâ şerîke leh, lehül-mülkü ve lehül-hamdü ve hüve alâ külli şey’in kadîr.\n(Allah, bize yeter, O ne güzel vekildir. Ne güzel dost ve ne güzel yardımcıdır.)", "count": 0, "target": 100}, {"id": "mu5pjtqw4rn5e07x", "name": "Tesbihat duası", "text": "Sübhânallâhi velhamdülillâhi velâ ilâhe illallâhü vallâhü ekber, velâ havle velâ kuvvete illâ billâhil aliyyil azîm.\n(Allah'ı her türlü noksanlıktan ve kusurdan tenzih ederim, her türlü övgü ve şükür yalnızca O'na mahsustur, O'ndan başka hiçbir ilah yoktur, O en büyüktür ve ibadetlere yönelip günahlardan korunmak için gereken güç ile kuvvet ancak yüce ve azamet sahibi olan Allah'ın yardımıyladır.)", "count": 0, "target": 100}, {"id": "mu5r14q3qcyy6633", "name": "İstikamet ve Hidayet duası", "text": "Rabbi'rzukne't-tevfîka ve'l-istikâmete alâ dînike ve tâ'atike bi-rahmetike yâ erhame'r-râhimîn ve yâ ekreme'l-ekremîn.\n(Rabbim! Ey merhametlilerin en merhametlisi ve ey ikram edenlerin en cömerdi! Rahmetinle bize senin dinin üzerinde dosdoğru yolda kalmayı (istikameti), sana hakkıyla itaat etmeyi ve bu yolda muvaffak olmayı lütfeyle.)", "count": 0, "target": 100}, {"id": "mu5r5lmhd2w9d938", "name": "Tevekkül, Yardım isteme ve Şükür duası", "text": "Hasbünallâhu ve ni'mel vekîl. İyyâke na'büdü ve iyyâke neste'în. Velhamdülillâhi rabbil âlemîn.\n(Allah bize yeter, O ne güzel vekildir; yalnız sana ibadet edip yalnız senden yardım dileriz ve her türlü övgü ile şükür, âlemlerin Rabbi olan Allah'a mahsustur.)", "count": 0, "target": 100}, {"id": "mu5r877xf88jbhri", "name": "Fetih, Başarı ve Hayırlı kapıların açılması duası", "text": "İnnâ fetahnâ leke fethan mübînâ.\n(Şüphesiz biz sana apaçık bir fetih (zafer ve başarı) ihsan ettik.)", "count": 0, "target": 100}, {"id": "mu5rfzr0sow2wcj8", "name": "İsm-i Azam Duası", "text": "Eûzü bi'llâhi mine'ş-şeytâni'r-racîm.\nBismillâhi'r-rahmâni'r-rahîm.\n\nAllâhümme innî es'elüke bi-ennî leke'l-hamdü lâ ilâhe illâ ente'l-Mennânü, yâ Hannânü, yâ Mennânü, yâ Bedi'as-semâvâti ve'l-ard, Yâ ze'l-celâli ve'l-ikrâm. Yâ Hayyu, Yâ Kayyûm.\nLâ ilâhe illâ ente sübhâneke innî küntü mine'z-zâlimîn.\n\nAllâhümme innî es'elüke bi-ennî eşhedü enneke ente'l-lâhü lâ ilâhe illâ ente'l-Ehadü's-Samedüllezi lem-yelid, ve lem yüled, ve lem yekün lehü küfüven Ehad.\nElif Lâm Mîm, Allâhü lâ ilâhe illâ Hüve'l-Hayyu'l-Kayyûm.\nVe ilâhüküm ilâhün Vâhidün lâ ilâhe illâ Hüve'r-Rahmânü'r-Rahîm. Yâ ze'l-celâli ve'l-ikrâm. Yâ Erhame'r-Râhimîn.\n\nAllâhümme innî es'elüke bi-enneke ente'l-lâhü lâ ilâhe, illâ ente'l-Vâhidü'l-Ehadü'l-Ferdü's-Samedüllezi lem yelid ve lem yüled, ve lem yekün lehü küfüven Ehad.\nLâ ilâhe illâllâhü vahdehü lâ şerîke leh, lehü'l-mülkü ve lehü'l-hamdü ve Hüve alâ külli şey'in Kadîr. Lâ ilâhe illâllâhü ve lâ havle ve lâ kuvvete illâ billâhi'l-Aliyyi'l-Azîm.\nEs'elüke bismike'l-e'azze'l-ecelli'l-ekrem. Rabbî, Rabbî, Rabbî, Yâ Rabbî, Yâ Rabbî, yâ Rabbî.\nLâ ilâhe illâllâhü Vahdehü lâ şerîke leh, lehü'l-mülkü ve lehü'l-hamdü ve Hüve alâ külli şey'in Kadîr.\nElhamdüli'llâhi ve Sübhân-Allâhi ve'l-hamdüli'llâhi ve lâ ilâhe illâllâhü va'llâhü Ekber, ve lâ havle ve lâ kuvvete illâ billâhi'l-Aliyyi'l-Azîm.\n\n(Kovulmuş şeytanın şerrinden Allah'a sığınırım.\nRahman ve Rahim olan Allah'ın adıyla.\n\nAllah'ım! Her türlü övgü Sana mahsustur, Senden başka ilah yoktur. Sen Mennân'sın (çokça nimet verensin), Ey Hannân (çok merhametli olan), Ey Mennân, Ey gökleri ve yeri eşsiz güzellikte yoktan var eden, Ey Celal ve İkram sahibi. Ey Hayy (daima diri olan) ve Kayyûm (her şeyi ayakta tutan)! Senden başka hiçbir ilah yoktur, Seni her türlü noksanlıktan tenzih ederim, şüphesiz ben zalimlerden oldum.\n\nAllah'ım! Senin Allah olduğuna, Senden başka ilah olmadığına, Senin Ehad (tek ve eşsiz) ve Samed (hiçbir şeye muhtaç olmayan) olduğuna, doğurmadığına, doğurulmadığına ve hiçbir şeyin Sana denk ve eş olmadığına şahitlik ederek Senden isterim.\nElif, Lâm, Mîm. Allah, O'ndan başka ilah yoktur, O Hayy'dır, Kayyûm'dur.\nSizin ilahınız tek bir ilahtır. O'ndan başka ilah yoktur. O Rahman'dır, Rahim'dir. Ey Celal ve İkram sahibi! Ey merhametlilerin en merhametlisi!\n\nAllah'ım! Senin Allah olduğuna, Senden başka ilah olmadığına, Vâhid (bir), Ehad (tek), Ferd (eşsiz) ve Samed olduğuna, doğurmadığına, doğurulmadığına ve hiçbir şeyin Sana denk olmadığına inanarak Senden isterim.\nAllah'tan başka ilah yoktur. O tektir, O'nun hiçbir ortağı yoktur. Mülk O'nundur, hamd O'na mahsustur ve O, her şeye hakkıyla gücü yetendir. Allah'tan başka ilah yoktur. Güç ve kuvvet ancak yüce ve azamet sahibi olan Allah'ın yardımıyladır.\nSenin en izzetli, en yüce ve en keremli isminle Senden isterim. Rabbim, Rabbim, Rabbim, Ey Rabbim, Ey Rabbim, Ey Rabbim!\nAllah'tan başka ilah yoktur. O tektir, O'nun hiçbir ortağı yoktur. Mülk O'nundur, hamd O'na mahsustur ve O, her şeye hakkıyla gücü yetendir.\nHamd Allah'a mahsustur. Allah'ı her türlü noksanlıktan tenzih ederim. Hamd Allah'a mahsustur. Allah'tan başka ilah yoktur. Allah en büyüktür. İbadetlere yönelmek ve günahlardan korunmak için gereken güç ve kuvvet, ancak yüce ve azamet sahibi olan Allah'ın yardımıyladır.)", "count": 0, "target": 100}];
const fresh=()=>({version:1,defaultsRevision:1,embeddedRevision:1,selected:EMBEDDED_ZIKIRS[0].id,items:EMBEDDED_ZIKIRS.map(i=>({...i})),history:[]});
function valid(s){const num=x=>Number.isSafeInteger(x)&&x>=0;return s&&s.version===1&&Array.isArray(s.items)&&Array.isArray(s.history)&&s.items.length<=10000&&s.history.length<=1000000&&s.items.every(i=>i&&typeof i.id==='string'&&typeof i.name==='string'&&i.name.trim()&&i.name.length<=100&&typeof i.text==='string'&&i.text.trim()&&i.text.length<=5000&&num(i.count)&&num(i.target)&&i.target<=999999999)&&new Set(s.items.map(i=>i.id)).size===s.items.length&&s.history.every(h=>h&&typeof h.id==='string'&&typeof h.name==='string'&&typeof h.text==='string'&&/^\d{4}-\d{2}-\d{2}$/.test(h.date)&&num(h.count))&&(s.selected===null||s.items.some(i=>i.id===s.selected))}
let state=fresh(),editing=null,timer,action=null;
function warn(message){$('storage-warning').textContent=message;$('storage-warning').hidden=false}
try{const raw=localStorage.getItem(KEY);if(raw){const saved=JSON.parse(raw);if(!valid(saved))throw Error('invalid');state=saved}}catch(e){warn('Kaydedilmiş veriler okunamadı veya tarayıcı depolamayı engelliyor. Yedeğiniz varsa Kayıt bölümünden yükleyin.');}
function persist(next){try{localStorage.setItem(KEY,JSON.stringify(next));state=next;$('storage-warning').hidden=true;return true}catch(e){warn('Değişiklik kaydedilemedi. Tarayıcı depolama iznini ve boş alanı kontrol edin. Mevcut verilerinizi yedekleyin.');return false}}
function change(fn){const next=JSON.parse(JSON.stringify(state));fn(next);return persist(next)}
function toast(t){$('toast').textContent=t;clearTimeout(timer);timer=setTimeout(()=>$('toast').textContent='',3500)}
const selected=()=>state.items.find(i=>i.id===state.selected);
function ask(title,message,fn,password=false){action=fn;$('modal-title').textContent=title;$('modal-text').textContent=message;$('password-field').hidden=!password;$('password').value='';$('password-error').textContent='';$('modal').showModal();if(password)$('password').focus()}
$('modal-cancel').onclick=()=>$('modal').close();$('modal-form').onsubmit=e=>{e.preventDefault();if(!$('password-field').hidden&&$('password').value!=='9'){$('password-error').textContent='Şifre hatalı.';return}const fn=action;$('modal').close();fn?.()};
function fitText(){const el=$('selected-text'),box=el.parentElement;if($('counter-view').hidden)return;let size=44;el.style.fontSize=size+'px';while(size>16&&(el.scrollHeight>box.clientHeight||el.scrollWidth>box.clientWidth)){size--;el.style.fontSize=size+'px'}box.style.alignItems=el.scrollHeight>box.clientHeight?'flex-start':'center'}
function renderCounter(){const i=selected();$('choose').replaceChildren();if(!state.items.length){$('choose').add(new Option('Önce Kayıt bölümünden zikir ekleyin',''))}for(const item of state.items)$('choose').add(new Option(item.name,item.id));$('choose').value=state.selected||'';$('tap').disabled=!i;$('reset-counter').disabled=!i;$('target').disabled=!i;$('count').textContent=fmt(i?.count||0);$('target').value=i?.target||0;$('selected-title').textContent=i?.name||'Henüz kayıt yok';$('selected-text').textContent=i?.text||'Kayıt bölümünden bir zikir ekleyin.';const pct=i?.target?Math.min(100,100*i.count/i.target):0;$('progress-fill').style.width=pct+'%';document.querySelector('.progress').setAttribute('aria-valuenow',Math.round(pct));$('goal-status').textContent=i?.target?(i.count>=i.target?'Hedef tamamlandı · '+fmt(i.target):fmt(i.count)+' / '+fmt(i.target)+' · Kalan '+fmt(i.target-i.count)):'Hedefsiz sayım';requestAnimationFrame(fitText)}
$('choose').onchange=e=>{if(change(s=>s.selected=e.target.value))renderCounter()};
$('tap').onclick=()=>{const i=selected();if(!i||i.count>=Number.MAX_SAFE_INTEGER)return;prepareAudio();const today=day();if(change(s=>{s.items.find(x=>x.id===i.id).count++;let h=s.history.find(h=>h.id===i.id&&h.date===today&&h.text===i.text&&h.name===i.name);if(h)h.count++;else s.history.push({id:i.id,name:i.name,text:i.text,date:today,count:1})})){renderCounter();if(i.target&&i.count+1===i.target){goalAlarm();toast('Hedefinize ulaştınız. Dilerseniz saymaya devam edebilirsiniz.')}}};
$('target').onchange=e=>{const n=Number(e.target.value);if(!Number.isInteger(n)||n<0||n>999999999){toast('0 ile 999.999.999 arasında tam sayı girin.');renderCounter();return}change(s=>s.items.find(i=>i.id===s.selected).target=n);renderCounter()};
$('reset-counter').onclick=()=>ask('Sayaç sıfırlansın mı?','Yalnızca seçili zikrin mevcut sayacı sıfırlanır. Rapor geçmişi korunur.',()=>{if(change(s=>s.items.find(i=>i.id===s.selected).count=0)){renderCounter();toast('Sayaç sıfırlandı.')}});
function cancelEdit(){editing=null;$('record-form').reset();$('form-title').textContent='Yeni kayıt';$('cancel-edit').hidden=true}$('cancel-edit').onclick=cancelEdit;
function renderRecords(){$('records').replaceChildren();$('record-total').textContent='('+state.items.length+')';if(!state.items.length){const p=document.createElement('p');p.className='empty';p.textContent='İlk zikrinizi ekleyerek başlayın.';$('records').append(p)}for(const item of state.items){const row=document.createElement('div');row.className='record';row.dataset.id=item.id;const handle=document.createElement('button');handle.type='button';handle.className='reorder-handle';handle.textContent='↕';handle.setAttribute('aria-label',item.name+' — Sıralamak için sürükleyin veya yukarı/aşağı ok tuşlarını kullanın');handle.title='Sürükleyerek sıralayın · Klavye: ↑ ↓';handle.onpointerdown=e=>startReorder(e,row,handle);handle.onkeydown=e=>{if(e.key!=='ArrowUp'&&e.key!=='ArrowDown')return;e.preventDefault();const ids=state.items.map(i=>i.id),from=ids.indexOf(item.id),to=from+(e.key==='ArrowUp'?-1:1);if(to<0||to>=ids.length)return;[ids[from],ids[to]]=[ids[to],ids[from]];saveOrder(ids,item.id)};row.append(handle);const info=document.createElement('div');info.className='record-info';const name=document.createElement('strong');name.textContent=item.name;const text=document.createElement('p');text.dir='auto';text.textContent=item.text;info.append(name,text);row.append(info);const actions=document.createElement('div');actions.className='record-actions';for(const [label,fn] of [['Seç',()=>{if(change(s=>s.selected=item.id))location.hash='index'}],['Düzenle',()=>{editing=item.id;$('name').value=item.name;$('text').value=item.text;$('form-title').textContent='Kaydı düzenle';$('cancel-edit').hidden=false;$('name').focus()}],['Sil',()=>ask('Kayıt silinsin mi?',item.name+' listeden kaldırılacak. Sayım geçmişi raporda kalacak.',()=>{if(change(s=>{s.items=s.items.filter(i=>i.id!==item.id);if(s.selected===item.id)s.selected=s.items[0]?.id||null})){if(editing===item.id)cancelEdit();renderRecords();renderCounter()}})]]){const b=document.createElement('button');b.className='btn'+(label==='Sil'?' danger':'');b.textContent=label;b.setAttribute('aria-label',item.name+' — '+label);b.onclick=fn;actions.append(b)}row.append(actions);$('records').append(row)}}
$('record-form').onsubmit=e=>{e.preventDefault();const name=$('name').value.trim(),text=$('text').value.trim();if(!name||!text){toast('Başlık ve zikir metni boş olamaz.');return}if(change(s=>{if(editing){const i=s.items.find(i=>i.id===editing);i.name=name;i.text=text}else{const id=uid();s.items.push({id,name,text,count:0,target:100});if(!s.selected)s.selected=id}})){cancelEdit();renderRecords();toast('Kayıt kaydedildi.')}};
const dateLabel=d=>new Date(d+'T12:00:00').toLocaleDateString('tr-TR');
$('from').value=day().slice(0,8)+'01';$('to').value=day();
function renderReport(){const from=$('from').value,to=$('to').value;if(!from||!to||from>to){toast('Geçerli bir tarih aralığı seçin.');return}$('range-label').textContent=dateLabel(from)+' — '+dateLabel(to);const groups=new Map();for(const h of state.history){if(h.date<from||h.date>to||h.count===0)continue;const key=JSON.stringify([h.id,h.name,h.text]);const old=groups.get(key);if(old)old.count+=h.count;else groups.set(key,{...h})}let total=0;const ids=new Set();$('report-body').replaceChildren();for(const g of [...groups.values()].sort((a,b)=>b.count-a.count)){total+=g.count;ids.add(g.id);const tr=document.createElement('tr'),a=document.createElement('td'),b=document.createElement('td'),strong=document.createElement('strong'),p=document.createElement('p');strong.textContent=g.name;p.textContent=g.text;p.dir='auto';p.className='sub';p.style.whiteSpace='pre-wrap';a.append(strong,p);b.textContent=fmt(g.count);tr.append(a,b);$('report-body').append(tr)}$('report-total').textContent=fmt(total);$('report-kinds').textContent=fmt(ids.size);$('report-empty').hidden=groups.size>0}
$('report-form').onsubmit=e=>{e.preventDefault();renderReport()};
$('report-reset').onclick=()=>ask('Tüm sayım geçmişi sıfırlansın mı?','Seçili tarih aralığından bağımsız olarak TÜM rapor geçmişi ve mevcut sayaçlar silinir. Zikir metinleri ve hedefler korunur. Bu işlem geri alınamaz. Sıfırlama şifresi: 9.',()=>{if(change(s=>{s.history=[];s.items.forEach(i=>i.count=0)})){renderReport();renderCounter();toast('Tüm sayım geçmişi sıfırlandı.')}},true);
// Self-contained XLSX backup: ZIP + Office Open XML. No network or external library.
const XLSX_MIME='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
const utf8=new TextEncoder(),unutf8=new TextDecoder();
const xmlEscape=v=>String(v).replace(/_x[0-9a-f]{4}_/gi,m=>'_x005F_'+m.slice(1)).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,c=>'_x'+c.charCodeAt(0).toString(16).padStart(4,'0')+'_').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/\r/g,' ');
const excelDecode=s=>s.replace(/_x([0-9a-f]{4})_/gi,(_,n)=>String.fromCharCode(parseInt(n,16)));
const crcTable=Array.from({length:256},(_,n)=>{for(let k=0;k<8;k++)n=n&1?0xedb88320^(n>>>1):n>>>1;return n>>>0});
function crc32(bytes){let n=0xffffffff;for(const b of bytes)n=crcTable[(n^b)&255]^(n>>>8);return (n^0xffffffff)>>>0}
function zipStore(files){const chunks=[],directory=[];let offset=0;for(const [name,content] of Object.entries(files)){const filename=utf8.encode(name),data=utf8.encode(content),crc=crc32(data),header=new Uint8Array(30+filename.length),h=new DataView(header.buffer);h.setUint32(0,0x04034b50,true);h.setUint16(4,20,true);h.setUint16(6,0x800,true);h.setUint16(12,33,true);h.setUint32(14,crc,true);h.setUint32(18,data.length,true);h.setUint32(22,data.length,true);h.setUint16(26,filename.length,true);header.set(filename,30);chunks.push(header,data);const central=new Uint8Array(46+filename.length),c=new DataView(central.buffer);c.setUint32(0,0x02014b50,true);c.setUint16(4,20,true);c.setUint16(6,20,true);c.setUint16(8,0x800,true);c.setUint16(14,33,true);c.setUint32(16,crc,true);c.setUint32(20,data.length,true);c.setUint32(24,data.length,true);c.setUint16(28,filename.length,true);c.setUint32(42,offset,true);central.set(filename,46);directory.push(central);offset+=header.length+data.length}const end=new Uint8Array(22),e=new DataView(end.buffer),length=directory.reduce((n,c)=>n+c.length,0);e.setUint32(0,0x06054b50,true);e.setUint16(8,directory.length,true);e.setUint16(10,directory.length,true);e.setUint32(12,length,true);e.setUint32(16,offset,true);return new Blob([...chunks,...directory,end],{type:XLSX_MIME})}
const xmlHead='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
const sheetNS='http://schemas.openxmlformats.org/spreadsheetml/2006/main';
function sheetXml(rows,widths){return xmlHead+'<worksheet xmlns="'+sheetNS+'"><sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews><cols>'+widths.map((w,i)=>'<col min="'+(i+1)+'" max="'+(i+1)+'" width="'+w+'" customWidth="1"/>').join('')+'</cols><sheetData>'+rows.map((row,r)=>'<row r="'+(r+1)+'">'+row.map((v,c)=>{const ref=String.fromCharCode(65+c)+(r+1),style=r===0?1:typeof v==='number'?3:2;return typeof v==='number'?'<c r="'+ref+'" s="'+style+'"><v>'+v+'</v></c>':'<c r="'+ref+'" s="'+style+'" t="inlineStr"><is><t xml:space="preserve">'+xmlEscape(v??'')+'</t></is></c>'}).join('')+'</row>').join('')+'</sheetData></worksheet>'}
function makeBackup(s){const sheets=[['Zikirler',[['Kimlik','Başlık','Zikir metni','Sayaç','Hedef'],...s.items.map(i=>[i.id,i.name,i.text,i.count,i.target])],[24,28,72,16,16]],['Geçmiş',[['Kimlik','Başlık','Zikir metni','Tarih (YYYY-AA-GG)','Adet'],...s.history.map(h=>[h.id,h.name,h.text,h.date,h.count])],[24,28,72,24,16]],['Ayarlar',[['Anahtar','Değer'],['Biçim','KUL_XLSX'],['Sürüm',1],['Seçili zikir',s.selected||'']],[24,30]]];
const files={'[Content_Types].xml':xmlHead+'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'+sheets.map((_,i)=>'<Override PartName="/xl/worksheets/sheet'+(i+1)+'.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>').join('')+'</Types>',
'_rels/.rels':xmlHead+'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>',
'xl/workbook.xml':xmlHead+'<workbook xmlns="'+sheetNS+'" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>'+sheets.map(([name],i)=>'<sheet name="'+name+'" sheetId="'+(i+1)+'" r:id="rId'+(i+1)+'"/>').join('')+'</sheets></workbook>',
'xl/_rels/workbook.xml.rels':xmlHead+'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'+sheets.map((_,i)=>'<Relationship Id="rId'+(i+1)+'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet'+(i+1)+'.xml"/>').join('')+'<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>',
'xl/styles.xml':xmlHead+'<styleSheet xmlns="'+sheetNS+'"><fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><color rgb="FFFFFFFF"/><sz val="11"/><name val="Calibri"/></font></fonts><fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FF103D39"/><bgColor indexed="64"/></patternFill></fill></fills><borders count="1"><border/></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs><cellXfs count="4"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/><xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf><xf numFmtId="3" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>'};sheets.forEach(([,rows,widths],i)=>files['xl/worksheets/sheet'+(i+1)+'.xml']=sheetXml(rows,widths));return zipStore(files)}
async function unzipBackup(buffer){const bytes=new Uint8Array(buffer),v=new DataView(buffer);if(bytes.length<22)throw Error('Dosya bir Excel çalışma kitabı değil.');let end=-1;for(let i=bytes.length-22;i>=Math.max(0,bytes.length-65557);i--)if(v.getUint32(i,true)===0x06054b50&&i+22+v.getUint16(i+20,true)===bytes.length){end=i;break}if(end<0)throw Error('Excel dosyası okunamadı.');const count=v.getUint16(end+10,true);let p=v.getUint32(end+16,true),total=0;const files=new Map();if(count>2000)throw Error('Çalışma kitabı çok büyük.');for(let n=0;n<count;n++){if(p+46>bytes.length||v.getUint32(p,true)!==0x02014b50)throw Error('Geçersiz Excel dosyası.');const flags=v.getUint16(p+8,true),method=v.getUint16(p+10,true),crc=v.getUint32(p+16,true),size=v.getUint32(p+20,true),rawSize=v.getUint32(p+24,true),nl=v.getUint16(p+28,true),extra=v.getUint16(p+30,true),comment=v.getUint16(p+32,true),local=v.getUint32(p+42,true);total+=rawSize;if(total>64*1024*1024||flags&1)throw Error('Yedek çok büyük veya şifreli.');const name=unutf8.decode(bytes.slice(p+46,p+46+nl));if(local+30>bytes.length||v.getUint32(local,true)!==0x04034b50)throw Error('Excel dosyası bozuk.');const start=local+30+v.getUint16(local+26,true)+v.getUint16(local+28,true);if(start+size>bytes.length)throw Error('Excel dosyası eksik.');let data=bytes.slice(start,start+size);if(method===8){if(typeof DecompressionStream==='undefined')throw Error('Bu Excel dosyası için güncel bir Chrome veya Edge tarayıcısı kullanın.');const reader=new Blob([data]).stream().pipeThrough(new DecompressionStream('deflate-raw')).getReader(),parts=[];let length=0;while(true){const {done,value}=await reader.read();if(done)break;length+=value.length;if(length>rawSize){await reader.cancel();throw Error('Excel dosyasının boyutu geçersiz.')}parts.push(value)}data=new Uint8Array(length);let at=0;for(const part of parts){data.set(part,at);at+=part.length}}else if(method!==0)throw Error('Desteklenmeyen Excel sıkıştırması.');if(data.length!==rawSize||crc32(data)!==crc)throw Error('Excel dosyası bozuk.');if(files.has(name))throw Error('Excel dosyasında yinelenen bölüm var.');files.set(name,unutf8.decode(data));p+=46+nl+extra+comment}return files}
function xmlDoc(text){if(!text||/<!DOCTYPE|<!ENTITY/i.test(text))throw Error('Excel yapısı geçersiz.');const doc=new DOMParser().parseFromString(text,'application/xml');if(doc.getElementsByTagName('parsererror').length)throw Error('Excel XML içeriği okunamadı.');return doc}
const tags=(el,name)=>Array.from(el.getElementsByTagNameNS('*',name));
function parseBackup(files){const book=xmlDoc(files.get('xl/workbook.xml')),rels=xmlDoc(files.get('xl/_rels/workbook.xml.rels')),relationships=new Map(tags(rels,'Relationship').map(r=>[r.getAttribute('Id'),r]));const pathFor=r=>{if(!r||r.getAttribute('TargetMode')==='External')throw Error('Yedek sayfası bulunamadı.');const target=r.getAttribute('Target');const parts=(target.startsWith('/')?target.slice(1):'xl/'+target).split('/'),out=[];for(const p of parts){if(p==='..')out.pop();else if(p&&p!=='.')out.push(p)}return out.join('/')};const sharedRel=tags(rels,'Relationship').find(r=>r.getAttribute('Type').endsWith('/sharedStrings'));const sharedPath=sharedRel?pathFor(sharedRel):'xl/sharedStrings.xml';const strings=files.has(sharedPath)?tags(xmlDoc(files.get(sharedPath)),'si').map(si=>excelDecode(tags(si,'t').map(t=>t.textContent).join(''))):[];const sheets=new Map(tags(book,'sheet').map(s=>[s.getAttribute('name'),pathFor(relationships.get(s.getAttribute('r:id')||s.getAttributeNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships','id')))]));
function rows(name,headers){const path=sheets.get(name);if(!path)throw Error(name+' sayfası bulunamadı.');const rows=tags(xmlDoc(files.get(path)),'row').map(r=>{const a=[];for(const c of tags(r,'c')){if(tags(c,'f').length)throw Error('Yedek hücrelerinde formül yerine değer kullanın.');const ref=c.getAttribute('r'),m=/^([A-Z]+)\d+$/.exec(ref);if(!m)throw Error('Hücre adresi geçersiz.');let col=0;for(const letter of m[1])col=col*26+letter.charCodeAt(0)-64;if(col>headers.length)continue;const type=c.getAttribute('t'),value=tags(c,'v')[0]?.textContent||'';a[col-1]=type==='inlineStr'?excelDecode(tags(c,'t').map(t=>t.textContent).join('')):type==='s'?strings[Number(value)]:type==='str'?excelDecode(value):type==='b'||type==='e'?null:value===''?'':Number(value)}return a}).filter(r=>r.some(v=>v!==''&&v!==undefined));const header=rows.shift();if(!header||headers.some((h,i)=>header[i]!==h))throw Error(name+' sütun başlıkları geçersiz.');return rows}
const config=new Map(rows('Ayarlar',['Anahtar','Değer']).map(r=>[r[0],r[1]]));if(config.get('Biçim')!=='KUL_XLSX'||config.get('Sürüm')!==1)throw Error('Bu dosya KUL Excel yedeği değil.');const s={version:1,defaultsRevision:1,embeddedRevision:1,selected:config.get('Seçili zikir')||null,items:rows('Zikirler',['Kimlik','Başlık','Zikir metni','Sayaç','Hedef']).map(r=>({id:r[0],name:r[1],text:r[2],count:r[3],target:r[4]})),history:rows('Geçmiş',['Kimlik','Başlık','Zikir metni','Tarih (YYYY-AA-GG)','Adet']).map(r=>({id:r[0],name:r[1],text:r[2],date:r[3],count:r[4]}))};if(!valid(s))throw Error('Yedekteki kimlik, metin, sayaç, hedef veya tarih bilgileri geçersiz.');return s}
function isMobileDevice(){return navigator.userAgentData?.mobile===true||/Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent)||(/Mac/i.test(navigator.platform||'')&&navigator.maxTouchPoints>1)}
function backupDownload(){return isMobileDevice()?{blob:new Blob([JSON.stringify(state,null,2)],{type:'application/json'}),name:'kul-yedek-'+day()+'.json'}:{blob:makeBackup(state),name:'zikir_yedek.xlsx'}}
$('export').onclick=()=>{try{const {blob,name}=backupDownload();if(blob.size>20*1024*1024)throw Error('Yedek 20 MB sınırını aşıyor.');const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);toast('Yedek hazırlandı: '+name)}catch(e){toast(e.message||'Yedek hazırlanamadı.')}};
async function readBackupFile(file){if(file.size>20*1024*1024)throw Error('Yedek dosyası en fazla 20 MB olabilir.');let parsed;if(/\.json$/i.test(file.name)){try{parsed=JSON.parse((await file.text()).replace(/^\uFEFF/,''))}catch(e){throw Error('JSON yedek dosyası okunamadı.')}}else if(/\.xlsx$/i.test(file.name)){parsed=parseBackup(await unzipBackup(await file.arrayBuffer()))}else throw Error('KUL yedeği (.json veya .xlsx) seçin.');if(!valid(parsed))throw Error('Yedek verileri geçersiz. Mevcut kayıtlar değiştirilmedi.');parsed.defaultsRevision=1;parsed.embeddedRevision=1;return parsed}
$('import').onclick=()=>$('backup-file').click();$('backup-file').onchange=async e=>{const file=e.target.files[0];e.target.value='';if(!file)return;try{const parsed=await readBackupFile(file);ask('Yedek yüklensin mi?','Mevcut kayıtlar ve geçmiş bu yedekle değiştirilecek. Önce mevcut verilerinizi yedekleyebilirsiniz.',()=>{if(persist(parsed)){cancelEdit();renderRecords();renderCounter();renderReport();toast('Yedek yüklendi.')}})}catch(e){toast(e.message||'Geçerli bir KUL yedek dosyası seçin.')}};
// Apply the requested default to existing records once; later user targets stay intact.
if(state.defaultsRevision!==1){change(s=>{s.items.forEach(i=>i.target=100);s.defaultsRevision=1})}
// Install the supplied default list once, preserving personal records and report history.
if(state.embeddedRevision!==1){change(s=>{const old=new Map(s.items.map(i=>[i.id,i]));const legacy=new Map([['s1','Sübhanallah'],['s2','Elhamdülillah'],['s3','Allahu ekber']]);const supplied=new Set(EMBEDDED_ZIKIRS.map(i=>i.id));const personal=s.items.filter(i=>!supplied.has(i.id)&&legacy.get(i.id)!==i.text);s.items=[...EMBEDDED_ZIKIRS.map(i=>({...i,count:old.get(i.id)?.count||0})),...personal];if(!s.items.some(i=>i.id===s.selected))s.selected=s.items[0]?.id||null;s.embeddedRevision=1})}
let audioContext=null;
function prepareAudio(){try{const Audio=window.AudioContext||window.webkitAudioContext;if(!Audio)return null;if(!audioContext)audioContext=new Audio();if(audioContext.state==='suspended')audioContext.resume().catch(()=>{});return audioContext}catch(_){return null}}
async function goalAlarm(){try{const ctx=prepareAudio();if(!ctx)return;if(ctx.state!=='running')await ctx.resume();if(ctx.state!=='running')return;const now=ctx.currentTime+.03;for(const delay of [0,.3]){const oscillator=ctx.createOscillator(),gain=ctx.createGain();oscillator.type='sine';oscillator.frequency.value=880;gain.gain.setValueAtTime(0,now+delay);gain.gain.linearRampToValueAtTime(.2,now+delay+.015);gain.gain.setValueAtTime(.2,now+delay+.13);gain.gain.linearRampToValueAtTime(0,now+delay+.19);oscillator.connect(gain);gain.connect(ctx.destination);oscillator.start(now+delay);oscillator.stop(now+delay+.2);oscillator.onended=()=>{oscillator.disconnect();gain.disconnect()}}}catch(_){}}
function saveOrder(ids,focusId){if(ids.length!==state.items.length||new Set(ids).size!==ids.length||ids.some(id=>!state.items.some(i=>i.id===id))){renderRecords();return}const changed=ids.some((id,n)=>id!==state.items[n].id);if(changed&&change(s=>{const byId=new Map(s.items.map(i=>[i.id,i]));s.items=ids.map(id=>byId.get(id))}))toast('Sıralama kaydedildi.');renderRecords();for(const row of $('records').children)if(row.dataset.id===focusId)row.querySelector('.reorder-handle')?.focus({preventScroll:true})}
let activeReorder=null;
function startReorder(e,row,handle){
if(activeReorder||e.isPrimary===false||(e.pointerType==='mouse'&&e.button!==0))return;
e.preventDefault();
// Capture on a stable ancestor: moving a captured row can release capture.
const capture=document.documentElement,drag={id:e.pointerId,y:e.clientY,frame:null};
activeReorder=drag;row.classList.add('dragging');document.body.classList.add('reordering');
function place(){
const siblings=[...$('records').children].filter(el=>el!==row);
const before=siblings.find(el=>{const rect=el.getBoundingClientRect();return drag.y<rect.top+rect.height/2})||null;
if(row.nextElementSibling!==before)$('records').insertBefore(row,before);
}
function tick(){if(activeReorder!==drag)return;const edge=70,delta=drag.y<edge?-Math.ceil((edge-drag.y)/5):drag.y>innerHeight-edge?Math.ceil((drag.y-innerHeight+edge)/5):0;if(delta){window.scrollBy(0,Math.max(-20,Math.min(20,delta)));place()}drag.frame=requestAnimationFrame(tick)}
function move(ev){if(ev.pointerId!==drag.id)return;if(ev.cancelable)ev.preventDefault();drag.y=ev.clientY;place()}
function cleanup(commit){if(activeReorder!==drag)return;activeReorder=null;cancelAnimationFrame(drag.frame);
window.removeEventListener('pointermove',move,true);window.removeEventListener('pointerup',finish,true);window.removeEventListener('pointercancel',cancel,true);window.removeEventListener('blur',abort);window.removeEventListener('hashchange',abort);
try{if(capture.hasPointerCapture(drag.id))capture.releasePointerCapture(drag.id)}catch(_){}
document.body.classList.remove('reordering');row.classList.remove('dragging');
if(commit)saveOrder([...$('records').children].map(el=>el.dataset.id),row.dataset.id);else renderRecords();
}
function finish(ev){if(ev.pointerId!==drag.id)return;drag.y=ev.clientY;place();cleanup(true)}
function cancel(ev){if(ev.pointerId===drag.id)cleanup(false)}
function abort(){cleanup(false)}
window.addEventListener('pointermove',move,{capture:true,passive:false});window.addEventListener('pointerup',finish,true);window.addEventListener('pointercancel',cancel,true);window.addEventListener('blur',abort);window.addEventListener('hashchange',abort);
try{capture.setPointerCapture(drag.id)}catch(_){}
drag.frame=requestAnimationFrame(tick);
}
function route(){const key=['kayit','rapor'].includes(location.hash.slice(1))?location.hash.slice(1):'index';document.body.dataset.view=key;$('counter-view').hidden=key!=='index';$('records-view').hidden=key!=='kayit';$('report-view').hidden=key!=='rapor';document.querySelectorAll('nav a').forEach(a=>{if(a.dataset.route===key)a.setAttribute('aria-current','page');else a.removeAttribute('aria-current')});if(key==='index')renderCounter();if(key==='kayit')renderRecords();if(key==='rapor')renderReport();document.title='KUL · '+({index:'Zikir Sayacı',kayit:'Kayıtlar',rapor:'Rapor'}[key])}
window.addEventListener('hashchange',route);window.addEventListener('resize',fitText);if(window.visualViewport)window.visualViewport.addEventListener('resize',fitText);window.addEventListener('storage',e=>{if(e.key===KEY&&e.newValue){try{const next=JSON.parse(e.newValue);if(valid(next)){state=next;route()}}catch(_){}}});route();
</script></body></html>





