// Curated blocked domains — static lists (no search) function cidrToMask(bits) { const n = parseInt(bits || "32", 10); if (n === 32) return "255.255.255.255"; return Array.from({ length: 4 }, (_, i) => (65280 >> Math.min(8, Math.max(0, n - i * 8))) & 255).join("."); } function buildKeeneticBat(group, allGroups) { const lines = ["@echo off", "rem Keenetic static routes — " + (group ? group.title : "все группы"), ""]; const src = group ? [group] : allGroups; const seen = new Set(); src.forEach((g) => { (g.ips || []).forEach((cidr) => { const [ip, bits] = cidr.split("/"); if (!ip || seen.has(cidr)) return; seen.add(cidr); const mask = cidrToMask(bits); lines.push(`route ADD ${ip} MASK ${mask} 0.0.0.0 & rem ${g.title}`); }); }); if (lines.length <= 3) { lines.push("rem IP-префиксы не заданы — используйте список доменов вручную в VPN"); } return lines.join("\r\n") + "\r\n"; } function buildBatForSelectedDomains(selectedSet, allGroups) { const selected = [...selectedSet]; const lines = ["@echo off", "rem Keenetic — выбранные домены (" + selected.length + ")", ""]; const seen = new Set(); allGroups.forEach((g) => { const picked = g.domains.filter((d) => selectedSet.has(d)); if (!picked.length) return; (g.ips || []).forEach((cidr) => { const [ip, bits] = cidr.split("/"); if (!ip || seen.has(cidr)) return; seen.add(cidr); lines.push(`route ADD ${ip} MASK ${cidrToMask(bits)} 0.0.0.0 & rem ${g.title}: ${picked.join(", ")}`); }); }); if (lines.length <= 3) { lines.push("rem Нет IP/CIDR для выбранных доменов — настройте VPN вручную"); selected.forEach((d) => lines.push(`rem ${d}`)); } return lines.join("\r\n") + "\r\n"; } function downloadText(filename, text) { const blob = new Blob([text], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } async function copyText(text) { try { await navigator.clipboard.writeText(text); return true; } catch { const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); return true; } } function DomainGroupCard({ group, selectedDomains, onToggleDomain, onToggleGroup, onDownloadBat }) { const [open, setOpen] = React.useState(true); const [copied, setCopied] = React.useState(null); const domainText = group.domains.join("\n"); const ipText = (group.ips || []).join("\n"); const selectedInGroup = group.domains.filter((d) => selectedDomains.has(d)); const allSelected = group.domains.length > 0 && selectedInGroup.length === group.domains.length; async function handleCopy(kind) { const text = kind === "domains" ? domainText : ipText; if (!text) return; await copyText(text); setCopied(kind); setTimeout(() => setCopied(null), 1800); } return ( {open && (
{group.v2fly_tags?.length > 0 && (
{group.v2fly_tags.map((t) => ( {t} ))}
)}
{group.domains.map((d) => { const selected = selectedDomains.has(d); return ( ); })}
{(group.ips || []).length > 0 && (
IP / CIDR (префиксы)
{group.ips.map((ip) => ( {ip} ))}
)}
{(group.ips || []).length > 0 && ( )}
)}
); } function DomainsApp() { const [groups, setGroups] = React.useState([]); const [loading, setLoading] = React.useState(true); const [filter, setFilter] = React.useState(""); const [selectedDomains, setSelectedDomains] = React.useState(new Set()); const [copiedSelected, setCopiedSelected] = React.useState(false); React.useEffect(() => { fetch("data/blocked-domains.json") .then((r) => r.json()) .then((data) => { setGroups(data.groups || []); setLoading(false); }) .catch(() => setLoading(false)); }, []); const filtered = React.useMemo(() => { const q = filter.trim().toLowerCase(); if (!q) return groups; return groups.map((g) => ({ ...g, domains: g.domains.filter((d) => d.includes(q) || g.title.toLowerCase().includes(q)), })).filter((g) => g.domains.length > 0); }, [groups, filter]); const allDomains = groups.flatMap((g) => g.domains).join("\n"); const toggleDomain = (d) => { setSelectedDomains((prev) => { const next = new Set(prev); if (next.has(d)) next.delete(d); else next.add(d); return next; }); }; const toggleGroup = (group, select) => { setSelectedDomains((prev) => { const next = new Set(prev); group.domains.forEach((d) => { if (select) next.add(d); else next.delete(d); }); return next; }); }; const copySelectedDomains = async () => { await copyText([...selectedDomains].join("\n")); setCopiedSelected(true); setTimeout(() => setCopiedSelected(false), 1800); }; const downloadSelectedBat = () => { downloadText("keenetic-selected.bat", buildBatForSelectedDomains(selectedDomains, groups)); }; return (
Статические списки · без поиска

Популярные заблокированные домены

Кликните по доменам для выбора, затем скачайте .bat только для выбранных. Кураторские списки для настройки VPN и маршрутизации на роутере Keenetic.

setFilter(e.target.value)} placeholder="Фильтр по домену…" style={{ flex: "1 1 220px", height: 42, padding: "0 16px", borderRadius: 12, border: "1px solid var(--line)", fontSize: 14, }} />
{selectedDomains.size > 0 && (
Выбрано: {selectedDomains.size}
)} {loading &&

Загрузка списков…

} {!loading && filtered.length === 0 && (

Ничего не найдено по фильтру.

)}
{filtered.map((g) => ( downloadText(`keenetic-${grp.id}.bat`, buildKeeneticBat(grp, groups))} /> ))}
); } ReactDOM.createRoot(document.getElementById("root")).render();