feat: добавлены функции для работы с пользовательскими rule-sets
Добавлены новые API-методы для получения и сохранения пользовательских rule-sets. Обновлены компоненты для работы с этими данными, включая интерфейс для добавления и удаления rule-sets. Refs: None
This commit is contained in:
@@ -2,6 +2,24 @@ import React, { useEffect, useState } from 'react';
|
||||
import { api } from '../api.js';
|
||||
import { formatRelative } from '../utils/format.js';
|
||||
|
||||
const SUGGESTED_RULE_SETS = [
|
||||
{
|
||||
tag: 'geosite-runet',
|
||||
url: 'https://github.com/runetfreedom/russia-blocked-geosite/releases/latest/download/rule-set/ru.srs',
|
||||
label: 'runetfreedom / RU заблокированные домены',
|
||||
},
|
||||
{
|
||||
tag: 'geoip-ru-loyalsoldier',
|
||||
url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/sing-box-rules@rule-set/geoip-ru.srs',
|
||||
label: 'Loyalsoldier / geoip-ru',
|
||||
},
|
||||
{
|
||||
tag: 'geosite-category-ru-loyalsoldier',
|
||||
url: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/sing-box-rules@rule-set/geosite-category-ru.srs',
|
||||
label: 'Loyalsoldier / geosite-category-ru',
|
||||
},
|
||||
];
|
||||
|
||||
function SubscriptionCard({ state, subscriptionUrl, setSubscriptionUrl, busy, onFetch, onForget, pushToast }) {
|
||||
const [editing, setEditing] = useState(!state?.hasSubscription);
|
||||
|
||||
@@ -126,6 +144,143 @@ function ConfigCard({ state, busy, onShowConfig, onClearConfig, pushToast }) {
|
||||
);
|
||||
}
|
||||
|
||||
function RuleSetsCard({ pushToast }) {
|
||||
const [ruleSets, setRuleSets] = useState([]);
|
||||
const [newTag, setNewTag] = useState('');
|
||||
const [newUrl, setNewUrl] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api.ruleSets.get().then((d) => setRuleSets(d.ruleSets || [])).catch(() => {});
|
||||
}, []);
|
||||
|
||||
async function save(next) {
|
||||
setBusy(true);
|
||||
try {
|
||||
const data = await api.ruleSets.save(next);
|
||||
setRuleSets(data.ruleSets || []);
|
||||
pushToast({ kind: 'success', title: 'Rule-sets сохранены' });
|
||||
} catch (err) {
|
||||
pushToast({ kind: 'danger', title: 'Ошибка', message: err.message });
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function addNew() {
|
||||
const tag = newTag.trim();
|
||||
const url = newUrl.trim();
|
||||
if (!tag || !url) return;
|
||||
if (!/^[a-z0-9][a-z0-9-]*$/i.test(tag)) {
|
||||
pushToast({ kind: 'danger', title: 'Невалидный тег', message: 'Только буквы, цифры, дефис' });
|
||||
return;
|
||||
}
|
||||
if (ruleSets.some((rs) => rs.tag === tag)) {
|
||||
pushToast({ kind: 'danger', title: 'Тег уже существует' });
|
||||
return;
|
||||
}
|
||||
const next = [...ruleSets, { tag, url }];
|
||||
setNewTag('');
|
||||
setNewUrl('');
|
||||
save(next);
|
||||
}
|
||||
|
||||
function remove(tag) {
|
||||
save(ruleSets.filter((rs) => rs.tag !== tag));
|
||||
}
|
||||
|
||||
function addSuggested(suggested) {
|
||||
if (ruleSets.some((rs) => rs.tag === suggested.tag)) {
|
||||
pushToast({ kind: 'info', title: `${suggested.tag} уже добавлен` });
|
||||
return;
|
||||
}
|
||||
save([...ruleSets, { tag: suggested.tag, url: suggested.url }]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h2>Источники (rule-sets)</h2>
|
||||
</div>
|
||||
<small className="muted" style={{ display: 'block', marginBottom: 12 }}>
|
||||
Пользовательские geo-базы в формате <strong>.srs</strong> (sing-box rule-set binary).
|
||||
После добавления тег можно указать в правиле маршрутизации.
|
||||
</small>
|
||||
|
||||
{ruleSets.length > 0 && (
|
||||
<table className="table" style={{ marginBottom: 16 }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Тег</th>
|
||||
<th>URL</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ruleSets.map((rs) => (
|
||||
<tr key={rs.tag}>
|
||||
<td className="text-mono">{rs.tag}</td>
|
||||
<td className="muted" style={{ fontSize: 12, wordBreak: 'break-all' }}>{rs.url}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
<button className="btn btn-ghost sm" disabled={busy} onClick={() => remove(rs.tag)}>×</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
<div className="field">
|
||||
<span className="field-label">Добавить вручную</span>
|
||||
<div className="flex" style={{ gap: 8, flexWrap: 'wrap' }}>
|
||||
<input
|
||||
className="input"
|
||||
style={{ width: 180 }}
|
||||
placeholder="тег (напр. geosite-runet)"
|
||||
value={newTag}
|
||||
onChange={(e) => setNewTag(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="input"
|
||||
style={{ flex: 1, minWidth: 200 }}
|
||||
placeholder="https://…/rule-set.srs"
|
||||
value={newUrl}
|
||||
onChange={(e) => setNewUrl(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" disabled={busy || !newTag || !newUrl} onClick={addNew}>
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="field">
|
||||
<span className="field-label">Готовые источники</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{SUGGESTED_RULE_SETS.map((s) => (
|
||||
<div key={s.tag} className="flex" style={{ alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ flex: 1, fontSize: 13 }}>
|
||||
<strong className="text-mono">{s.tag}</strong>
|
||||
<span className="muted" style={{ marginLeft: 8 }}>{s.label}</span>
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-secondary sm"
|
||||
disabled={busy || ruleSets.some((rs) => rs.tag === s.tag)}
|
||||
onClick={() => addSuggested(s)}
|
||||
>
|
||||
{ruleSets.some((rs) => rs.tag === s.tag) ? '✓ добавлен' : '+ Добавить'}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<small className="muted" style={{ marginTop: 8, display: 'block' }}>
|
||||
Sing-box скачает эти файлы автоматически при первом запуске.
|
||||
<strong>.dat файлы (v2ray) не поддерживаются</strong> — используйте .srs эквиваленты.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PortsCard({ state }) {
|
||||
return (
|
||||
<div className="card">
|
||||
@@ -165,6 +320,7 @@ export function SettingsPage({
|
||||
onClearConfig={onClearConfig}
|
||||
pushToast={pushToast}
|
||||
/>
|
||||
<RuleSetsCard pushToast={pushToast} />
|
||||
<PortsCard state={state} />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user