// 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();