Release 1.3.0: Hysteria2 BBR/Salamander/masquerade options and subscription URL import.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+157
-4
@@ -112,6 +112,32 @@
|
||||
|
||||
.stack { display: grid; gap: 10px; margin-bottom: 14px; }
|
||||
|
||||
details.hy2 {
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255,255,255,.55);
|
||||
}
|
||||
details.hy2 summary {
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
font-size: .84rem;
|
||||
color: var(--accent);
|
||||
list-style: none;
|
||||
}
|
||||
details.hy2 .grid2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
details.hy2 label.field { margin-top: 6px; }
|
||||
.check {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: .88rem; margin-top: 8px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; padding: 12px 14px;
|
||||
@@ -299,8 +325,71 @@
|
||||
<label class="field" for="proxy">Ссылка сервера</label>
|
||||
<input id="proxy" type="text" placeholder="naive+https://… или hysteria2://pass@host:443/" spellcheck="false" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="subUrl">URL подписки</label>
|
||||
<div class="profile-bar">
|
||||
<input id="subUrl" type="text" placeholder="https://…/sub" spellcheck="false" />
|
||||
<button class="icon-btn" id="subBtn" type="button" title="Обновить подписку" style="width:auto;padding:0 14px;font-size:.8rem">↻</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="hy2" id="hy2Box">
|
||||
<summary>Hysteria 2 · BBR / Salamander / маскировка</summary>
|
||||
<div class="grid2">
|
||||
<div>
|
||||
<label class="field" for="hy2Congestion">Congestion</label>
|
||||
<select id="hy2Congestion">
|
||||
<option value="bbr">BBR</option>
|
||||
<option value="reno">Reno</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Bbr">BBR profile</label>
|
||||
<select id="hy2Bbr">
|
||||
<option value="standard">standard</option>
|
||||
<option value="conservative">conservative</option>
|
||||
<option value="aggressive">aggressive</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Up">Brutal up</label>
|
||||
<input id="hy2Up" type="text" placeholder="пусто = BBR" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Down">Brutal down</label>
|
||||
<input id="hy2Down" type="text" placeholder="пусто = BBR" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Obfs">Obfs</label>
|
||||
<select id="hy2Obfs">
|
||||
<option value="">нет</option>
|
||||
<option value="salamander">salamander</option>
|
||||
<option value="gecko">gecko</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2ObfsPass">Obfs password</label>
|
||||
<input id="hy2ObfsPass" type="text" placeholder="пароль salamander/gecko" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Sni">SNI (маскарад HTTPS)</label>
|
||||
<input id="hy2Sni" type="text" placeholder="cdn.example.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Pin">pinSHA256</label>
|
||||
<input id="hy2Pin" type="text" placeholder="опционально" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="hy2Hop">Hop interval</label>
|
||||
<input id="hy2Hop" type="text" placeholder="30s" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="check"><input id="hy2Insecure" type="checkbox" /> TLS insecure</label>
|
||||
<label class="check"><input id="hy2Fast" type="checkbox" /> Fast Open</label>
|
||||
<label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label>
|
||||
</details>
|
||||
|
||||
<div class="row">
|
||||
<span>Системный прокси Windows</span>
|
||||
<label class="switch">
|
||||
@@ -373,8 +462,51 @@
|
||||
const updateBtn = $("updateBtn");
|
||||
const pingList = $("pingList");
|
||||
const verLabel = $("verLabel");
|
||||
const subUrl = $("subUrl");
|
||||
const subBtn = $("subBtn");
|
||||
const SHOP_URL = "https://evilfox.win/";
|
||||
|
||||
function readHy2() {
|
||||
return {
|
||||
congestion: $("hy2Congestion").value,
|
||||
bbr_profile: $("hy2Bbr").value,
|
||||
bandwidth_up: $("hy2Up").value.trim(),
|
||||
bandwidth_down: $("hy2Down").value.trim(),
|
||||
obfs: $("hy2Obfs").value,
|
||||
obfs_password: $("hy2ObfsPass").value.trim(),
|
||||
sni: $("hy2Sni").value.trim(),
|
||||
insecure: !!$("hy2Insecure").checked,
|
||||
pin_sha256: $("hy2Pin").value.trim(),
|
||||
fast_open: !!$("hy2Fast").checked,
|
||||
lazy: !!$("hy2Lazy").checked,
|
||||
hop_interval: $("hy2Hop").value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
function fillHy2(h) {
|
||||
if (!h) return;
|
||||
if (h.congestion) $("hy2Congestion").value = h.congestion;
|
||||
if (h.bbr_profile) $("hy2Bbr").value = h.bbr_profile;
|
||||
$("hy2Up").value = h.bandwidth_up || "";
|
||||
$("hy2Down").value = h.bandwidth_down || "";
|
||||
$("hy2Obfs").value = h.obfs || "";
|
||||
$("hy2ObfsPass").value = h.obfs_password || "";
|
||||
$("hy2Sni").value = h.sni || "";
|
||||
$("hy2Pin").value = h.pin_sha256 || "";
|
||||
$("hy2Hop").value = h.hop_interval || "";
|
||||
$("hy2Insecure").checked = !!h.insecure;
|
||||
$("hy2Fast").checked = !!h.fast_open;
|
||||
$("hy2Lazy").checked = !!h.lazy;
|
||||
}
|
||||
|
||||
async function persistHy2IfNeeded(protocol) {
|
||||
const p = (protocol || "").toLowerCase();
|
||||
const link = proxy.value.trim().toLowerCase();
|
||||
if (p === "hysteria2" || link.startsWith("hysteria2://") || link.startsWith("hy2://")) {
|
||||
await saveHy2(readHy2());
|
||||
}
|
||||
}
|
||||
|
||||
let busy = false;
|
||||
let connected = false;
|
||||
let formHydrated = false;
|
||||
@@ -382,10 +514,15 @@
|
||||
let profiles = [];
|
||||
|
||||
function markDirty() { dirty = true; }
|
||||
[proxy, nameInput, sysproxy].forEach((el) => {
|
||||
[
|
||||
proxy, nameInput, sysproxy, subUrl,
|
||||
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
|
||||
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
|
||||
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
|
||||
].forEach((el) => {
|
||||
if (!el) return;
|
||||
el.addEventListener("input", markDirty);
|
||||
el.addEventListener("change", markDirty);
|
||||
el.addEventListener("focus", markDirty);
|
||||
});
|
||||
|
||||
function setMeta(text, kind) {
|
||||
@@ -465,12 +602,16 @@
|
||||
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
||||
renderPings(state.pings || []);
|
||||
renderUpdate(state.update, state.version);
|
||||
if (typeof state.subscription_url === "string" && !dirty) {
|
||||
subUrl.value = state.subscription_url;
|
||||
}
|
||||
|
||||
if (syncForm || (!formHydrated && !dirty)) {
|
||||
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
|
||||
nameInput.value = active.name || state.active_profile || "";
|
||||
proxy.value = typeof state.proxy === "string" ? state.proxy : (active.proxy || "");
|
||||
if (typeof state.system_proxy === "boolean") sysproxy.checked = state.system_proxy;
|
||||
fillHy2(state.hy2);
|
||||
formHydrated = true;
|
||||
if (syncForm) dirty = false;
|
||||
}
|
||||
@@ -498,7 +639,7 @@
|
||||
async function withBusy(fn) {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn].forEach((b) => { if (b) b.disabled = true; });
|
||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn, subBtn].forEach((b) => { if (b) b.disabled = true; });
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
@@ -507,7 +648,7 @@
|
||||
await refresh({ syncForm: true });
|
||||
} catch (e) {
|
||||
setMeta(String(e), "err");
|
||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn].forEach((b) => { if (b) b.disabled = false; });
|
||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn, subBtn].forEach((b) => { if (b) b.disabled = false; });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,6 +668,7 @@
|
||||
} else {
|
||||
setMeta("Подключение…");
|
||||
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
|
||||
await persistHy2IfNeeded();
|
||||
await connect();
|
||||
setMeta("Туннель активен", "ok");
|
||||
}
|
||||
@@ -536,6 +678,7 @@
|
||||
saveBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
|
||||
await persistHy2IfNeeded();
|
||||
setMeta("Профиль сохранён", "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
@@ -589,6 +732,16 @@
|
||||
shopBtn.addEventListener("click", openShop);
|
||||
shopLink.addEventListener("click", openShop);
|
||||
|
||||
subBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Загрузка подписки…");
|
||||
const n = await importSubscription(subUrl.value.trim());
|
||||
formHydrated = false;
|
||||
dirty = false;
|
||||
setMeta("Импортировано профилей: " + n, "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
pingBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Проверка серверов…");
|
||||
|
||||
Reference in New Issue
Block a user