• Bugün, 06:13:43
    #1
    Selamlar, Cobanov.dev'in malum scripti bende yapayzekayla kendim için geliştirdim.
    İsteyen Varsa Kullanabilir. Kesinlikle Hesap kaydetme vs yoktur kodları taratabilirsiniz.
    Sadece "Cobanov.dev" 'in Editlenmiş Halidir kendim için yaptığım Kullanım talimatları altta.
    Kullanım talimatlarını istiyorsanız belirtin yazayım.

    Scriptin Özellikleri
    • Takip listesini tarar.
    • Seni geri takip etmeyen hesapları gösterir.
    • Yaklaşık 10 dakikada bir otomatik kontrol yapar.
    • Yeni takip etmeyen tespit edilirse bildirim gönderir.
    • İlk taramada mevcut durumu kaydeder.
    • Sonraki taramalarda değişiklikleri karşılaştırır.
    • Seçtiğin hesapları kullanıcı onayıyla takipten bırakır.
    • Hesapları otomatik olarak takipten bırakmaz.
    • Tarama sırasında istekler arasında bekleme kullanır.
    • Belirli aralıklarla mola verir.
    • Instagram hız sınırı uyguladığında işlemi durdurur veya bekler.
    • Başarısız istekleri yeniden dener.
    • Kullanıcı adı veya isme göre arama yapar.
    • Onaylı hesapları filtreler.
    • Gizli hesapları filtreler.
    • Profil fotoğrafı olmayan hesapları filtreler.
    • İstenmeyen hesapları listeden gizler.
    • Tüm hesapları seçme özelliği vardır.
    • Seçimleri temizleme özelliği vardır.
    • Profil bağlantılarını yeni sekmede açar.
    • Kullanıcı adlarını kopyalar.
    • Türkçe ve İngilizce dil desteği vardır.
    • Panel taşınabilir.
    • Panel küçültülebilir.
    • Panel kapatılabilir.
    • Ayarları tarayıcıda saklar.
    • Panel Instagram sekmesi açıkken çalışır.
    • Bilgisayar uykuya geçerse kontrol durur.
    • Instagram sekmesi kapanırsa kontrol durur.
    • Gerçek zamanlı saniyelik kontrol yapmaz; yaklaşık 10 dakikada bir kontrol eder.
    Kod:
    (() => { "use strict"; if (location.hostname !== "www.instagram.com") { alert("Open [url]https://www.instagram.com[/url] first, then paste this script again."); return; } const APP_ID = "iu-app"; const STYLE_ID = "iu-style"; const STORAGE_KEY = "iu_state_v3"; const IG_HEADERS = { "x-ig-app-id": "936619743392459", "x-requested-with": "XMLHttpRequest" }; const DEFAULT_TIMINGS = { scanDelayMin: 700, scanDelayMax: 1500, scanPauseEveryPages: 5, scanPauseMs: 8000, unfollowDelayMin: 5000, unfollowDelayMax: 9000, unfollowPauseEvery: 5, unfollowPauseMs: 300000 }; const PROGRESS_TITLE_ID = "iu-progress-title"; const PANEL_WIDTH = 380; const PANEL_MARGIN = 18; const MAX_RETRIES = 3; const I18N = { en: { title: "Instagram Unfollower", subtitle: "See who doesn't follow you back", welcomeTitle: "Ready when you are", welcomeBody: "We'll scan only the people you follow and use Instagram's follow-back status to keep the scan fast. Nothing is changed on your account during the scan.", scanBtn: "Scan now", scanning: "Scanning", loadingFollowing: "Loading the people you follow", paused: "Paused", pause: "Pause", resume: "Resume", cancel: "Cancel", ofTotal: "{current} of {total}", ofUnknown: "{current} so far", scanCompletedToast: "{count} non-followers found", scanFailed: "Scan failed", retry: "Try again", goBack: "Back to results", search: "Search by name or username", filterVerified: "Verified", filterPrivate: "Private", filterNoAvatar: "No profile picture", filterShowHidden: "Hidden users", foundCount: "{count} non-followers", foundOne: "1 non-follower", foundNone: "Nice — everyone you follow follows you back.", noMatches: "No users match your filters.", hide: "Hide", unhide: "Unhide", hideTooltip: "Hide from this list", unhideTooltip: "Show again", openProfile: "Open profile", copy: "Copy", copyAll: "Copy all", copiedToast: "Copied {count} usernames", selectAll: "Select all", selectNone: "Clear", selectedCount: "{count} selected", unfollow: "Unfollow", unfollowConfirmTitle: "Unfollow {count} users?", unfollowConfirmBody: "This will run slowly to protect your account from rate limiting. You can pause at any time, but already-completed unfollows cannot be reversed from this tool.", unfollowConfirmBtn: "Yes, unfollow {count}", unfollowing: "Unfollowing", currently: "Currently", nextActionIn: "Next action in {seconds}s", cooldownIn: "Cooldown — {seconds}s", scanPause: "Long pause — {seconds}s", unfollowDoneTitle: "Done", unfollowDoneBody: "{ok} unfollowed, {fail} failed", unfollowBlocked: "Instagram blocked this action, so {count} remaining users were skipped. Wait a few hours before trying again.", settings: "Settings", settingsTitle: "Timing settings", settingsBody: "Lower delays make Instagram more likely to throttle or block your account. Keep these conservative.", minScanDelay: "Min scan delay (ms)", maxScanDelay: "Max scan delay (ms)", scanPauseEvery: "Long pause every N pages", scanPauseLength: "Long pause length (ms)", minUnfollowDelay: "Min unfollow delay (ms)", maxUnfollowDelay: "Max unfollow delay (ms)", unfollowPauseEvery: "Cooldown every N unfollows", unfollowPauseLength: "Cooldown length (ms)", restoreDefaults: "Restore defaults", save: "Save", saved: "Settings saved", cookieMissing: "Could not read your login cookie. Make sure you are signed in.", csrfMissing: "Could not read csrftoken cookie.", requestFailed: "Request failed: {status}", tooManyRequests: "Instagram is rate-limiting requests. Try again later or increase delays in settings.", close: "Close", langSwitch: "Switch to Turkish", minimize: "Minimize", expand: "Expand", langCode: "TR", pillScanning: "Scanning {current}/{total}", pillUnfollowing: "Unfollowing {current}/{total}", pillResults: "{count} non-followers", pillIdle: "Open" }, tr: { title: "Instagram Takip Etmeyenler", subtitle: "Seni geri takip etmeyenleri gör", welcomeTitle: "Hazır olduğunda başlat", welcomeBody: "Sadece takip ettiklerin taranır ve Instagram'ın geri takip durumuyla hızlıca kontrol edilir. Tarama sırasında hesabında hiçbir şey değişmez.", scanBtn: "Taramayı başlat", scanning: "Taranıyor", loadingFollowing: "Takip ettiklerin yükleniyor", paused: "Duraklatıldı", pause: "Duraklat", resume: "Devam et", cancel: "İptal", ofTotal: "{current} / {total}", ofUnknown: "Şu ana kadar {current}", scanCompletedToast: "{count} takip etmeyen bulundu", scanFailed: "Tarama başarısız", retry: "Tekrar dene", goBack: "Sonuçlara dön", search: "İsim veya kullanıcı adı ara", filterVerified: "Onaylı", filterPrivate: "Gizli", filterNoAvatar: "Profil resmi yok", filterShowHidden: "Gizlenen kullanıcılar", foundCount: "{count} takip etmeyen", foundOne: "1 takip etmeyen", foundNone: "Harika — takip ettiğin herkes seni takip ediyor.", noMatches: "Filtrelerinle eşleşen kullanıcı yok.", hide: "Gizle", unhide: "Göster", hideTooltip: "Bu listeden gizle", unhideTooltip: "Tekrar göster", openProfile: "Profili aç", copy: "Kopyala", copyAll: "Tümünü kopyala", copiedToast: "{count} kullanıcı adı kopyalandı", selectAll: "Tümünü seç", selectNone: "Temizle", selectedCount: "{count} seçili", unfollow: "Takibi bırak", unfollowConfirmTitle: "{count} kullanıcının takibi bırakılsın mı?", unfollowConfirmBody: "Hesabını korumak için işlem yavaş çalışır. İstediğin zaman duraklatabilirsin, ama tamamlanan işlemler bu araçtan geri alınamaz.", unfollowConfirmBtn: "Evet, {count} kişiyi bırak", unfollowing: "Takip bırakılıyor", currently: "Şu an", nextActionIn: "Sonraki işlem {seconds} sn sonra", cooldownIn: "Mola — {seconds} sn", scanPause: "Uzun mola — {seconds} sn", unfollowDoneTitle: "Tamamlandı", unfollowDoneBody: "{ok} başarılı, {fail} başarısız", unfollowBlocked: "Instagram bu işlemi engelledi, kalan {count} kişi atlandı. Tekrar denemeden önce birkaç saat bekle.", settings: "Ayarlar", settingsTitle: "Hız ayarları", settingsBody: "Düşük gecikmeler Instagram'ın hesabını kısıtlamasına neden olabilir. Yavaş tut.", minScanDelay: "Min tarama gecikmesi (ms)", maxScanDelay: "Maks tarama gecikmesi (ms)", scanPauseEvery: "Her N sayfada uzun mola", scanPauseLength: "Uzun mola süresi (ms)", minUnfollowDelay: "Min takip bırakma gecikmesi (ms)", maxUnfollowDelay: "Maks takip bırakma gecikmesi (ms)", unfollowPauseEvery: "Her N takip bırakmada mola", unfollowPauseLength: "Mola süresi (ms)", restoreDefaults: "Varsayılana dön", save: "Kaydet", saved: "Ayarlar kaydedildi", cookieMissing: "Giriş çerezi okunamadı. Giriş yaptığından emin ol.", csrfMissing: "csrftoken çerezi okunamadı.", requestFailed: "İstek başarısız: {status}", tooManyRequests: "Instagram istekleri kısıtlıyor. Sonra dene veya ayarlardan gecikmeleri artır.", close: "Kapat", langSwitch: "İngilizce'ye geç", minimize: "Küçült", expand: "Aç", langCode: "EN", pillScanning: "Taranıyor {current}/{total}", pillUnfollowing: "Bırakılıyor {current}/{total}", pillResults: "{count} takip etmeyen", pillIdle: "Aç" } }; const SVG = { minimize: '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><rect x="3" y="7.25" width="10" height="1.5" rx="0.75" fill="currentColor"/></svg>', close: '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>', gear: '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zm6.7 2.5a6.7 6.7 0 0 0-.1-1.1l1.4-1.1-1.5-2.6-1.7.7a6.6 6.6 0 0 0-1.9-1.1L10.5 1h-3l-.4 1.8a6.6 6.6 0 0 0-1.9 1.1l-1.7-.7L1.9 5.8 3.3 6.9a6.7 6.7 0 0 0 0 2.2L1.9 10.2l1.5 2.6 1.7-.7a6.6 6.6 0 0 0 1.9 1.1L7.5 15h3l.4-1.8a6.6 6.6 0 0 0 1.9-1.1l1.7.7 1.5-2.6-1.4-1.1c.1-.4.1-.7.1-1.1z"/></svg>', open: '<svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M6 3h7v7M13 3L6.5 9.5M9 13H3V7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>', sparkle: '<svg viewBox="0 0 24 24" width="36" height="36" aria-hidden="true"><path fill="currentColor" d="M12 2l1.8 5.4L19 9l-5.2 1.6L12 16l-1.8-5.4L5 9l5.2-1.6L12 2zm6 11l1 2.8L22 17l-3 .8L18 21l-1-3.2L14 17l3-1.2 1-2.8z"/></svg>', alert: '<svg viewBox="0 0 24 24" width="36" height="36" aria-hidden="true"><path fill="currentColor" d="M12 2 1 21h22L12 2zm0 6 7.5 13h-15L12 8zm-1 4v4h2v-4h-2zm0 5v2h2v-2h-2z"/></svg>', check: '<svg viewBox="0 0 16 16" width="36" height="36" aria-hidden="true"><path fill="currentColor" d="M14 4.5L6 12.5l-4-4L3 7.5l3 3 7-7z"/></svg>' }; const persisted = loadStored(); const state = { mode: "idle", scanPaused: false, scanCancelled: false, unfollowPaused: false, unfollowBlocked: 0, progress: { current: 0, total: 0, label: "scanning", note: "" }, waitUntil: 0, waitReason: "", users: [], followingCount: 0, followersCount: 0, selected: new Set(), hidden: new Set(persisted.hidden || []), log: [], search: "", filters: persisted.filters || { verified: true, private: true, noAvatar: true, showHidden: false }, timings: { ...DEFAULT_TIMINGS, ...(persisted.timings || {}) }, panelPos: persisted.panelPos || null, minimized: Boolean(persisted.minimized), language: persisted.language === "tr" || persisted.language === "en" ? persisted.language : (String(navigator.language || "").toLowerCase().startsWith("tr") ? "tr" : "en"), lastNonFollowerIds: new Set(Array.isArray(persisted.lastNonFollowerIds) ? persisted.lastNonFollowerIds : []), pollIntervalMs: Number.isFinite(persisted.pollIntervalMs) ? Math.max(60000, persisted.pollIntervalMs) : 600000, error: "" }; let countdownTimer = null; let pollTimer = null; let toastTimer = null; let dialogCounter = 0; let closeActiveDialog = null; function loadStored() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } function persist() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ hidden: [...state.hidden], timings: state.timings, filters: state.filters, panelPos: state.panelPos, minimized: state.minimized, language: state.language, lastNonFollowerIds: [...state.lastNonFollowerIds], pollIntervalMs: state.pollIntervalMs })); } catch { } } function t(key, vars) { const dict = I18N[state.language] || I18N.en; const template = dict[key] ?? I18N.en[key] ?? key; if (!vars) return template; return template.replace(/\{(\w+)\}/g, (_, name) => vars[name] ?? ""); } function cleanupExisting() { if (typeof window.__iuCleanup === "function") { try { window.__iuCleanup(); } catch {   } } document.getElementById(APP_ID)?.remove(); document.getElementById(STYLE_ID)?.remove(); } function injectStyles() { const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = CSS; document.head.appendChild(style); } function mount() { const root = document.createElement("div"); root.id = APP_ID; document.body.appendChild(root); window.__iuCleanup = unmount; renderShell(); startPolling(); } function unmount() { stopPolling(); stopCountdown(); closeActiveDialog?.(); if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; } document.getElementById(APP_ID)?.remove(); document.getElementById(STYLE_ID)?.remove(); if (window.__iuCleanup === unmount) window.__iuCleanup = null; } function renderShell() { const root = document.getElementById(APP_ID); if (!root) return; if (state.minimized) { root.innerHTML = ` <button class="iu-pill" data-action="expand" type="button" aria-label="${escapeAttr(t("expand"))}"> <span class="iu-pill-dot ${pillStateClass()}"></span> <span data-pill-label>${escapeHTML(pillLabel())}</span> </button> `; root.querySelector("[data-action='expand']")?.addEventListener("click", () => setMinimized(false)); applyPanelPosition(); return; } root.innerHTML = ` <section class="iu-panel" role="dialog" aria-label="${escapeAttr(t("title"))}"> <header class="iu-header" data-drag> <div class="iu-brand"> <span class="iu-brand-dot"></span> <div class="iu-brand-text"> <strong>${escapeHTML(t("title"))}</strong> <span data-subtitle>${escapeHTML(t("subtitle"))}</span> </div> </div> <div class="iu-header-actions"> <button type="button" data-action="settings" aria-label="${escapeAttr(t("settings"))}" title="${escapeAttr(t("settings"))}">${SVG.gear}</button> <button type="button" data-action="language" aria-label="${escapeAttr(t("langSwitch"))}" title="${escapeAttr(t("langSwitch"))}"><span data-lang aria-hidden="true">${escapeHTML(t("langCode"))}</span></button> <button type="button" data-action="minimize" aria-label="${escapeAttr(t("minimize"))}" title="${escapeAttr(t("minimize"))}">${SVG.minimize}</button> <button type="button" data-action="close" aria-label="${escapeAttr(t("close"))}" title="${escapeAttr(t("close"))}">${SVG.close}</button> </div> </header> <div class="iu-body" data-body></div> </section> `; bindHeader(root); bindDrag(root.querySelector("[data-drag]")); applyPanelPosition(); renderBody(); } function bindHeader(root) { root.querySelector("[data-action='close']")?.addEventListener("click", () => unmount()); root.querySelector("[data-action='minimize']")?.addEventListener("click", () => setMinimized(true)); root.querySelector("[data-action='settings']")?.addEventListener("click", showSettings); root.querySelector("[data-action='language']")?.addEventListener("click", toggleLanguage); } function applyPanelPosition() { const root = document.getElementById(APP_ID); if (!root) return; const node = root.querySelector(".iu-panel") || root.querySelector(".iu-pill"); if (!node) return; const pos = state.panelPos; if (pos && Number.isFinite(pos.x) && Number.isFinite(pos.y)) { const max = panelBounds(node); node.style.left = clamp(pos.x, 0, max.x) + "px"; node.style.top = clamp(pos.y, 0, max.y) + "px"; node.style.right = "auto"; node.style.bottom = "auto"; } else { node.style.left = "auto"; node.style.top = "auto"; node.style.right = PANEL_MARGIN + "px"; node.style.bottom = PANEL_MARGIN + "px"; } } function panelBounds(node) { const rect = node.getBoundingClientRect(); return { x: Math.max(0, window.innerWidth - rect.width), y: Math.max(0, window.innerHeight - rect.height) }; } function bindDrag(handle) { if (!handle) return; const panel = handle.closest(".iu-panel"); if (!panel) return; let startX = 0; let startY = 0; let originX = 0; let originY = 0; let dragging = false; handle.addEventListener("pointerdown", (event) => { if (event.target.closest("button")) return; dragging = true; const rect = panel.getBoundingClientRect(); originX = rect.left; originY = rect.top; startX = event.clientX; startY = event.clientY; panel.style.left = originX + "px"; panel.style.top = originY + "px"; panel.style.right = "auto"; panel.style.bottom = "auto"; handle.setPointerCapture(event.pointerId); handle.classList.add("iu-dragging"); }); handle.addEventListener("pointermove", (event) => { if (!dragging) return; const max = panelBounds(panel); const x = clamp(originX + (event.clientX - startX), 0, max.x); const y = clamp(originY + (event.clientY - startY), 0, max.y); panel.style.left = x + "px"; panel.style.top = y + "px"; }); const stop = (event) => { if (!dragging) return; dragging = false; handle.releasePointerCapture?.(event.pointerId); handle.classList.remove("iu-dragging"); const rect = panel.getBoundingClientRect(); state.panelPos = { x: rect.left, y: rect.top }; persist(); }; handle.addEventListener("pointerup", stop); handle.addEventListener("pointercancel", stop); } function setMinimized(value) { state.minimized = Boolean(value); persist(); renderShell(); } function pillLabel() { if (state.mode === "scanning") { return t("pillScanning", { current: state.progress.current, total: state.progress.total || "?" }); } if (state.mode === "unfollowing") { return t("pillUnfollowing", { current: state.progress.current, total: state.progress.total }); } if (state.mode === "results" && state.users.length) { return t("pillResults", { count: state.users.filter((u) => !u.follows_viewer && !state.hidden.has(u.id)).length }); } return t("pillIdle"); } function pillStateClass() { if (state.mode === "scanning" || state.mode === "unfollowing") return "iu-pill-dot--active"; if (state.error) return "iu-pill-dot--error"; return ""; } function renderBody() { const body = document.querySelector(`#${APP_ID} [data-body]`); if (!body) return; if (state.mode === "scanning") { body.innerHTML = renderScanView(); bindScan(body); } else if (state.mode === "results") { body.innerHTML = renderResultsView(); bindResults(body); } else if (state.mode === "unfollowing" || state.mode === "unfollowDone") { body.innerHTML = renderUnfollowView(); bindUnfollow(body); } else { body.innerHTML = renderIdleView(); bindIdle(body); } if (state.mode === "scanning" || state.mode === "unfollowing") startCountdown(); else stopCountdown(); } function renderIdleView() { if (state.error) { return ` <div class="iu-welcome"> <div class="iu-welcome-icon iu-welcome-icon--error">${SVG.alert}</div> <h2>${escapeHTML(t("scanFailed"))}</h2> <p>${escapeHTML(state.error)}</p> <button type="button" class="iu-btn iu-btn--primary" data-action="scan">${escapeHTML(t("retry"))}</button> </div> `; } return ` <div class="iu-welcome"> <div class="iu-welcome-icon">${SVG.sparkle}</div> <h2>${escapeHTML(t("welcomeTitle"))}</h2> <p>${escapeHTML(t("welcomeBody"))}</p> <button type="button" class="iu-btn iu-btn--primary iu-btn--lg" data-action="scan">${escapeHTML(t("scanBtn"))}</button> </div> `; } function bindIdle(body) { body.querySelector("[data-action='scan']")?.addEventListener("click", startScan); } function renderBar(percent, valueText, known) { const now = known ? ` aria-valuenow="${percent}"` : ""; return ` <div class="iu-bar" role="progressbar" aria-labelledby="${PROGRESS_TITLE_ID}" aria-valuemin="0" aria-valuemax="100"${now} aria-valuetext="${escapeAttr(valueText)}" data-bar> <span data-progress-bar style="width:${percent}%"></span> </div>`; } function renderScanView() { const { current, total, label, note } = state.progress; const percent = total ? Math.min(100, Math.round((current / total) * 100)) : 0; const counter = total ? t("ofTotal", { current, total }) : t("ofUnknown", { current }); return ` <div class="iu-progress"> <div class="iu-progress-head"> <h2 id="${PROGRESS_TITLE_ID}" data-progress-label>${escapeHTML(state.scanPaused ? t("paused") : t(label))}</h2> <p data-progress-note>${escapeHTML(note)}</p> </div> ${renderBar(percent, counter, Boolean(total))} <div class="iu-progress-meta"> <span data-progress-counter>${escapeHTML(counter)}</span> <span data-countdown class="iu-muted"></span> </div> <div class="iu-progress-actions"> <button type="button" class="iu-btn" data-action="pause-scan">${escapeHTML(t(state.scanPaused ? "resume" : "pause"))}</button> <button type="button" class="iu-btn iu-btn--ghost" data-action="cancel-scan">${escapeHTML(t("cancel"))}</button> </div> </div> `; } function bindScan(body) { body.querySelector("[data-action='pause-scan']")?.addEventListener("click", () => { state.scanPaused = !state.scanPaused; const btn = body.querySelector("[data-action='pause-scan']"); if (btn) btn.textContent = t(state.scanPaused ? "resume" : "pause"); const label = body.querySelector("[data-progress-label]"); if (label) label.textContent = state.scanPaused ? t("paused") : t(state.progress.label); }); body.querySelector("[data-action='cancel-scan']")?.addEventListener("click", () => { state.scanCancelled = true; state.scanPaused = false; }); } function renderResultsView() { const display = getDisplayUsers(); const totalNonFollowers = state.users.filter((u) => !u.follows_viewer && !state.hidden.has(u.id)).length; const allSelected = display.length > 0 && display.every((u) => state.selected.has(u.id)); let summary; if (totalNonFollowers === 0) summary = t("foundNone"); else if (totalNonFollowers === 1) summary = t("foundOne"); else summary = t("foundCount", { count: totalNonFollowers }); return ` <div class="iu-results"> <div class="iu-results-summary">${escapeHTML(summary)}</div> <div class="iu-search-row"> <input class="iu-search" type="search" data-search placeholder="${escapeAttr(t("search"))}" value="${escapeAttr(state.search)}" autocomplete="off" spellcheck="false" > </div> <div class="iu-filters"> ${filterChip("verified", t("filterVerified"))} ${filterChip("private", t("filterPrivate"))} ${filterChip("noAvatar", t("filterNoAvatar"))} ${filterChip("showHidden", t("filterShowHidden"), state.hidden.size > 0 || state.filters.showHidden)} </div> <div class="iu-list" data-list>${renderUserList(display)}</div> <div class="iu-actionbar"> <div class="iu-actionbar-left"> <button type="button" class="iu-btn iu-btn--small" data-action="select-all" ${display.length ? "" : "disabled"}> ${escapeHTML(allSelected ? t("selectNone") : t("selectAll"))} </button> <span class="iu-muted" data-selected-label>${escapeHTML(t("selectedCount", { count: state.selected.size }))}</span> </div> <div class="iu-actionbar-right"> <button type="button" class="iu-btn iu-btn--small" data-action="copy" ${display.length ? "" : "disabled"}>${escapeHTML(state.selected.size ? t("copy") : t("copyAll"))}</button> <button type="button" class="iu-btn iu-btn--danger iu-btn--small" data-action="unfollow" ${state.selected.size ? "" : "disabled"}>${escapeHTML(t("unfollow"))}${state.selected.size ? " (" + state.selected.size + ")" : ""}</button> </div> </div> </div> `; } function filterChip(key, label, visible = true) { if (!visible && key === "showHidden") return ""; const active = state.filters[key]; return ` <button type="button" class="iu-chip ${active ? "iu-chip--on" : ""}" data-filter="${escapeAttr(key)}" aria-pressed="${active ? "true" : "false"}"> <span class="iu-chip-tick">${active ? SVG.check : ""}</span> ${escapeHTML(label)} </button> `; } function renderUserList(display) { if (!display.length) { return `<div class="iu-list-empty">${escapeHTML(t("noMatches"))}</div>`; } return display.map(renderUserRow).join(""); } function renderUserRow(user) { const hidden = state.hidden.has(user.id); const checked = state.selected.has(user.id); const tags = []; if (user.is_verified) tags.push(`<span class="iu-tag iu-tag--blue">${escapeHTML(t("filterVerified"))}</span>`); if (user.is_private) tags.push(`<span class="iu-tag">${escapeHTML(t("filterPrivate"))}</span>`); return ` <div class="iu-row ${hidden ? "iu-row--hidden" : ""} ${checked ? "iu-row--selected" : ""}" data-row="${escapeAttr(user.id)}" role="button" tabindex="0"> <input type="checkbox" class="iu-row-check" data-select="${escapeAttr(user.id)}" ${checked ? "checked" : ""} aria-label="${escapeAttr(user.username)}"> <img class="iu-avatar" src="${escapeAttr(user.profile_pic_url || "")}" alt="" loading="lazy" onerror="this.style.visibility='hidden'"> <div class="iu-row-text"> <div class="iu-row-name"> <a href="/${encodeURIComponent(user.username)}/" target="_blank" rel="noopener noreferrer" data-stop>@${escapeHTML(user.username)}</a> ${tags.join(" ")} </div> <div class="iu-row-sub">${escapeHTML(user.full_name || "")}</div> </div> <div class="iu-row-actions"> <a class="iu-icon-btn" href="/${encodeURIComponent(user.username)}/" target="_blank" rel="noopener noreferrer" data-stop title="${escapeAttr(t("openProfile"))}" aria-label="${escapeAttr(t("openProfile"))}">${SVG.open}</a> <button type="button" class="iu-icon-btn iu-text-btn" data-hide="${escapeAttr(user.id)}" data-stop title="${escapeAttr(hidden ? t("unhideTooltip") : t("hideTooltip"))}">${escapeHTML(hidden ? t("unhide") : t("hide"))}</button> </div> </div> `; } function bindResults(body) { const search = body.querySelector("[data-search]"); if (search) { search.addEventListener("input", (event) => { state.search = event.target.value; const list = body.querySelector("[data-list]"); if (list) list.innerHTML = renderUserList(getDisplayUsers()); updateActionBar(body); }); } body.querySelectorAll("[data-filter]").forEach((el) => { el.addEventListener("click", () => { const key = el.getAttribute("data-filter"); state.filters[key] = !state.filters[key]; pruneSelectionToDisplayed(); persist(); renderBody(); }); }); body.addEventListener("change", (event) => { const checkbox = event.target.closest("[data-select]"); if (!checkbox) return; const id = checkbox.getAttribute("data-select"); if (checkbox.checked) state.selected.add(id); else state.selected.delete(id); const row = checkbox.closest("[data-row]"); if (row) row.classList.toggle("iu-row--selected", checkbox.checked); updateActionBar(body); }); body.addEventListener("click", (event) => { const hideBtn = event.target.closest("[data-hide]"); if (hideBtn) { event.preventDefault(); event.stopPropagation(); const id = hideBtn.getAttribute("data-hide"); if (state.hidden.has(id)) state.hidden.delete(id); else { state.hidden.add(id); state.selected.delete(id); } persist(); renderBody(); return; } if (event.target.closest("[data-stop]")) { event.stopPropagation(); return; } const row = event.target.closest("[data-row]"); if (!row || event.target.closest(".iu-row-check")) return; const id = row.getAttribute("data-row"); const checkbox = row.querySelector("[data-select]"); if (!checkbox) return; if (state.selected.has(id)) { state.selected.delete(id); checkbox.checked = false; } else { state.selected.add(id); checkbox.checked = true; } row.classList.toggle("iu-row--selected", checkbox.checked); updateActionBar(body); }); body.addEventListener("keydown", (event) => { if (event.key !== " " && event.key !== "Enter") return; const row = event.target.closest("[data-row]"); if (!row || event.target.tagName === "INPUT" || event.target.tagName === "A" || event.target.tagName === "BUTTON") return; event.preventDefault(); row.click(); }); body.querySelector("[data-action='select-all']")?.addEventListener("click", () => { const display = getDisplayUsers(); const allSelected = display.length && display.every((u) => state.selected.has(u.id)); if (allSelected) display.forEach((u) => state.selected.delete(u.id)); else display.forEach((u) => state.selected.add(u.id)); renderBody(); }); body.querySelector("[data-action='copy']")?.addEventListener("click", copyUsernames); body.querySelector("[data-action='unfollow']")?.addEventListener("click", confirmUnfollow); } function updateActionBar(body) { const root = body || document.querySelector(`#${APP_ID} [data-body]`); if (!root) return; const display = getDisplayUsers(); const allSelected = display.length > 0 && display.every((u) => state.selected.has(u.id)); const selectAllBtn = root.querySelector("[data-action='select-all']"); if (selectAllBtn) { selectAllBtn.disabled = display.length === 0; selectAllBtn.textContent = allSelected ? t("selectNone") : t("selectAll"); } const copyBtn = root.querySelector("[data-action='copy']"); if (copyBtn) copyBtn.disabled = display.length === 0; updateSelectedLabel(root); } function updateSelectedLabel(body) { const root = body || document.querySelector(`#${APP_ID} [data-body]`); if (!root) return; const label = root.querySelector("[data-selected-label]"); if (label) label.textContent = t("selectedCount", { count: state.selected.size }); const unfollowBtn = root.querySelector("[data-action='unfollow']"); if (unfollowBtn) { unfollowBtn.disabled = state.selected.size === 0; unfollowBtn.textContent = t("unfollow") + (state.selected.size ? " (" + state.selected.size + ")" : ""); } const copyBtn = root.querySelector("[data-action='copy']"); if (copyBtn) copyBtn.textContent = state.selected.size ? t("copy") : t("copyAll"); } function renderUnfollowView() { const total = state.progress.total; const done = state.progress.current; const percent = total ? Math.round((done / total) * 100) : 0; const last = state.log[state.log.length - 1]; const summary = unfollowSummary(); return ` <div class="iu-progress"> <div class="iu-progress-head"> <h2 id="${PROGRESS_TITLE_ID}" data-progress-label>${escapeHTML(unfollowTitle())}</h2> <p data-progress-note>${escapeHTML(state.progress.note || "")}</p> </div> ${renderBar(percent, summary, Boolean(total))} <div class="iu-progress-meta"> <span data-progress-counter>${escapeHTML(summary)}</span> <span data-countdown class="iu-muted"></span> </div> <div class="iu-current" data-current ${last ? "" : "hidden"}> <span class="iu-muted">${escapeHTML(t("currently"))}</span> <strong data-current-user>${last ? "@" + escapeHTML(last.user.username) : ""}</strong> <span class="iu-tag ${last && !last.ok ? "iu-tag--red" : "iu-tag--green"}" data-current-tag title="${escapeAttr(last?.reason || "")}">${last && !last.ok ? "✕" : "✓"}</span> </div> ${state.unfollowBlocked ? ` <div class="iu-blocked">${escapeHTML(t("unfollowBlocked", { count: state.unfollowBlocked }))}</div>` : ""} <div class="iu-progress-actions"> ${state.mode === "unfollowing" ? ` <button type="button" class="iu-btn" data-action="pause-unfollow">${escapeHTML(t(state.unfollowPaused ? "resume" : "pause"))}</button> <button type="button" class="iu-btn iu-btn--ghost" data-action="cancel-unfollow">${escapeHTML(t("cancel"))}</button> ` : ` <button type="button" class="iu-btn iu-btn--primary" data-action="back-results">${escapeHTML(t("goBack"))}</button> `} </div> </div> `; } function unfollowTitle() { if (state.mode === "unfollowDone") return t("unfollowDoneTitle"); return state.unfollowPaused ? t("paused") : t("unfollowing"); } function unfollowSummary() { if (state.mode === "unfollowDone") { return t("unfollowDoneBody", { ok: state.log.filter((e) => e.ok).length, fail: state.log.filter((e) => !e.ok).length }); } return t("ofTotal", { current: state.progress.current, total: state.progress.total }); } function bindUnfollow(body) { body.querySelector("[data-action='pause-unfollow']")?.addEventListener("click", () => { state.unfollowPaused = !state.unfollowPaused; const btn = body.querySelector("[data-action='pause-unfollow']"); if (btn) btn.textContent = t(state.unfollowPaused ? "resume" : "pause"); const label = body.querySelector("[data-progress-label]"); if (label) label.textContent = unfollowTitle(); }); body.querySelector("[data-action='cancel-unfollow']")?.addEventListener("click", () => { state.unfollowCancelled = true; state.unfollowPaused = false; }); body.querySelector("[data-action='back-results']")?.addEventListener("click", () => { state.mode = "results"; state.log = []; state.unfollowCancelled = false; renderBody(); }); } async function ensureNotificationPermission() { if (!("Notification" in window)) return false; if (Notification.permission === "default") { try { await Notification.requestPermission(); } catch {} } return Notification.permission === "granted"; } function stopPolling() { if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; } } function startPolling() { stopPolling(); pollTimer = setTimeout(async () => { pollTimer = null; if (document.hidden || state.mode === "scanning" || state.mode === "unfollowing") { startPolling(); return; } try { await startScan({ silent: true }); } finally { if (document.getElementById(APP_ID)) startPolling(); } }, state.pollIntervalMs); } function notifyNewNonFollowers(users) { const currentIds = new Set(users.filter((u) => !u.follows_viewer).map((u) => u.id)); const previousIds = state.lastNonFollowerIds; const newlyDetected = users.filter((u) => currentIds.has(u.id) && !previousIds.has(u.id)); state.lastNonFollowerIds = currentIds; persist(); if (!previousIds.size || !newlyDetected.length) return; const names = newlyDetected.slice(0, 3).map((u) => "@" + u.username).join(", "); const suffix = newlyDetected.length > 3 ? " …" : ""; const message = (state.language === "tr" ? "Yeni takip etmeyen: " : "New non-followers: ") + names + suffix; toast(message); if ("Notification" in window && Notification.permission === "granted") { new Notification(state.language === "tr" ? "Instagram kontrolü" : "Instagram check", { body: message }); } } async function startScan(options = {}) { state.error = ""; if (!options.silent) await ensureNotificationPermission(); state.mode = "scanning"; state.scanPaused = false; state.scanCancelled = false; state.users = []; state.followingCount = 0; state.followersCount = 0; state.selected.clear(); state.log = []; state.progress = { current: 0, total: 0, label: "loadingFollowing", note: "" }; renderBody(); try { const viewerId = getCookie("ds_user_id"); if (!viewerId) throw new Error(t("cookieMissing")); const onPage = (results, totalGuess) => { state.progress = { current: results.length, total: totalGuess || 0, label: "loadingFollowing", note: "" }; updateProgressDOM(); }; const following = await fetchFollowingWithBackStatus(viewerId, onPage); if (state.scanCancelled) return resetToIdle(); state.followingCount = following.length; state.followersCount = following.filter((user) => user.follows_viewer).length; state.users = following; state.mode = "results"; const nonFollowers = state.users.filter((u) => !u.follows_viewer && !state.hidden.has(u.id)).length; notifyNewNonFollowers(state.users); if (!options.silent) toast(t("scanCompletedToast", { count: nonFollowers })); renderBody(); } catch (error) { console.error("[iu] scan failed:", error); state.error = error?.message || String(error) || t("scanFailed"); state.mode = "idle"; renderBody(); } } function resetToIdle() { state.mode = "idle"; state.users = []; state.scanCancelled = false; state.error = ""; renderBody(); } async function fetchFollowingWithBackStatus(viewerId, onPage) { const results = []; let cursor = ""; let page = 0; let totalGuess = 0; while (true) { await waitWhile(() => state.scanPaused && !state.scanCancelled); if (state.scanCancelled) return results; const variables = { id: viewerId, include_reel: true, fetch_mutual: false, first: 24 }; if (cursor) variables.after = cursor; const url = `/graphql/query/?query_hash=3dec7e2c57367ef3da3d987d89f9dbc8&variables=${encodeURIComponent(JSON.stringify(variables))}`; const json = await igFetch(url); const edge = json?.data?.user?.edge_follow; if (!edge?.edges) throw new Error(t("scanFailed")); const users = edge.edges.map((item) => normalizeUser(item.node)).filter((u) => u.id && u.username); results.push(...users); if (!totalGuess && typeof edge.count === "number") totalGuess = edge.count; onPage(results, totalGuess); cursor = edge.page_info?.end_cursor || ""; page += 1; if (!edge.page_info?.has_next_page || !cursor) break; await sleep(randomBetween(state.timings.scanDelayMin, state.timings.scanDelayMax)); if (state.timings.scanPauseEveryPages > 0 && page % state.timings.scanPauseEveryPages === 0) { await sleepWithCountdown(state.timings.scanPauseMs, "scanPause"); } } return dedupe(results); } async function igFetch(url, init = {}) { let attempt = 0; while (true) { const response = await fetch(url, { credentials: "include", headers: { ...IG_HEADERS, ...(init.headers || {}) }, ...init }); if (response.ok) return response.json(); if (response.status === 429 || (response.status >= 500 && response.status < 600)) { if (attempt >= MAX_RETRIES) { if (response.status === 429) throw new Error(t("tooManyRequests")); throw new Error(t("requestFailed", { status: response.status })); } const retryAfter = parseRetryAfter(response.headers.get("retry-after")); const wait = retryAfter || Math.min(60000, 5000 * Math.pow(2, attempt)); await sleepWithCountdown(wait, "cooldownIn"); attempt += 1; continue; } throw new Error(t("requestFailed", { status: response.status })); } } function parseRetryAfter(value) { if (!value) return 0; const seconds = Number(value); if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000); const date = Date.parse(value); if (!Number.isNaN(date)) return Math.max(0, date - Date.now()); return 0; } function evaluateUnfollowResponse(status, bodyText) { const text = typeof bodyText === "string" ? bodyText : ""; let payload = null; try { payload = JSON.parse(text); } catch { payload = null; } const message = String(payload?.message || ""); const blockedByPayload = payload?.feedback_required === true || payload?.spam === true || payload?.require_login === true || /feedback_required|checkpoint_required|challenge_required|login_required/i.test(message); if (status === 401 || status === 403 || status === 429 || blockedByPayload) { return { ok: false, blocked: true, reason: message || `HTTP ${status}` }; } if (status >= 500) { return { ok: false, blocked: false, reason: `HTTP ${status}` }; } if (status < 200 || status >= 300) { return { ok: false, blocked: false, reason: message || `HTTP ${status}` }; } if (payload === null) { return { ok: false, blocked: false, reason: "unexpected response" }; } if (payload.status === "ok" || payload.friendship_status !== undefined) { return { ok: true, blocked: false, reason: "" }; } return { ok: false, blocked: false, reason: message || String(payload.status || "unfollow rejected") }; } function confirmUnfollow() { if (!state.selected.size) return; const count = state.selected.size; showDialog({ title: t("unfollowConfirmTitle", { count }), body: t("unfollowConfirmBody"), confirmLabel: t("unfollowConfirmBtn", { count }), destructive: true, onConfirm: () => startUnfollow() }); } async function startUnfollow() { const targets = state.users.filter((u) => state.selected.has(u.id)); if (!targets.length) return; const csrf = getCookie("csrftoken"); if (!csrf) { toast(t("csrfMissing")); return; } state.mode = "unfollowing"; state.unfollowPaused = false; state.unfollowCancelled = false; state.unfollowBlocked = 0; state.log = []; state.progress = { current: 0, total: targets.length, label: "unfollowing", note: "" }; renderBody(); let processed = 0; for (let i = 0; i < targets.length; i += 1) { await waitWhile(() => state.unfollowPaused && !state.unfollowCancelled); if (state.unfollowCancelled) break; const user = targets[i]; let outcome = { ok: false, blocked: false, reason: "" }; try { outcome = await unfollowUser(user.id, csrf); } catch (error) { console.error("[iu] unfollow failed for", user.username, error); outcome = { ok: false, blocked: false, reason: error?.message || "" }; } state.log.push({ user, ok: outcome.ok, reason: outcome.reason }); if (outcome.ok) { state.selected.delete(user.id); const userRef = state.users.find((u) => u.id === user.id); if (userRef) userRef.unfollowed = true; } processed += 1; state.progress = { current: processed, total: targets.length, label: "unfollowing", note: outcome.ok ? "" : outcome.reason }; updateUnfollowDOM(); if (outcome.blocked) { state.unfollowBlocked = targets.length - processed; console.warn("[iu] Instagram blocked the unfollow action:", outcome.reason); break; } const isLast = i === targets.length - 1; if (!isLast) { await sleepWithCountdown(randomBetween(state.timings.unfollowDelayMin, state.timings.unfollowDelayMax), "nextActionIn"); if (state.timings.unfollowPauseEvery > 0 && (i + 1) % state.timings.unfollowPauseEvery === 0) { await sleepWithCountdown(state.timings.unfollowPauseMs, "cooldownIn"); } } } state.users = state.users.filter((u) => !u.unfollowed); state.mode = "unfollowDone"; state.waitUntil = 0; state.waitReason = ""; renderBody(); } async function unfollowUser(id, csrf) { const formHeaders = { "content-type": "application/x-www-form-urlencoded", "x-csrftoken": csrf }; const attempts = [ { url: `/api/v1/friendships/destroy/${id}/`, headers: { ...IG_HEADERS, ...formHeaders } }, { url: `/web/friendships/${id}/unfollow/`, headers: formHeaders } ]; let outcome = { ok: false, blocked: false, reason: "" }; for (let i = 0; i < attempts.length; i += 1) { if (i > 0) await sleep(randomBetween(1200, 2500)); let response; try { response = await fetch(attempts[i].url, { method: "POST", credentials: "include", headers: attempts[i].headers }); } catch (error) { outcome = { ok: false, blocked: false, reason: error?.message || "network error" }; continue; } const text = await response.text().catch(() => ""); outcome = evaluateUnfollowResponse(response.status, text); if (outcome.ok || outcome.blocked) return outcome; } return outcome; } async function copyUsernames() { const display = getDisplayUsers(); const target = state.selected.size ? display.filter((u) => state.selected.has(u.id)) : display; if (!target.length) return; const text = target.map((u) => u.username).sort().join("\n"); try { await navigator.clipboard.writeText(text); toast(t("copiedToast", { count: target.length })); } catch (error) { console.error("[iu] copy failed:", error); const ta = document.createElement("textarea"); ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select(); try { document.execCommand("copy"); toast(t("copiedToast", { count: target.length })); } catch {   } ta.remove(); } } function showSettings() { const fields = [ ["scanDelayMin", "minScanDelay", 100], ["scanDelayMax", "maxScanDelay", 100], ["scanPauseEveryPages", "scanPauseEvery", 1], ["scanPauseMs", "scanPauseLength", 1000], ["unfollowDelayMin", "minUnfollowDelay", 1000], ["unfollowDelayMax", "maxUnfollowDelay", 1000], ["unfollowPauseEvery", "unfollowPauseEvery", 1], ["unfollowPauseMs", "unfollowPauseLength", 1000] ]; const formHTML = fields.map(([key, label, step]) => ` <label class="iu-field"> <span>${escapeHTML(t(label))}</span> <input type="number" min="0" step="${step}" data-setting="${escapeAttr(key)}" value="${Number(state.timings[key])}"> </label> `).join(""); showDialog({ title: t("settingsTitle"), body: t("settingsBody"), contentHTML: `<div class="iu-form">${formHTML}</div>`, confirmLabel: t("save"), extraButton: { label: t("restoreDefaults"), action: "restore" }, onConfirm: (dialog) => { dialog.querySelectorAll("[data-setting]").forEach((input) => { const key = input.getAttribute("data-setting"); const val = Number(input.value); if (Number.isFinite(val) && val >= 0) state.timings[key] = val; }); persist(); toast(t("saved")); }, onExtra: (dialog) => { Object.assign(state.timings, DEFAULT_TIMINGS); dialog.querySelectorAll("[data-setting]").forEach((input) => { const key = input.getAttribute("data-setting"); input.value = state.timings[key]; }); } }); } function showDialog({ title, body, contentHTML, confirmLabel, destructive, extraButton, onConfirm, onExtra }) { dialogCounter += 1; const titleId = `${APP_ID}-dialog-title-${dialogCounter}`; const previouslyFocused = document.activeElement; const overlay = document.createElement("div"); overlay.className = "iu-overlay"; overlay.innerHTML = ` <div class="iu-dialog" role="dialog" aria-modal="true" aria-labelledby="${titleId}" tabindex="-1"> <h3 id="${titleId}">${escapeHTML(title)}</h3> ${body ? `<p>${escapeHTML(body)}</p>` : ""} ${contentHTML || ""} <div class="iu-dialog-actions"> ${extraButton ? `<button type="button" class="iu-btn iu-btn--ghost iu-btn--small" data-extra>${escapeHTML(extraButton.label)}</button>` : ""} <button type="button" class="iu-btn iu-btn--small" data-cancel>${escapeHTML(t("cancel"))}</button> <button type="button" class="iu-btn iu-btn--small ${destructive ? "iu-btn--danger" : "iu-btn--primary"}" data-confirm>${escapeHTML(confirmLabel)}</button> </div> </div> `; document.getElementById(APP_ID).appendChild(overlay); const dialog = overlay.querySelector(".iu-dialog"); const close = () => { document.removeEventListener("keydown", onKeyDown, true); if (closeActiveDialog === close) closeActiveDialog = null; overlay.remove(); if (previouslyFocused && document.contains(previouslyFocused)) previouslyFocused.focus(); }; const focusables = () => Array.from( dialog.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') ).filter((el) => !el.disabled && el.getClientRects().length > 0); function onKeyDown(event) { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); return; } if (event.key !== "Tab") return; const items = focusables(); if (!items.length) return; const first = items[0]; const last = items[items.length - 1]; if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog)) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } } document.addEventListener("keydown", onKeyDown, true); closeActiveDialog = close; overlay.addEventListener("click", (event) => { if (event.target === overlay) close(); }); overlay.querySelector("[data-cancel]").addEventListener("click", close); overlay.querySelector("[data-confirm]").addEventListener("click", () => { try { onConfirm?.(dialog); } finally { close(); } }); if (extraButton && onExtra) { overlay.querySelector("[data-extra]").addEventListener("click", () => onExtra(dialog)); } (dialog.querySelector("input") || (destructive ? dialog.querySelector("[data-cancel]") : dialog)).focus(); } function toggleLanguage() { state.language = state.language === "tr" ? "en" : "tr"; persist(); renderShell(); } function getDisplayUsers() { const query = state.search.trim().toLowerCase(); return state.users .filter((u) => !u.follows_viewer) .filter((u) => state.filters.showHidden ? state.hidden.has(u.id) : !state.hidden.has(u.id)) .filter((u) => state.filters.verified || !u.is_verified) .filter((u) => state.filters.private || !u.is_private) .filter((u) => state.filters.noAvatar || !isDefaultAvatar(u)) .filter((u) => !query || (u.username + " " + (u.full_name || "")).toLowerCase().includes(query)) .sort((a, b) => a.username.localeCompare(b.username)); } function pruneSelectionToDisplayed() { if (!state.selected.size) return; const visible = new Set(getDisplayUsers().map((u) => u.id)); state.selected.forEach((id) => { if (!visible.has(id)) state.selected.delete(id); }); } function normalizeUser(raw) { return { id: String(raw.id || raw.pk || raw.pk_id || ""), username: String(raw.username || ""), full_name: String(raw.full_name || ""), profile_pic_url: String(raw.profile_pic_url || raw.profile_pic_url_hd || ""), is_verified: Boolean(raw.is_verified), is_private: Boolean(raw.is_private), follows_viewer: Boolean(raw.follows_viewer ?? raw.friendship_status?.followed_by ?? raw.followed_by) }; } function dedupe(list) { const seen = new Set(); return list.filter((u) => { if (!u.id || seen.has(u.id)) return false; seen.add(u.id); return true; }); } function isDefaultAvatar(user) { const url = user.profile_pic_url || ""; return /44884218_345707102882519|464760996_1254146839119862/.test(url); } function getCookie(name) { const match = document.cookie.match(new RegExp("(^|; )" + name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=([^;]*)")); return match ? decodeURIComponent(match[2]) : null; } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms))); } function randomBetween(min, max) { const lo = Math.min(min, max); const hi = Math.max(min, max); return Math.floor(Math.random() * (hi - lo + 1)) + lo; } async function waitWhile(predicate, interval = 200) { while (predicate()) await sleep(interval); } async function sleepWithCountdown(ms, reasonKey) { state.waitReason = reasonKey; let remaining = Math.max(0, ms); state.waitUntil = Date.now() + remaining; updateCountdownDOM(); while (remaining > 0) { if (state.scanCancelled || state.unfollowCancelled) break; if (state.scanPaused || state.unfollowPaused) { await waitWhile(() => state.scanPaused || state.unfollowPaused); state.waitUntil = Date.now() + remaining; updateCountdownDOM(); continue; } const before = Date.now(); await sleep(Math.min(250, remaining)); remaining -= Date.now() - before; } state.waitUntil = 0; state.waitReason = ""; updateCountdownDOM(); } function startCountdown() { if (countdownTimer) return; countdownTimer = setInterval(updateCountdownDOM, 500); } function stopCountdown() { if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; } } function updateCountdownDOM() { const node = document.querySelector(`#${APP_ID} [data-countdown]`); if (!node) return; if (!state.waitUntil) { node.textContent = ""; return; } const remaining = Math.max(0, Math.ceil((state.waitUntil - Date.now()) / 1000)); const reason = state.waitReason || "nextActionIn"; node.textContent = t(reason, { seconds: remaining }); } function setBar(root, percent, valueText, known) { const bar = root.querySelector("[data-progress-bar]"); if (bar) bar.style.width = percent + "%"; const box = root.querySelector("[data-bar]"); if (!box) return; if (known) box.setAttribute("aria-valuenow", String(percent)); else box.removeAttribute("aria-valuenow"); box.setAttribute("aria-valuetext", valueText); } function updatePillDOM() { if (!state.minimized) return; const pillLabelEl = document.querySelector(`#${APP_ID} [data-pill-label]`); if (pillLabelEl) pillLabelEl.textContent = pillLabel(); } function updateProgressDOM() { const root = document.querySelector(`#${APP_ID} [data-body]`); if (!root) return; const { current, total, label, note } = state.progress; const percent = total ? Math.min(100, Math.round((current / total) * 100)) : 0; const counter = total ? t("ofTotal", { current, total }) : t("ofUnknown", { current }); setBar(root, percent, counter, Boolean(total)); const counterEl = root.querySelector("[data-progress-counter]"); if (counterEl) counterEl.textContent = counter; const labelEl = root.querySelector("[data-progress-label]"); if (labelEl) labelEl.textContent = state.scanPaused ? t("paused") : t(label); const noteEl = root.querySelector("[data-progress-note]"); if (noteEl) noteEl.textContent = note || ""; updatePillDOM(); } function updateUnfollowDOM() { const root = document.querySelector(`#${APP_ID} [data-body]`); if (!root) return; const { current, total, note } = state.progress; const percent = total ? Math.min(100, Math.round((current / total) * 100)) : 0; const summary = unfollowSummary(); setBar(root, percent, summary, Boolean(total)); const counterEl = root.querySelector("[data-progress-counter]"); if (counterEl) counterEl.textContent = summary; const labelEl = root.querySelector("[data-progress-label]"); if (labelEl) labelEl.textContent = unfollowTitle(); const noteEl = root.querySelector("[data-progress-note]"); if (noteEl) noteEl.textContent = note || ""; const last = state.log[state.log.length - 1]; const currentEl = root.querySelector("[data-current]"); if (currentEl) { currentEl.hidden = !last; if (last) { const userEl = currentEl.querySelector("[data-current-user]"); if (userEl) userEl.textContent = "@" + last.user.username; const tagEl = currentEl.querySelector("[data-current-tag]"); if (tagEl) { tagEl.textContent = last.ok ? "✓" : "✕"; tagEl.title = last.reason || ""; tagEl.classList.toggle("iu-tag--green", last.ok); tagEl.classList.toggle("iu-tag--red", !last.ok); } } } updatePillDOM(); } function toast(message) { const root = document.getElementById(APP_ID); if (!root) return; root.querySelector(".iu-toast")?.remove(); if (toastTimer) { clearTimeout(toastTimer); toastTimer = null; } const node = document.createElement("div"); node.className = "iu-toast"; node.setAttribute("role", "status"); node.setAttribute("aria-live", "polite"); node.textContent = message; root.appendChild(node); toastTimer = setTimeout(() => { node.remove(); toastTimer = null; }, 3500); } function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function escapeHTML(value) { return String(value ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "'" }[ch])); } function escapeAttr(value) { return escapeHTML(value); } const CSS = ` #${APP_ID}, #${APP_ID} * { box-sizing: border-box; } #${APP_ID} { --iu-bg: #161513; --iu-bg-2: #201e1a; --iu-bg-3: #2b2823; --iu-line: rgba(247,244,239,0.10); --iu-line-strong: rgba(247,244,239,0.20); --iu-text: #f7f4ef; --iu-muted: #9a948a; --iu-accent: #f0508a; --iu-accent-2: #ff6b9d; --iu-gradient: linear-gradient(135deg, #feda75, #fa7e1e, #d62976, #962fbf, #4f5bd5); --iu-danger: #b3251c; --iu-danger-text: #f06a5d; --iu-success: #22c55e; position: fixed; inset: 0; pointer-events: none; z-index: 2147483647; color: var(--iu-text); color-scheme: dark; font: 14px/1.45 Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; } #${APP_ID} .iu-list, #${APP_ID} .iu-form { scrollbar-width: thin; scrollbar-color: var(--iu-line-strong) transparent; } #${APP_ID} .iu-list::-webkit-scrollbar, #${APP_ID} .iu-form::-webkit-scrollbar { width: 8px; } #${APP_ID} .iu-list::-webkit-scrollbar-track, #${APP_ID} .iu-form::-webkit-scrollbar-track { background: transparent; } #${APP_ID} .iu-list::-webkit-scrollbar-thumb, #${APP_ID} .iu-form::-webkit-scrollbar-thumb { background: var(--iu-line-strong); border-radius: 4px; } #${APP_ID} .iu-list::-webkit-scrollbar-thumb:hover, #${APP_ID} .iu-form::-webkit-scrollbar-thumb:hover { background: var(--iu-muted); } #${APP_ID} :focus-visible { outline: 2px solid var(--iu-accent); outline-offset: 2px; } #${APP_ID} > * { pointer-events: auto; } #${APP_ID} button, #${APP_ID} input, #${APP_ID} a { font: inherit; color: inherit; } #${APP_ID} a { text-decoration: none; } #${APP_ID} button { cursor: pointer; } #${APP_ID} [hidden] { display: none !important; } #${APP_ID} .iu-panel { position: absolute; width: ${PANEL_WIDTH}px; max-width: calc(100vw - 24px); max-height: calc(100vh - 24px); display: flex; flex-direction: column; background: var(--iu-bg); border: 1px solid var(--iu-line); border-radius: 14px; box-shadow: 0 24px 60px rgba(0,0,0,0.45), 0 2px 6px rgba(0,0,0,0.3); overflow: hidden; animation: iu-pop 0.18s ease-out; } @keyframes iu-pop { from { opacity: 0; transform: translateY(8px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } } #${APP_ID} .iu-header { flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 12px 12px 14px; border-bottom: 1px solid var(--iu-line); cursor: grab; user-select: none; background: linear-gradient(180deg, rgba(255,255,255,0.02), transparent); } #${APP_ID} .iu-header.iu-dragging { cursor: grabbing; } #${APP_ID} .iu-brand { display: flex; align-items: center; gap: 10px; min-width: 0; } #${APP_ID} .iu-brand-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--iu-gradient); flex-shrink: 0; } #${APP_ID} .iu-brand-text { min-width: 0; } #${APP_ID} .iu-brand-text strong { display: block; font-size: 14px; font-weight: 600; } #${APP_ID} .iu-brand-text span { display: block; font-size: 11px; color: var(--iu-muted); } #${APP_ID} .iu-header-actions { display: flex; gap: 4px; flex-shrink: 0; } #${APP_ID} .iu-header-actions button { width: 28px; height: 28px; display: grid; place-items: center; border: none; background: transparent; border-radius: 6px; color: var(--iu-muted); transition: background 0.15s, color 0.15s; font-size: 11px; font-weight: 600; } #${APP_ID} .iu-header-actions button:hover { background: var(--iu-bg-2); color: var(--iu-text); } #${APP_ID} .iu-header-actions svg { display: block; } #${APP_ID} .iu-body { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } #${APP_ID} .iu-welcome { padding: 28px 22px; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 10px; } #${APP_ID} .iu-welcome-icon { width: 64px; height: 64px; display: grid; place-items: center; border-radius: 50%; background: rgba(240,80,138,0.15); color: var(--iu-accent); margin-bottom: 4px; } #${APP_ID} .iu-welcome-icon--error { background: rgba(240,106,93,0.15); color: var(--iu-danger-text); } #${APP_ID} .iu-welcome h2 { margin: 0; font-size: 17px; font-weight: 600; } #${APP_ID} .iu-welcome p { margin: 0; color: var(--iu-muted); font-size: 13px; max-width: 300px; } #${APP_ID} .iu-btn { border: 1px solid var(--iu-line); background: var(--iu-bg-2); color: var(--iu-text); padding: 8px 14px; border-radius: 8px; font-size: 13px; font-weight: 500; transition: background 0.15s, border-color 0.15s, transform 0.05s; } #${APP_ID} .iu-btn:hover:not(:disabled) { background: var(--iu-bg-3); border-color: var(--iu-line-strong); } #${APP_ID} .iu-btn:active:not(:disabled) { transform: scale(0.98); } #${APP_ID} .iu-btn:disabled { opacity: 0.4; cursor: not-allowed; } #${APP_ID} .iu-btn--primary { background: var(--iu-accent); border-color: var(--iu-accent); color: var(--iu-bg); } #${APP_ID} .iu-btn--primary:hover:not(:disabled) { background: var(--iu-accent-2); border-color: var(--iu-accent-2); } #${APP_ID} .iu-btn--danger { background: var(--iu-danger); border-color: var(--iu-danger); color: #fff; } #${APP_ID} .iu-btn--danger:hover:not(:disabled) { background: #8f1d16; border-color: #8f1d16; } #${APP_ID} .iu-btn--ghost { background: transparent; } #${APP_ID} .iu-btn--lg { padding: 10px 20px; font-size: 14px; margin-top: 8px; } #${APP_ID} .iu-btn--small { padding: 6px 10px; font-size: 12px; } #${APP_ID} .iu-progress { padding: 22px 20px; display: flex; flex-direction: column; gap: 14px; } #${APP_ID} .iu-progress-head h2 { margin: 0; font-size: 15px; font-weight: 600; } #${APP_ID} .iu-progress-head p { margin: 4px 0 0; color: var(--iu-muted); font-size: 12px; min-height: 1em; } #${APP_ID} .iu-bar { height: 6px; border-radius: 3px; background: rgba(255,255,255,0.06); overflow: hidden; } #${APP_ID} .iu-bar > span { display: block; height: 100%; background: var(--iu-gradient); transition: width 0.3s ease; } #${APP_ID} .iu-progress-meta { display: flex; justify-content: space-between; align-items: center; font-size: 12px; } #${APP_ID} .iu-progress-actions { display: flex; gap: 8px; } #${APP_ID} .iu-current { display: flex; align-items: center; gap: 8px; padding: 10px; background: var(--iu-bg-2); border-radius: 8px; font-size: 12px; } #${APP_ID} .iu-current strong { font-weight: 600; } #${APP_ID} .iu-blocked { padding: 10px; border-radius: 8px; font-size: 12px; line-height: 1.5; color: #fecaca; background: rgba(240, 106, 93, 0.12); border: 1px solid rgba(240, 106, 93, 0.35); } #${APP_ID} .iu-results { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; } #${APP_ID} .iu-results-summary { padding: 12px 16px 4px; font-size: 13px; color: var(--iu-muted); } #${APP_ID} .iu-search-row { padding: 8px 16px; } #${APP_ID} .iu-search { width: 100%; height: 36px; padding: 0 12px; border: 1px solid var(--iu-line); border-radius: 8px; background: var(--iu-bg-2); color: var(--iu-text); transition: border-color 0.15s; } #${APP_ID} .iu-search:focus { border-color: var(--iu-accent); } #${APP_ID} .iu-search::-webkit-search-cancel-button { filter: opacity(0.6); cursor: pointer; } #${APP_ID} .iu-filters { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 16px 10px; } #${APP_ID} .iu-chip { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; border: 1px solid var(--iu-line); border-radius: 999px; background: transparent; color: var(--iu-muted); font-size: 12px; transition: border-color 0.15s, color 0.15s, background 0.15s; } #${APP_ID} .iu-chip:hover { border-color: var(--iu-line-strong); color: var(--iu-text); } #${APP_ID} .iu-chip--on { background: rgba(240,80,138,0.12); border-color: rgba(240,80,138,0.5); color: var(--iu-accent-2); } #${APP_ID} .iu-chip-tick { display: inline-grid; place-items: center; width: 12px; height: 12px; } #${APP_ID} .iu-chip-tick svg { width: 12px; height: 12px; } #${APP_ID} .iu-list { flex: 1 1 auto; min-height: 100px; max-height: 50vh; overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid var(--iu-line); border-bottom: 1px solid var(--iu-line); } #${APP_ID} .iu-list-empty { padding: 30px 20px; text-align: center; color: var(--iu-muted); font-size: 13px; } #${APP_ID} .iu-row { display: grid; grid-template-columns: 18px 36px 1fr auto; gap: 10px; padding: 8px 16px; align-items: center; cursor: pointer; border-bottom: 1px solid rgba(255,255,255,0.04); transition: background 0.1s; } #${APP_ID} .iu-row:hover { background: rgba(247,244,239,0.04); } #${APP_ID} .iu-row:focus-visible { outline: 2px solid var(--iu-accent); outline-offset: -2px; } #${APP_ID} .iu-row--selected { background: rgba(240,80,138,0.14); box-shadow: inset 3px 0 0 var(--iu-accent); } #${APP_ID} .iu-row--selected:hover { background: rgba(240,80,138,0.2); } #${APP_ID} .iu-row:last-child { border-bottom: none; } #${APP_ID} .iu-row--hidden { opacity: 0.55; } #${APP_ID} .iu-row-check { width: 16px; height: 16px; accent-color: var(--iu-accent); cursor: pointer; margin: 0; } #${APP_ID} .iu-avatar { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; background: var(--iu-bg-3); } #${APP_ID} .iu-row-text { min-width: 0; } #${APP_ID} .iu-row-name { display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 13px; font-weight: 500; } #${APP_ID} .iu-row-name > a { color: var(--iu-text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #${APP_ID} .iu-row-name a:hover { color: var(--iu-accent-2); } #${APP_ID} .iu-row-sub { font-size: 12px; color: var(--iu-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #${APP_ID} .iu-row-actions { display: flex; gap: 4px; align-items: center; } #${APP_ID} .iu-icon-btn { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 6px; border: none; background: transparent; color: var(--iu-muted); transition: background 0.15s, color 0.15s; } #${APP_ID} .iu-icon-btn:hover { background: var(--iu-bg-2); color: var(--iu-text); } #${APP_ID} .iu-text-btn { width: auto; padding: 0 8px; font-size: 11px; font-weight: 500; } #${APP_ID} .iu-tag { display: inline-flex; align-items: center; flex-shrink: 0; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.06); color: var(--iu-muted); letter-spacing: 0.02em; text-transform: uppercase; } #${APP_ID} .iu-tag--blue { background: rgba(240,80,138,0.15); color: var(--iu-accent-2); } #${APP_ID} .iu-tag--green { background: rgba(34,197,94,0.15); color: var(--iu-success); } #${APP_ID} .iu-tag--red { background: rgba(240,106,93,0.15); color: var(--iu-danger-text); } #${APP_ID} .iu-actionbar { flex-shrink: 0; display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 10px 16px; background: var(--iu-bg); border-top: 1px solid var(--iu-line); } #${APP_ID} .iu-actionbar-left, #${APP_ID} .iu-actionbar-right { display: flex; align-items: center; gap: 8px; min-width: 0; } #${APP_ID} .iu-muted { color: var(--iu-muted); font-size: 12px; } #${APP_ID} .iu-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); display: grid; place-items: center; padding: 20px; z-index: 1; } #${APP_ID} .iu-dialog { width: 100%; max-width: 380px; background: var(--iu-bg); border: 1px solid var(--iu-line); border-radius: 12px; padding: 18px; box-shadow: 0 30px 80px rgba(0,0,0,0.55); } #${APP_ID} .iu-dialog h3 { margin: 0 0 8px; font-size: 16px; } #${APP_ID} .iu-dialog p { margin: 0 0 14px; color: var(--iu-muted); font-size: 13px; } #${APP_ID} .iu-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; } #${APP_ID} .iu-dialog-actions [data-extra] { margin-right: auto; } #${APP_ID} .iu-form { display: grid; gap: 8px; max-height: 50vh; overflow-y: auto; padding-right: 4px; } #${APP_ID} .iu-field { display: grid; grid-template-columns: 1fr 110px; align-items: center; gap: 10px; font-size: 12px; color: var(--iu-muted); } #${APP_ID} .iu-field input { height: 32px; padding: 0 10px; border: 1px solid var(--iu-line); border-radius: 6px; background: var(--iu-bg-2); color: var(--iu-text); } #${APP_ID} .iu-field input:focus { border-color: var(--iu-accent); } #${APP_ID} .iu-toast { position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); padding: 8px 14px; background: rgba(32,30,26,0.96); border: 1px solid var(--iu-line); border-radius: 999px; font-size: 12px; box-shadow: 0 6px 20px rgba(0,0,0,0.35); pointer-events: none; animation: iu-pop 0.18s ease-out; } #${APP_ID} .iu-pill { position: absolute; display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; background: var(--iu-bg); border: 1px solid var(--iu-line); color: var(--iu-text); border-radius: 999px; box-shadow: 0 12px 30px rgba(0,0,0,0.4); font-size: 12px; font-weight: 500; transition: background 0.15s; cursor: pointer; } #${APP_ID} .iu-pill:hover { background: var(--iu-bg-2); } #${APP_ID} .iu-pill-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--iu-muted); } #${APP_ID} .iu-pill-dot--active { background: var(--iu-accent); animation: iu-pulse 1.4s infinite; } #${APP_ID} .iu-pill-dot--error { background: var(--iu-danger-text); } @keyframes iu-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } @media (max-width: 480px) { #${APP_ID} .iu-panel { width: calc(100vw - 16px) !important; max-height: calc(100vh - 16px) !important; left: 8px !important; right: 8px !important; top: 8px !important; bottom: 8px !important; } #${APP_ID} .iu-list { max-height: none; flex: 1 1 auto; } } @media (prefers-reduced-motion: reduce) { #${APP_ID} *, #${APP_ID} *::before, #${APP_ID} *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } `; if (globalThis.__IU_TEST__) { globalThis.__IU_TEST__({ evaluateUnfollowResponse, unfollowUser, normalizeUser, isDefaultAvatar, parseRetryAfter, I18N, t }); return; } cleanupExisting(); injectStyles(); mount(); })();
  • Bugün, 06:18:07
    #2
    Teşekkürler
  • Bugün, 06:18:20
    #3
    CanR10 adlı üyeden alıntı: mesajı görüntüle
    Teşekkürler
    Rica ederim iyi forumlar hocam
  • Bugün, 11:00:45
    #4
    Elinize sağlık hocam