mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-16 22:30:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69c75e92f4 | ||
|
|
977783e39c | ||
|
|
d95e9ffab9 | ||
|
|
513b4cf6b2 | ||
|
|
09e491885f | ||
|
|
e11b74206d | ||
|
|
ae7915c66f | ||
|
|
edf5246e7f | ||
|
|
4ade3a840f | ||
|
|
07cbecb01d | ||
|
|
fc2ff69bdb | ||
|
|
bd10ef8ec9 | ||
|
|
662dd20be1 | ||
|
|
b7ae54b9f3 | ||
|
|
a60ef75061 | ||
|
|
d79ad8976a | ||
|
|
e538fa0573 | ||
|
|
d58b054d93 | ||
|
|
165cc685bb | ||
|
|
243594c5d1 | ||
|
|
753dad7c40 | ||
|
|
3956b0d034 | ||
|
|
1d28cf7b5a | ||
|
|
8c1c143b5e |
@@ -71,6 +71,7 @@ If the PR references an issue, inspect that issue as well and compare the stated
|
||||
- No referenced issue, or only vague claims like "fixes multiple issues" without a concrete issue number
|
||||
- Single large commit or a very small number of commits covering many unrelated areas
|
||||
- PR body reads like a generated report rather than a maintainer-owned change description
|
||||
- PR body or author replies use stock report sections such as "Root Cause", "Root Case", "Checklist", or "Check List" without concrete issue evidence, reproduction context, or project-specific reasoning
|
||||
- Explicit AI provenance links or bot-authored commits from coding agents
|
||||
- Large-scale mechanical edits with little behavioral justification
|
||||
- Random renames, comment rewrites, or same-meaning text changes that do not support the fix
|
||||
@@ -87,6 +88,7 @@ If the PR references an issue, inspect that issue as well and compare the stated
|
||||
- Tests that directly validate the reported regression or expected behavior
|
||||
- Clear explanation of why each changed area is necessary for the fix
|
||||
- Cross-Contextual Logic, the author explains *why* a change was made in a way that shows understanding of the project's specific constraints, rather than just repeating the issue text.
|
||||
- Report-style sections are backed by concrete reproduction steps, failure evidence, or repository-specific constraints; template-required checklists should not count as a slop signal by themselves
|
||||
- Evidence of established repository ownership or ongoing stewardship may reduce slop likelihood, but must never be disclosed in the public comment
|
||||
|
||||
## Decision Rules
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Sync Mihomo Changelog
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# UTC+8 Monday 11:30
|
||||
- cron: '30 3 * * 1'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: sync-mihomo-changelog
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync Changelog
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24.15.0'
|
||||
|
||||
- name: Sync Mihomo version
|
||||
run: node scripts/sync-mihomo-version.mjs
|
||||
|
||||
- name: Detect changelog changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet -- Changelog.md; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mihomo_version="$(grep -Eo 'Mihomo\(Meta\).*v[0-9]+(\.[0-9]+)+' Changelog.md | grep -Eo 'v[0-9]+(\.[0-9]+)+' | head -n 1)"
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "mihomo_version=$mihomo_version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Open pull request
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BASE_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
MIHOMO_VERSION: ${{ steps.changes.outputs.mihomo_version }}
|
||||
run: |
|
||||
branch="chore/sync-mihomo-changelog"
|
||||
title="chore: sync Mihomo changelog to ${MIHOMO_VERSION}"
|
||||
body="Automated update for the Mihomo core version recorded in Changelog.md."
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git fetch origin "$branch:refs/remotes/origin/$branch" || true
|
||||
git checkout -B "$branch"
|
||||
git add Changelog.md
|
||||
git commit -m "$title"
|
||||
git push --force-with-lease origin "$branch"
|
||||
|
||||
if gh pr view "$branch" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
|
||||
gh pr edit "$branch" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "$title" \
|
||||
--body "$body"
|
||||
else
|
||||
gh pr create \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--base "$BASE_BRANCH" \
|
||||
--head "$branch" \
|
||||
--title "$title" \
|
||||
--body "$body"
|
||||
fi
|
||||
Generated
+6
-55
@@ -1150,7 +1150,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clash-verge"
|
||||
version = "2.5.0-rc"
|
||||
version = "2.5.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -1665,19 +1665,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
@@ -1706,15 +1693,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -3490,7 +3468,7 @@ dependencies = [
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.10",
|
||||
"socket2 0.4.10",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -3582,7 +3560,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry 0.6.1",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7256,16 +7234,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
@@ -7817,7 +7785,7 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.5.3",
|
||||
"windows-registry",
|
||||
"windows-result 0.3.4",
|
||||
]
|
||||
|
||||
@@ -7931,18 +7899,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-mihomo"
|
||||
version = "0.1.5"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo?branch=revert#1cc80bc0fbe1245315617f4cecd93710a152325b"
|
||||
version = "0.1.9"
|
||||
source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#a185fe1a8802367d0b3d50d402cb823759d2c57e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"crossbeam",
|
||||
"futures-util",
|
||||
"http 1.4.0",
|
||||
"http-body-util",
|
||||
"httparse",
|
||||
"hyper 1.9.0",
|
||||
"libc",
|
||||
"log",
|
||||
"pin-project",
|
||||
"rand 0.9.4",
|
||||
@@ -9841,17 +9803,6 @@ dependencies = [
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
|
||||
+5
-2
@@ -3,7 +3,7 @@
|
||||
> [!IMPORTANT]
|
||||
> 关于版本的说明:Clash Verge 版本号遵循 x.y.z:x 为重大架构变更,y 为功能新增,z 为 Bug 修复。
|
||||
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.24**
|
||||
- **Mihomo(Meta) 内核升级至 v1.19.25**
|
||||
|
||||
### 🐞 修复问题
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
- 修复修改定时自动更新后记时未及时刷新
|
||||
- 修复 Linux 关闭 TUN 不立即生效
|
||||
- 修复系统代理关闭序列逻辑(防止快速退出时系统代理关闭状态没有保存)
|
||||
- 修复 Linux 快捷键映射错误
|
||||
- 修复 Linux Wayland 兼容性导致渲染错误
|
||||
- 修复 Linux 边缘情况无法加载页面
|
||||
|
||||
### ✨ 新增功能
|
||||
|
||||
@@ -29,4 +32,4 @@
|
||||
|
||||
### 👙 界面样式
|
||||
|
||||
- 代理组实现sticky scroll的效果
|
||||
- 代理组实现 Sticky Scroll 效果
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "2.5.0-rc",
|
||||
"version": "2.5.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"scripts": {
|
||||
"prepare": "husky || true",
|
||||
@@ -76,7 +76,7 @@
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^7.13.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#revert",
|
||||
"tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo",
|
||||
"types-pac": "^1.0.3",
|
||||
"validator": "^13.15.26"
|
||||
},
|
||||
|
||||
Generated
+6
-6
@@ -135,8 +135,8 @@ importers:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
tauri-plugin-mihomo-api:
|
||||
specifier: github:clash-verge-rev/tauri-plugin-mihomo#revert
|
||||
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1cc80bc0fbe1245315617f4cecd93710a152325b
|
||||
specifier: github:clash-verge-rev/tauri-plugin-mihomo
|
||||
version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a185fe1a8802367d0b3d50d402cb823759d2c57e
|
||||
types-pac:
|
||||
specifier: ^1.0.3
|
||||
version: 1.0.3
|
||||
@@ -3361,9 +3361,9 @@ packages:
|
||||
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1cc80bc0fbe1245315617f4cecd93710a152325b:
|
||||
resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1cc80bc0fbe1245315617f4cecd93710a152325b}
|
||||
version: 0.1.5
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a185fe1a8802367d0b3d50d402cb823759d2c57e:
|
||||
resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a185fe1a8802367d0b3d50d402cb823759d2c57e}
|
||||
version: 0.1.9
|
||||
|
||||
terser@5.46.2:
|
||||
resolution: {integrity: sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==}
|
||||
@@ -7045,7 +7045,7 @@ snapshots:
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/1cc80bc0fbe1245315617f4cecd93710a152325b:
|
||||
tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/a185fe1a8802367d0b3d50d402cb823759d2c57e:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import fs from 'node:fs'
|
||||
import fsp from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
const VERSION_URL =
|
||||
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
|
||||
const DEFAULT_CHANGELOG = 'Changelog.md'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const CHECK = args.includes('--check')
|
||||
const DRY_RUN = args.includes('--dry-run')
|
||||
|
||||
const logError = (message, ...optionalParams) =>
|
||||
console.error(message, ...optionalParams)
|
||||
const logInfo = (message, ...optionalParams) =>
|
||||
console.log(message, ...optionalParams)
|
||||
|
||||
function readArgValue(name) {
|
||||
const index = args.indexOf(name)
|
||||
if (index === -1) return null
|
||||
return args[index + 1] ?? null
|
||||
}
|
||||
|
||||
function normalizeVersion(version) {
|
||||
const normalized = version.trim()
|
||||
return normalized.startsWith('v') ? normalized : `v${normalized}`
|
||||
}
|
||||
|
||||
async function fetchLatestVersion() {
|
||||
const explicitVersion = readArgValue('--version')
|
||||
if (explicitVersion) return normalizeVersion(explicitVersion)
|
||||
|
||||
const response = await fetch(VERSION_URL, {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${VERSION_URL}: ${response.status}`)
|
||||
}
|
||||
|
||||
return normalizeVersion(await response.text())
|
||||
}
|
||||
|
||||
function replaceExistingLine(content, version) {
|
||||
const mihomoLine =
|
||||
/^(\s*-\s*(?:\*\*)?\s*Mihomo\(Meta\)\s*内核升级至\s*)v?\d+(?:\.\d+)+(?:[-+][^\s*]+)?((?:\*\*)?\s*)$/m
|
||||
|
||||
if (!mihomoLine.test(content)) return null
|
||||
|
||||
return content.replace(mihomoLine, `$1${version}$2`)
|
||||
}
|
||||
|
||||
function insertMihomoLine(content, version) {
|
||||
const title = /^## .+$/m
|
||||
const match = content.match(title)
|
||||
|
||||
if (!match || match.index === undefined) {
|
||||
throw new Error('could not find changelog title')
|
||||
}
|
||||
|
||||
const titleEnd = match.index + match[0].length
|
||||
const before = content.slice(0, titleEnd)
|
||||
const after = content.slice(titleEnd).replace(/^\n*/, '\n\n')
|
||||
|
||||
return `${before}\n\n- **Mihomo(Meta) 内核升级至 ${version}**${after}`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const cwd = process.cwd()
|
||||
const changelogArg = readArgValue('--file')
|
||||
const changelogPath = path.join(cwd, changelogArg || DEFAULT_CHANGELOG)
|
||||
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
throw new Error(`could not find ${path.relative(cwd, changelogPath)}`)
|
||||
}
|
||||
|
||||
const version = await fetchLatestVersion()
|
||||
const content = await fsp.readFile(changelogPath, 'utf-8')
|
||||
const nextContent =
|
||||
replaceExistingLine(content, version) ?? insertMihomoLine(content, version)
|
||||
|
||||
if (nextContent === content) {
|
||||
logInfo(`Mihomo version in changelog is already ${version}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (CHECK) {
|
||||
logError(`Mihomo version in changelog is not ${version}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (DRY_RUN) {
|
||||
logInfo(`Would update Mihomo version in changelog to ${version}`)
|
||||
return
|
||||
}
|
||||
|
||||
await fsp.writeFile(changelogPath, nextContent)
|
||||
logInfo(`Updated Mihomo version in changelog to ${version}`)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
logError('sync-mihomo-version failed:', error.message)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clash-verge"
|
||||
version = "2.5.0-rc"
|
||||
version = "2.5.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -95,7 +95,7 @@ backon = { version = "1.6.0", features = ["tokio-sleep"] }
|
||||
tauri-plugin-http = "2.5.7"
|
||||
console-subscriber = { version = "0.5.0", optional = true }
|
||||
tauri-plugin-devtools = { version = "2.0.1" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "revert" }
|
||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
|
||||
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
|
||||
async-trait = "0.1.89"
|
||||
clash_verge_service_ipc = { version = "2.3.0", features = [
|
||||
|
||||
@@ -5,7 +5,6 @@ use regex::Regex;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
let cookie_store = Arc::new(Jar::default());
|
||||
@@ -54,12 +53,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
if device_id.is_empty() {
|
||||
return UnlockItem {
|
||||
name: "Bahamut Anime".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return UnlockItem::checked("Bahamut Anime", "Failed", None);
|
||||
}
|
||||
|
||||
let url = format!("https://ani.gamer.com.tw/ajax/token.php?adID=89422&sn=37783&device={device_id}");
|
||||
@@ -79,12 +73,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
if token_result.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Bahamut Anime".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return UnlockItem::checked("Bahamut Anime", "No", None);
|
||||
}
|
||||
|
||||
let region = match client_with_cookies.get("https://ani.gamer.com.tw/").send().await {
|
||||
@@ -92,8 +81,7 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
Ok(body) => match Regex::new(r#"data-geo="([^"]+)"#) {
|
||||
Ok(region_re) => region_re.captures(&body).and_then(|caps| caps.get(1)).map(|m| {
|
||||
let country_code = m.as_str();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{emoji}{country_code}")
|
||||
UnlockItem::region_label(country_code)
|
||||
}),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
@@ -110,10 +98,5 @@ pub(super) async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
UnlockItem {
|
||||
name: "Bahamut Anime".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked("Bahamut Anime", "Yes", region)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use reqwest::Client;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::get_local_date_string;
|
||||
|
||||
pub(super) async fn check_bilibili_china_mainland(client: &Client) -> UnlockItem {
|
||||
let url = "https://api.bilibili.com/pgc/player/web/playurl?avid=82846771&qn=0&type=&otype=json&ep_id=307247&fourk=1&fnver=0&fnval=16&module=bangumi";
|
||||
@@ -24,26 +23,11 @@ pub(super) async fn check_bilibili_china_mainland(client: &Client) -> UnlockItem
|
||||
})
|
||||
.unwrap_or("Failed");
|
||||
|
||||
UnlockItem {
|
||||
name: "哔哩哔哩大陆".to_string(),
|
||||
status: status.to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked("哔哩哔哩大陆", status, None)
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩大陆".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩大陆".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
Err(_) => UnlockItem::checked("哔哩哔哩大陆", "Failed", None),
|
||||
},
|
||||
Err(_) => UnlockItem::checked("哔哩哔哩大陆", "Failed", None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,25 +51,10 @@ pub(super) async fn check_bilibili_hk_mc_tw(client: &Client) -> UnlockItem {
|
||||
})
|
||||
.unwrap_or("Failed");
|
||||
|
||||
UnlockItem {
|
||||
name: "哔哩哔哩港澳台".to_string(),
|
||||
status: status.to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked("哔哩哔哩港澳台", status, None)
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩港澳台".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "哔哩哔哩港澳台".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
Err(_) => UnlockItem::checked("哔哩哔哩港澳台", "Failed", None),
|
||||
},
|
||||
Err(_) => UnlockItem::checked("哔哩哔哩港澳台", "Failed", None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::collections::HashMap;
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
let mut results = Vec::new();
|
||||
@@ -23,10 +22,7 @@ pub(super) async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
}
|
||||
}
|
||||
|
||||
map.get("loc").map(|loc| {
|
||||
let emoji = country_code_to_emoji(loc);
|
||||
format!("{emoji}{loc}")
|
||||
})
|
||||
map.get("loc").map(|loc| UnlockItem::region_label(loc))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -76,19 +72,9 @@ pub(super) async fn check_chatgpt_combined(client: &Client) -> Vec<UnlockItem> {
|
||||
Err(_) => "Failed",
|
||||
};
|
||||
|
||||
results.push(UnlockItem {
|
||||
name: "ChatGPT iOS".to_string(),
|
||||
status: ios_status.to_string(),
|
||||
region: region.clone(),
|
||||
check_time: Some(get_local_date_string()),
|
||||
});
|
||||
results.push(UnlockItem::checked("ChatGPT iOS", ios_status, region.clone()));
|
||||
|
||||
results.push(UnlockItem {
|
||||
name: "ChatGPT Web".to_string(),
|
||||
status: web_status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
});
|
||||
results.push(UnlockItem::checked("ChatGPT Web", web_status, region));
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const BLOCKED_CODES: [&str; 10] = ["AF", "BY", "CN", "CU", "HK", "IR", "KP", "MO", "RU", "SY"];
|
||||
|
||||
@@ -21,40 +20,19 @@ pub(super) async fn check_claude(client: &Client) -> UnlockItem {
|
||||
}
|
||||
|
||||
if let Some(code) = country_code {
|
||||
let emoji = country_code_to_emoji(&code);
|
||||
let status = if BLOCKED_CODES.contains(&code.as_str()) {
|
||||
"No"
|
||||
} else {
|
||||
"Yes"
|
||||
};
|
||||
|
||||
UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: status.to_string(),
|
||||
region: Some(format!("{emoji}{code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked_region("Claude", status, &code)
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked("Claude", "Failed", None)
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
},
|
||||
Err(_) => UnlockItem {
|
||||
name: "Claude".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
Err(_) => UnlockItem::checked("Claude", "Failed", None),
|
||||
},
|
||||
Err(_) => UnlockItem::checked("Claude", "Failed", None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,51 @@ use reqwest::Client;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const DISNEY_PLUS: &str = "Disney+";
|
||||
const AUTH_HEADER: &str = "Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
|
||||
|
||||
fn disney_item(status: impl Into<String>, region: Option<String>) -> UnlockItem {
|
||||
UnlockItem::checked(DISNEY_PLUS, status, region)
|
||||
}
|
||||
|
||||
async fn fetch_main_page_region(client: &Client) -> Option<String> {
|
||||
let body = client
|
||||
.get("https://www.disneyplus.com/")
|
||||
.send()
|
||||
.await
|
||||
.ok()?
|
||||
.text()
|
||||
.await
|
||||
.ok()?;
|
||||
let region_re = match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re,
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
|
||||
}
|
||||
|
||||
async fn disney_region_from_main_page(client: &Client) -> Option<UnlockItem> {
|
||||
let region = fetch_main_page_region(client).await?;
|
||||
Some(disney_item(
|
||||
"Yes",
|
||||
Some(format!("{} (from main page)", UnlockItem::region_label(®ion))),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
let device_api_url = "https://disney.api.edge.bamgrid.com/devices";
|
||||
let auth_header = "Bearer ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84";
|
||||
|
||||
let device_req_body = serde_json::json!({
|
||||
"deviceFamily": "browser",
|
||||
@@ -20,52 +59,27 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
|
||||
let device_result = client
|
||||
.post(device_api_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("authorization", AUTH_HEADER)
|
||||
.header("content-type", "application/json; charset=UTF-8")
|
||||
.json(&device_req_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if device_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let device_response = match device_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to get Disney+ device response: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
Err(_) => {
|
||||
return disney_item("Failed (Network Connection)", None);
|
||||
}
|
||||
};
|
||||
|
||||
if device_response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No (IP Banned By Disney+)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("No (IP Banned By Disney+)", None);
|
||||
}
|
||||
|
||||
let device_body = match device_response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot read response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("Failed (Error: Cannot read response)", None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,12 +92,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
"Failed to compile assertion regex for Disney+: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("Failed (Regex Error)", None);
|
||||
}
|
||||
};
|
||||
let assertion = match re.captures(&device_body) {
|
||||
@@ -92,27 +101,11 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
if assertion.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot extract assertion)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("Failed (Error: Cannot extract assertion)", None);
|
||||
}
|
||||
|
||||
let token_url = "https://disney.api.edge.bamgrid.com/token";
|
||||
let assertion_str = match assertion {
|
||||
Some(assertion) => assertion,
|
||||
None => {
|
||||
logging!(error, Type::Network, "No assertion found for Disney+");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (No Assertion)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let assertion_str = assertion.unwrap_or_default();
|
||||
let token_body = [
|
||||
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
|
||||
("latitude", "0"),
|
||||
@@ -124,31 +117,16 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
|
||||
let token_result = client
|
||||
.post(token_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("authorization", AUTH_HEADER)
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.form(&token_body)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if token_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let token_response = match token_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to get Disney+ token response: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
Err(_) => {
|
||||
return disney_item("Failed (Network Connection)", None);
|
||||
}
|
||||
};
|
||||
let token_status = token_response.status();
|
||||
@@ -156,22 +134,12 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
let token_body_text = match token_response.text().await {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Error: Cannot read token response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("Failed (Error: Cannot read token response)", None);
|
||||
}
|
||||
};
|
||||
|
||||
if token_body_text.contains("forbidden-location") || token_body_text.contains("403 ERROR") {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No (IP Banned By Disney+)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("No (IP Banned By Disney+)", None);
|
||||
}
|
||||
|
||||
let token_json: Result<serde_json::Value, _> = serde_json::from_str(&token_body_text);
|
||||
@@ -198,16 +166,14 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
if refresh_token.is_none() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
return disney_item(
|
||||
format!(
|
||||
"Failed (Error: Cannot extract refresh token, status: {}, response: {})",
|
||||
token_status.as_u16(),
|
||||
token_body_text.chars().take(100).collect::<String>() + "..."
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let graphql_url = "https://disney.api.edge.bamgrid.com/graph/v1/device/graphql";
|
||||
@@ -219,21 +185,12 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
|
||||
let graphql_result = client
|
||||
.post(graphql_url)
|
||||
.header("authorization", auth_header)
|
||||
.header("authorization", AUTH_HEADER)
|
||||
.header("content-type", "application/json")
|
||||
.body(graphql_payload)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if graphql_result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let preview_check = client.get("https://disneyplus.com").send().await;
|
||||
|
||||
let is_unavailable = match preview_check {
|
||||
@@ -246,14 +203,8 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
|
||||
let graphql_response = match graphql_result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to get Disney+ GraphQL response: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
Err(_) => {
|
||||
return disney_item("Failed (Network Connection)", None);
|
||||
}
|
||||
};
|
||||
let graphql_status = graphql_response.status();
|
||||
@@ -271,58 +222,27 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
};
|
||||
|
||||
if graphql_body_text.is_empty() || graphql_status.as_u16() >= 400 {
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(region) = region_from_main {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
if let Some(item) = disney_region_from_main_page(client).await {
|
||||
return item;
|
||||
}
|
||||
|
||||
if graphql_body_text.is_empty() {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
return disney_item(
|
||||
format!(
|
||||
"Failed (GraphQL error: empty response, status: {})",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
None,
|
||||
);
|
||||
}
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!(
|
||||
return disney_item(
|
||||
format!(
|
||||
"Failed (GraphQL error: {}, status: {})",
|
||||
graphql_body_text.chars().take(50).collect::<String>() + "...",
|
||||
graphql_status.as_u16()
|
||||
),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let region_re = match Regex::new(r#""countryCode"\s*:\s*"([^"]+)"#) {
|
||||
@@ -334,12 +254,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
"Failed to compile Disney+ countryCode regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("Failed (Regex Error)", None);
|
||||
}
|
||||
};
|
||||
let region_code = region_re
|
||||
@@ -355,12 +270,7 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
"Failed to compile Disney+ supported location regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("Failed (Regex Error)", None);
|
||||
}
|
||||
};
|
||||
let in_supported_location = supported_re
|
||||
@@ -368,101 +278,29 @@ pub(super) async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str() == "true"));
|
||||
|
||||
if region_code.is_none() {
|
||||
let region_from_main = match client.get("https://www.disneyplus.com/").send().await {
|
||||
Ok(response) => match response.text().await {
|
||||
Ok(body) => match Regex::new(r#"region"\s*:\s*"([^"]+)"#) {
|
||||
Ok(region_re) => region_re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string())),
|
||||
Err(e) => {
|
||||
logging!(
|
||||
error,
|
||||
Type::Network,
|
||||
"Failed to compile Disney+ main page region regex: {}",
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(_) => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
if let Some(region) = region_from_main {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region} (from main page)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
if let Some(item) = disney_region_from_main_page(client).await {
|
||||
return item;
|
||||
}
|
||||
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("No", None);
|
||||
}
|
||||
|
||||
let region = match region_code {
|
||||
Some(code) => code,
|
||||
None => {
|
||||
logging!(error, Type::Network, "No region code found for Disney+");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
};
|
||||
let region = region_code.unwrap_or_default();
|
||||
|
||||
if region == "JP" {
|
||||
let emoji = country_code_to_emoji("JP");
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return UnlockItem::checked_region(DISNEY_PLUS, "Yes", ®ion);
|
||||
}
|
||||
|
||||
if is_unavailable {
|
||||
return UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return disney_item("No", None);
|
||||
}
|
||||
|
||||
match in_supported_location {
|
||||
Some(false) => {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Soon".to_string(),
|
||||
region: Some(format!("{emoji}{region}(即将上线)")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Some(true) => {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
None => UnlockItem {
|
||||
name: "Disney+".to_string(),
|
||||
status: format!("Failed (Error: Unknown region status for {region})"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
Some(false) => disney_item(
|
||||
"Soon",
|
||||
Some(format!("{}(即将上线)", UnlockItem::region_label(®ion))),
|
||||
),
|
||||
Some(true) => UnlockItem::checked_region(DISNEY_PLUS, "Yes", ®ion),
|
||||
None => disney_item(format!("Failed (Error: Unknown region status for {region})"), None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const BLOCKED_CODES: [&str; 9] = ["CHN", "RUS", "BLR", "CUB", "IRN", "PRK", "SYR", "HKG", "MAC"];
|
||||
const REGION_MARKER: &str = ",2,1,200,\"";
|
||||
|
||||
pub(super) async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
let url = "https://gemini.google.com";
|
||||
let failed = || UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
let failed = || UnlockItem::checked("Gemini", "Failed", None);
|
||||
|
||||
let response = match client.get(url).send().await {
|
||||
Ok(r) => r,
|
||||
@@ -34,14 +28,8 @@ pub(super) async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
|
||||
match country_code {
|
||||
Some(code) => {
|
||||
let emoji = country_code_to_emoji(code);
|
||||
let status = if BLOCKED_CODES.contains(&code) { "No" } else { "Yes" };
|
||||
UnlockItem {
|
||||
name: "Gemini".to_string(),
|
||||
status: status.to_string(),
|
||||
region: Some(format!("{emoji}{code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked_region("Gemini", status, code)
|
||||
}
|
||||
None => failed(),
|
||||
}
|
||||
|
||||
@@ -4,7 +4,12 @@ use serde_json::Value;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const NETFLIX: &str = "Netflix";
|
||||
|
||||
fn netflix_item(status: impl Into<String>, region: Option<String>) -> UnlockItem {
|
||||
UnlockItem::checked(NETFLIX, status, region)
|
||||
}
|
||||
|
||||
pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
let cdn_result = check_netflix_cdn(client).await;
|
||||
@@ -23,12 +28,7 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
|
||||
if let Err(e) = &result1 {
|
||||
logging!(error, Type::Network, "Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("Failed", None);
|
||||
}
|
||||
|
||||
let result2 = client
|
||||
@@ -39,24 +39,14 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
logging!(error, Type::Network, "Netflix请求错误: {e}");
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("Failed", None);
|
||||
}
|
||||
|
||||
let status1 = match result1 {
|
||||
Ok(response) => response.status().as_u16(),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to get Netflix response 1: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("Failed", None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,31 +54,16 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
Ok(response) => response.status().as_u16(),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to get Netflix response 2: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("Failed", None);
|
||||
}
|
||||
};
|
||||
|
||||
if status1 == 404 && status2 == 404 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Originals Only".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("Originals Only", None);
|
||||
}
|
||||
|
||||
if status1 == 403 || status2 == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("No", None);
|
||||
}
|
||||
|
||||
if status1 == 200 || status1 == 301 || status2 == 200 || status2 == 301 {
|
||||
@@ -106,41 +81,19 @@ pub(super) async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
let parts: Vec<&str> = location_str.split('/').collect();
|
||||
if parts.len() >= 4 {
|
||||
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
||||
let emoji = country_code_to_emoji(region_code);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region_code}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return UnlockItem::checked_region(NETFLIX, "Yes", region_code);
|
||||
}
|
||||
}
|
||||
|
||||
let emoji = country_code_to_emoji("us");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{}", "us")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked_region(NETFLIX, "Yes", "us")
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "获取Netflix区域信息失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
netflix_item("Yes (但无法获取区域)", None)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (状态码: {status1}_{status2}"),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
netflix_item(format!("Failed (状态码: {status1}_{status2}"), None)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +103,7 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
match client.get(url).timeout(std::time::Duration::from_secs(30)).send().await {
|
||||
Ok(response) => {
|
||||
if response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No (IP Banned By Netflix)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return netflix_item("No (IP Banned By Netflix)", None);
|
||||
}
|
||||
|
||||
match response.json::<Value>().await {
|
||||
@@ -165,41 +113,20 @@ async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
&& let Some(location) = targets[0].get("location")
|
||||
&& let Some(country) = location.get("country").and_then(|c| c.as_str())
|
||||
{
|
||||
let emoji = country_code_to_emoji(country);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{country}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return UnlockItem::checked_region(NETFLIX, "Yes", country);
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Unknown".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
netflix_item("Unknown", None)
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "解析Fast.com API响应失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
netflix_item("Failed (解析错误)", None)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Fast.com API请求失败: {e}");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
netflix_item("Failed (CDN API)", None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,22 @@ use reqwest::Client;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
const PRIME_VIDEO: &str = "Prime Video";
|
||||
|
||||
fn prime_video_item(status: impl Into<String>, region: Option<String>) -> UnlockItem {
|
||||
UnlockItem::checked(PRIME_VIDEO, status, region)
|
||||
}
|
||||
|
||||
pub(super) async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.primevideo.com";
|
||||
|
||||
let result = client.get(url).send().await;
|
||||
|
||||
if result.is_err() {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to get Prime Video response: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
Err(_) => {
|
||||
return prime_video_item("Failed (Network Connection)", None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,12 +36,7 @@ pub(super) async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
"Failed to compile Prime Video region regex: {}",
|
||||
e
|
||||
);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Regex Error)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return prime_video_item("Failed (Regex Error)", None);
|
||||
}
|
||||
};
|
||||
let region_code = region_re
|
||||
@@ -59,45 +44,19 @@ pub(super) async fn check_prime_video(client: &Client) -> UnlockItem {
|
||||
.and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()));
|
||||
|
||||
if is_blocked {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "No (Service Not Available)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return prime_video_item("No (Service Not Available)", None);
|
||||
}
|
||||
|
||||
if let Some(region) = region_code {
|
||||
let emoji = country_code_to_emoji(®ion);
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{emoji}{region}")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return UnlockItem::checked_region(PRIME_VIDEO, "Yes", ®ion);
|
||||
}
|
||||
|
||||
if !is_blocked {
|
||||
return UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: PAGE ERROR)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
return prime_video_item("Failed (Error: PAGE ERROR)", None);
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: Unknown Region)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
prime_video_item("Failed (Error: Unknown Region)", None)
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Prime Video".to_string(),
|
||||
status: "Failed (Error: Cannot read response)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
Err(_) => prime_video_item("Failed (Error: Cannot read response)", None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use reqwest::{Client, Url};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_spotify(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.spotify.com/api/content/v1/country-selector?platform=web&format=json";
|
||||
@@ -15,19 +14,9 @@ pub(super) async fn check_spotify(client: &Client) -> UnlockItem {
|
||||
let region = extract_region(&final_url).or_else(|| extract_region_from_body(&body));
|
||||
let status = determine_status(status_code.as_u16(), &body);
|
||||
|
||||
UnlockItem {
|
||||
name: "Spotify".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked("Spotify", status, region)
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Spotify".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
Err(_) => UnlockItem::checked("Spotify", "Failed", None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,8 +47,7 @@ fn extract_region(url: &Url) -> Option<String> {
|
||||
|
||||
let country_code = first_segment.split('-').next().unwrap_or(first_segment);
|
||||
let upper = country_code.to_uppercase();
|
||||
let emoji = country_code_to_emoji(&upper);
|
||||
Some(format!("{emoji}{upper}"))
|
||||
Some(UnlockItem::region_label(&upper))
|
||||
}
|
||||
|
||||
fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
@@ -70,8 +58,7 @@ fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
if let Some(end) = rest.find('"') {
|
||||
let code = rest[..end].to_uppercase();
|
||||
if !code.is_empty() {
|
||||
let emoji = country_code_to_emoji(&code);
|
||||
return Some(format!("{emoji}{code}"));
|
||||
return Some(UnlockItem::region_label(&code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_tiktok(client: &Client) -> UnlockItem {
|
||||
let trace_url = "https://www.tiktok.com/cdn-cgi/trace";
|
||||
@@ -38,12 +37,7 @@ pub(super) async fn check_tiktok(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
|
||||
UnlockItem {
|
||||
name: "TikTok".to_string(),
|
||||
status,
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
UnlockItem::checked("TikTok", status, region)
|
||||
}
|
||||
|
||||
fn determine_status(status: u16, body: &str) -> &'static str {
|
||||
@@ -78,8 +72,7 @@ fn extract_region_from_body(body: &str) -> Option<String> {
|
||||
let raw = matched.as_str();
|
||||
let country_code = raw.split('-').next().unwrap_or(raw).to_uppercase();
|
||||
if !country_code.is_empty() {
|
||||
let emoji = country_code_to_emoji(&country_code);
|
||||
return Some(format!("{emoji}{country_code}"));
|
||||
return Some(UnlockItem::region_label(&country_code));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UnlockItem {
|
||||
pub name: String,
|
||||
@@ -9,6 +11,24 @@ pub struct UnlockItem {
|
||||
}
|
||||
|
||||
impl UnlockItem {
|
||||
pub fn checked(name: &str, status: impl Into<String>, region: Option<String>) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
status: status.into(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checked_region(name: &str, status: impl Into<String>, country_code: &str) -> Self {
|
||||
Self::checked(name, status, Some(Self::region_label(country_code)))
|
||||
}
|
||||
|
||||
pub fn region_label(country_code: &str) -> String {
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
format!("{emoji}{country_code}")
|
||||
}
|
||||
|
||||
pub fn pending(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
|
||||
@@ -4,58 +4,56 @@ use reqwest::Client;
|
||||
use clash_verge_logging::{Type, logging};
|
||||
|
||||
use super::UnlockItem;
|
||||
use super::utils::{country_code_to_emoji, get_local_date_string};
|
||||
|
||||
pub(super) async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.youtube.com/premium";
|
||||
let url = "https://www.youtube.com/premium?hl=en";
|
||||
let item = |status: &str, region: Option<String>| UnlockItem::checked("YouTube Premium", status, region);
|
||||
|
||||
match client.get(url).send().await {
|
||||
Ok(response) => {
|
||||
let status_code = response.status().as_u16();
|
||||
|
||||
if let Ok(body) = response.text().await {
|
||||
let body_lower = body.to_lowercase();
|
||||
let mut status = "Failed";
|
||||
let mut region = None;
|
||||
|
||||
if body_lower.contains("youtube premium is not available in your country") {
|
||||
status = "No";
|
||||
} else if body_lower.contains("ad-free") {
|
||||
match Regex::new(r#"id="country-code"[^>]*>([^<]+)<"#) {
|
||||
Ok(re) => {
|
||||
if let Some(caps) = re.captures(&body)
|
||||
&& let Some(m) = caps.get(1)
|
||||
{
|
||||
let country_code = m.as_str().trim();
|
||||
let emoji = country_code_to_emoji(country_code);
|
||||
region = Some(format!("{emoji}{country_code}"));
|
||||
status = "Yes";
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to compile YouTube Premium regex: {}", e);
|
||||
}
|
||||
let body_lower = body.to_ascii_lowercase();
|
||||
let region = [
|
||||
r#"id=["']country-code["'][^>]*>\s*([A-Za-z]{2,3})\s*<"#,
|
||||
r#""GL"\s*:\s*"([A-Za-z]{2})""#,
|
||||
r#""countryCode"\s*:\s*"([A-Za-z]{2})""#,
|
||||
r#""country_code"\s*:\s*"([A-Za-z]{2})""#,
|
||||
]
|
||||
.iter()
|
||||
.find_map(|pattern| match Regex::new(pattern) {
|
||||
Ok(re) => re
|
||||
.captures(&body)
|
||||
.and_then(|caps| caps.get(1))
|
||||
.map(|m| m.as_str().trim().to_ascii_uppercase()),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Network, "Failed to compile YouTube Premium regex: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(|country_code| UnlockItem::region_label(&country_code));
|
||||
|
||||
UnlockItem {
|
||||
name: "YouTube Premium".to_string(),
|
||||
status: status.to_string(),
|
||||
region,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
let status = if body_lower.contains("youtube premium is not available in your country")
|
||||
|| body_lower.contains("premium is not available in your country")
|
||||
|| body_lower.contains("premium is not available in your region")
|
||||
{
|
||||
"No"
|
||||
} else if (200..300).contains(&status_code)
|
||||
&& (body_lower.contains("youtube premium")
|
||||
|| body_lower.contains("ad-free")
|
||||
|| body_lower.contains(r#""browseid":"spunlimited""#))
|
||||
{
|
||||
"Yes"
|
||||
} else {
|
||||
"Failed"
|
||||
};
|
||||
|
||||
item(status, region)
|
||||
} else {
|
||||
UnlockItem {
|
||||
name: "YouTube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
item("Failed", None)
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "YouTube Premium".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
},
|
||||
Err(_) => item("Failed", None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,9 +591,9 @@ fn fix_dirty_url(input: &str) -> Result<Url> {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"failed to parse deep link url: {:?}, input: {:?}",
|
||||
"failed to parse subscription URL: {:?}, input: {}",
|
||||
e,
|
||||
input
|
||||
help::mask_url(input)
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,6 +101,7 @@ impl Sysopt {
|
||||
}
|
||||
if !verge.enable_proxy_guard.unwrap_or_default() {
|
||||
logging!(info, Type::Core, "System proxy guard is disabled.");
|
||||
self.access_guard().write().stop();
|
||||
return;
|
||||
}
|
||||
logging!(
|
||||
|
||||
@@ -6,7 +6,7 @@ use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tauri::async_runtime::JoinHandle;
|
||||
use tauri_plugin_mihomo::models::ConnectionId;
|
||||
use tauri_plugin_mihomo::models::WebSocketConnectionId;
|
||||
|
||||
/// 托盘速率流异常后的重连间隔。
|
||||
const TRAY_SPEED_RETRY_DELAY: Duration = Duration::from_secs(1);
|
||||
@@ -19,7 +19,7 @@ const TRAY_SPEED_STALE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
#[derive(Clone)]
|
||||
pub struct TraySpeedController {
|
||||
speed_task: Arc<Mutex<Option<JoinHandle<()>>>>,
|
||||
speed_connection_id: Arc<Mutex<Option<ConnectionId>>>,
|
||||
speed_connection_id: Arc<Mutex<Option<WebSocketConnectionId>>>,
|
||||
}
|
||||
|
||||
impl Default for TraySpeedController {
|
||||
@@ -162,17 +162,19 @@ impl TraySpeedController {
|
||||
}
|
||||
|
||||
fn set_speed_connection_id(
|
||||
speed_connection_id: &Arc<Mutex<Option<ConnectionId>>>,
|
||||
connection_id: Option<ConnectionId>,
|
||||
speed_connection_id: &Arc<Mutex<Option<WebSocketConnectionId>>>,
|
||||
connection_id: Option<WebSocketConnectionId>,
|
||||
) {
|
||||
*speed_connection_id.lock() = connection_id;
|
||||
}
|
||||
|
||||
fn take_speed_connection_id(speed_connection_id: &Arc<Mutex<Option<ConnectionId>>>) -> Option<ConnectionId> {
|
||||
fn take_speed_connection_id(
|
||||
speed_connection_id: &Arc<Mutex<Option<WebSocketConnectionId>>>,
|
||||
) -> Option<WebSocketConnectionId> {
|
||||
speed_connection_id.lock().take()
|
||||
}
|
||||
|
||||
async fn disconnect_speed_connection(speed_connection_id: &Arc<Mutex<Option<ConnectionId>>>) {
|
||||
async fn disconnect_speed_connection(speed_connection_id: &Arc<Mutex<Option<WebSocketConnectionId>>>) {
|
||||
if let Some(connection_id) = Self::take_speed_connection_id(speed_connection_id) {
|
||||
connections_stream::disconnect_connection(connection_id).await;
|
||||
}
|
||||
|
||||
+14
-24
@@ -24,7 +24,6 @@ use tauri::{AppHandle, Manager as _};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_deep_link::DeepLinkExt as _;
|
||||
use tauri_plugin_mihomo::RejectPolicy;
|
||||
|
||||
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
|
||||
/// Application initialization helper functions
|
||||
@@ -59,15 +58,6 @@ mod app_init {
|
||||
tauri_plugin_mihomo::Builder::new()
|
||||
.protocol(tauri_plugin_mihomo::models::Protocol::LocalSocket)
|
||||
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
|
||||
.pool_config(
|
||||
tauri_plugin_mihomo::IpcPoolConfigBuilder::new()
|
||||
.min_connections(3)
|
||||
.max_connections(32)
|
||||
.idle_timeout(std::time::Duration::from_secs(60))
|
||||
.health_check_interval(std::time::Duration::from_secs(60))
|
||||
.reject_policy(RejectPolicy::Wait)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
|
||||
@@ -226,6 +216,8 @@ pub fn run() {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
utils::linux::workarounds::apply_nvidia_dmabuf_renderer_workaround();
|
||||
#[cfg(target_os = "linux")]
|
||||
utils::linux::workarounds::apply_wayland_webkit_fix();
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
@@ -395,22 +387,20 @@ pub fn run() {
|
||||
event_handlers::handle_reopen(has_visible_windows).await;
|
||||
});
|
||||
}
|
||||
tauri::RunEvent::Exit => AsyncHandler::block_on(async {
|
||||
if !handle::Handle::global().is_exiting() {
|
||||
feat::quit().await;
|
||||
}
|
||||
}),
|
||||
tauri::RunEvent::Exit => {
|
||||
logging!(info, Type::System, "Application exited");
|
||||
}
|
||||
#[allow(unused_variables)]
|
||||
tauri::RunEvent::ExitRequested { api, code, .. } => {
|
||||
if core::handle::Handle::global().is_exiting() {
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncHandler::block_on(async {
|
||||
let _ = handle::Handle::mihomo().await.clear_all_ws_connections().await;
|
||||
});
|
||||
|
||||
if code.is_none() {
|
||||
if module::lightweight::is_in_lightweight_mode() && !handle::Handle::global().is_exiting() {
|
||||
api.prevent_exit();
|
||||
} else if code.is_none() {
|
||||
api.prevent_exit();
|
||||
if !handle::Handle::global().is_exiting() {
|
||||
AsyncHandler::spawn(|| async {
|
||||
feat::quit().await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } if label == "main" => match event {
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use tauri_plugin_mihomo::models::{ConnectionId, WebSocketMessage};
|
||||
use tauri_plugin_mihomo::models::{WebSocketConnectionId, WebSocketMessage};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::Instant;
|
||||
|
||||
@@ -39,7 +39,7 @@ enum InternalWsEvent<T> {
|
||||
/// Mihomo WebSocket 订阅句柄(通用事件流)。
|
||||
pub struct MihomoWsEventStream<T> {
|
||||
/// 当前订阅连接 ID,用于主动断开。
|
||||
pub connection_id: ConnectionId,
|
||||
pub connection_id: WebSocketConnectionId,
|
||||
/// 当前订阅消息接收器。
|
||||
receiver: mpsc::Receiver<InternalWsEvent<T>>,
|
||||
/// 最近一次收到有效事件的时间戳。
|
||||
@@ -162,7 +162,7 @@ impl<T> MihomoWsEventStream<T> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `connection_id` - 目标连接 ID
|
||||
pub async fn disconnect_connection(connection_id: ConnectionId) {
|
||||
pub async fn disconnect_connection(connection_id: WebSocketConnectionId) {
|
||||
if let Err(err) = handle::Handle::mihomo()
|
||||
.await
|
||||
.disconnect(connection_id, Some(MIHOMO_WS_STREAM_CLOSE_CODE))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! These helpers are not fixes and may stop working as environments change.
|
||||
|
||||
use clash_verge_logging::{Type, logging};
|
||||
use std::{fs, path::Path};
|
||||
use std::{env, fs, path::Path, process::Command};
|
||||
|
||||
pub fn apply_nvidia_dmabuf_renderer_workaround() {
|
||||
if std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_some() {
|
||||
@@ -23,6 +23,29 @@ pub fn apply_nvidia_dmabuf_renderer_workaround() {
|
||||
}
|
||||
}
|
||||
|
||||
/// !Might cause more memory footpoint
|
||||
pub fn apply_wayland_webkit_fix() {
|
||||
let is_wayland = env::var("XDG_SESSION_TYPE").unwrap_or_default() == "wayland";
|
||||
|
||||
if !is_wayland {
|
||||
return;
|
||||
}
|
||||
|
||||
let version = Command::new("pkg-config")
|
||||
.args(["--modversion", "wayland-client"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok());
|
||||
|
||||
if let Some(v) = version
|
||||
&& v.trim() <= "1.23.0"
|
||||
{
|
||||
unsafe {
|
||||
env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_nvidia_gpu() -> bool {
|
||||
if Path::new("/proc/driver/nvidia/version").exists()
|
||||
|| Path::new("/sys/module/nvidia").exists()
|
||||
|
||||
@@ -8,12 +8,11 @@ use tauri::Url;
|
||||
use crate::{
|
||||
config::{Config, PrfItem, profiles},
|
||||
core::{CoreManager, handle},
|
||||
utils::help,
|
||||
};
|
||||
use clash_verge_logging::{Type, logging, logging_error};
|
||||
|
||||
pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
logging!(info, Type::Config, "received deep link: {param}");
|
||||
|
||||
let param_str = if param.starts_with("[") && param.len() > 4 {
|
||||
param
|
||||
.get(2..param.len() - 2)
|
||||
@@ -21,12 +20,19 @@ pub(super) async fn resolve_scheme(param: &str) -> Result<()> {
|
||||
} else {
|
||||
param
|
||||
};
|
||||
let masked_deep_link = help::mask_url(param_str);
|
||||
|
||||
let link_parsed =
|
||||
Url::parse(param_str).map_err(|e| anyhow::anyhow!("failed to parse deep link: {:?}, param: {:?}", e, param))?;
|
||||
logging!(debug, Type::Config, "received deep link: {masked_deep_link}");
|
||||
|
||||
let link_parsed = Url::parse(param_str)
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse deep link: {e:?}, param: {masked_deep_link}"))?;
|
||||
|
||||
let Some((url, name)) = extract_subscription_info(&link_parsed) else {
|
||||
logging!(error, Type::Config, "missing url parameter in deep link: {}", param_str);
|
||||
logging!(
|
||||
warn,
|
||||
Type::Config,
|
||||
"missing url parameter in deep link: {masked_deep_link}"
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use dark_light::{Mode as SystemTheme, detect as detect_system_theme};
|
||||
use tauri::utils::config::Color;
|
||||
use tauri::webview::PageLoadEvent;
|
||||
use tauri::{Theme, WebviewWindow};
|
||||
|
||||
use crate::{config::Config, core::handle, utils::resolve::window_script::build_window_initial_script};
|
||||
@@ -67,7 +68,15 @@ pub async fn build_new_window() -> Result<WebviewWindow, String> {
|
||||
.inner_size(DEFAULT_WIDTH, DEFAULT_HEIGHT)
|
||||
.min_inner_size(MINIMAL_WIDTH, MINIMAL_HEIGHT)
|
||||
.visible(false) // 等待主题色准备好后再展示,避免启动色差
|
||||
.initialization_script(&initial_script);
|
||||
.initialization_script(&initial_script)
|
||||
.on_page_load(move |window, payload| {
|
||||
if payload.event() != PageLoadEvent::Finished {
|
||||
return;
|
||||
}
|
||||
|
||||
logging_error!(Type::Window, window.show());
|
||||
logging_error!(Type::Window, window.set_focus());
|
||||
});
|
||||
|
||||
if let Some(theme) = resolved_theme {
|
||||
builder = builder.theme(Some(theme));
|
||||
|
||||
@@ -12,8 +12,8 @@ use objc2::rc::Retained;
|
||||
use objc2::runtime::AnyObject;
|
||||
use objc2_app_kit::{
|
||||
NSBaselineOffsetAttributeName, NSColor, NSFont, NSFontAttributeName, NSFontWeightRegular,
|
||||
NSForegroundColorAttributeName, NSMutableParagraphStyle, NSParagraphStyleAttributeName, NSStatusItem,
|
||||
NSTextAlignment,
|
||||
NSForegroundColorAttributeName, NSMutableParagraphStyle, NSParagraphStyleAttributeName, NSStatusBarButton,
|
||||
NSStatusItem, NSTextAlignment,
|
||||
};
|
||||
use objc2_foundation::{NSAttributedString, NSDictionary, NSNumber, NSString};
|
||||
|
||||
@@ -90,6 +90,16 @@ fn create_attributed_string(
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_click_target_frame(button: &NSStatusBarButton) {
|
||||
let bounds = button.bounds();
|
||||
let subviews = button.subviews();
|
||||
|
||||
for index in 0..subviews.count() {
|
||||
let subview = subviews.objectAtIndex(index);
|
||||
subview.setFrame(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
/// 在主线程下设置 NSStatusItem 按钮的富文本标题
|
||||
///
|
||||
/// 依赖 Tauri `with_inner_tray_icon` 保证回调在主线程执行;
|
||||
@@ -113,6 +123,7 @@ fn apply_status_item_attributed_title(
|
||||
};
|
||||
let attr_str = create_attributed_string(text, attrs);
|
||||
button.setAttributedTitle(&attr_str);
|
||||
sync_click_target_frame(&button);
|
||||
}
|
||||
|
||||
/// 将速率以富文本形式设置到 NSStatusItem 的按钮上
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.5.0-rc",
|
||||
"version": "2.5.0",
|
||||
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
||||
@@ -120,6 +120,11 @@
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
&-drag-region {
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content__left .the-logo {
|
||||
@@ -148,6 +153,12 @@
|
||||
box-sizing: border-box;
|
||||
height: 36px;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
&-drag-region {
|
||||
align-self: stretch;
|
||||
flex: 1 1 auto;
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-content__left {
|
||||
|
||||
@@ -12,6 +12,7 @@ export {
|
||||
} from './base-split-chip-editor'
|
||||
export { BaseStyledSelect } from './base-styled-select'
|
||||
export { BaseStyledTextField } from './base-styled-text-field'
|
||||
export { MonacoEditor } from './monaco-editor'
|
||||
export { Switch } from './base-switch'
|
||||
export { TooltipIcon } from './base-tooltip-icon'
|
||||
export { VirtualList, type VirtualListHandle } from './virtual-list'
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Suspense, lazy } from 'react'
|
||||
|
||||
import { loadMonacoEditor } from '@/services/monaco'
|
||||
|
||||
type MonacoEditorProps = import('@monaco-editor/react').EditorProps
|
||||
|
||||
let monacoEditorBundle: Awaited<ReturnType<typeof loadMonacoEditor>>
|
||||
|
||||
const MonacoEditorView = ({ beforeMount, ...props }: MonacoEditorProps) => {
|
||||
const { Editor, beforeEditorMount } = monacoEditorBundle
|
||||
|
||||
return (
|
||||
<Editor
|
||||
{...props}
|
||||
beforeMount={(monaco) => {
|
||||
beforeEditorMount()
|
||||
beforeMount?.(monaco)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const MonacoEditorContent = lazy(async () => {
|
||||
monacoEditorBundle = await loadMonacoEditor()
|
||||
return { default: MonacoEditorView }
|
||||
})
|
||||
|
||||
export const MonacoEditor = (props: MonacoEditorProps) => (
|
||||
<Suspense fallback={null}>
|
||||
<MonacoEditorContent {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ViewColumnRounded } from '@mui/icons-material'
|
||||
import { Box, IconButton, Tooltip } from '@mui/material'
|
||||
import { Box } from '@mui/material'
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnOrderState,
|
||||
@@ -128,20 +127,6 @@ const SX_RESIZE_HANDLE: React.ComponentProps<typeof Box>['sx'] = {
|
||||
},
|
||||
}
|
||||
|
||||
const SX_ICON_BTN: React.ComponentProps<typeof IconButton>['sx'] = {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
right: 4,
|
||||
zIndex: 3,
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? theme.palette.background.default
|
||||
: theme.palette.background.paper,
|
||||
'&:hover': {
|
||||
backgroundColor: (theme) => theme.palette.action.hover,
|
||||
},
|
||||
}
|
||||
|
||||
const SX_HEADER_ROW: React.ComponentProps<typeof Box>['sx'] = {
|
||||
display: 'flex',
|
||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
@@ -311,7 +296,6 @@ interface Props {
|
||||
connections: IConnectionsItem[]
|
||||
onShowDetail: (data: IConnectionsItem) => void
|
||||
columnManagerOpen: boolean
|
||||
onOpenColumnManager: () => void
|
||||
onCloseColumnManager: () => void
|
||||
}
|
||||
|
||||
@@ -320,7 +304,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
connections,
|
||||
onShowDetail: rawOnShowDetail,
|
||||
columnManagerOpen,
|
||||
onOpenColumnManager,
|
||||
onCloseColumnManager,
|
||||
} = props
|
||||
const onShowDetailRef = useRef(rawOnShowDetail)
|
||||
@@ -633,15 +616,6 @@ export const ConnectionTable = (props: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Box sx={SX_OUTER}>
|
||||
<Tooltip title={t('connections.components.columnManager.title')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onOpenColumnManager}
|
||||
sx={SX_ICON_BTN}
|
||||
>
|
||||
<ViewColumnRounded fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Box ref={tableContainerRef} sx={SX_SCROLL_CONTAINER}>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import {
|
||||
CloseFullscreenRounded,
|
||||
ContentPasteRounded,
|
||||
@@ -16,14 +15,13 @@ import {
|
||||
} from '@mui/material'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseLoadingOverlay } from '@/components/base'
|
||||
import { beforeEditorMount } from '@/services/monaco'
|
||||
import { BaseLoadingOverlay, MonacoEditor } from '@/components/base'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { MonacoEditorInstance, MonacoMarker } from '@/types/monaco'
|
||||
import debounce from '@/utils/debounce'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
@@ -44,7 +42,7 @@ export interface EditorViewerProps {
|
||||
onChange?: (value: string) => void
|
||||
onSave?: () => void | Promise<void>
|
||||
onClose: () => void
|
||||
onValidate?: (markers: editor.IMarker[]) => void
|
||||
onValidate?: (markers: MonacoMarker[]) => void
|
||||
}
|
||||
|
||||
export const EditorViewer = ({
|
||||
@@ -65,7 +63,7 @@ export const EditorViewer = ({
|
||||
const { t } = useTranslation()
|
||||
const themeMode = useThemeMode()
|
||||
const [isMaximized, setIsMaximized] = useState(false)
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null)
|
||||
|
||||
const resolvedTitle = title ?? t('profiles.components.menu.editFile')
|
||||
const disableSave = loading || saveDisabled || dirty === false
|
||||
@@ -218,7 +216,6 @@ export const EditorViewer = ({
|
||||
loading={null}
|
||||
saveViewState
|
||||
keepCurrentModel={false}
|
||||
beforeMount={beforeEditorMount}
|
||||
onMount={(editorInstance) => {
|
||||
editorRef.current = editorInstance
|
||||
syncEditorValue()
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
requestIdleCallback,
|
||||
} from 'foxact/request-idle-callback'
|
||||
import yaml from 'js-yaml'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import {
|
||||
startTransition,
|
||||
useCallback,
|
||||
@@ -46,7 +44,12 @@ import {
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseSearchBox, Switch, VirtualList } from '@/components/base'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { GroupItem } from '@/components/profile/group-item'
|
||||
import {
|
||||
getNetworkInterfaces,
|
||||
@@ -56,6 +59,7 @@ import {
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
interface Props {
|
||||
@@ -151,7 +155,7 @@ export const GroupsEditorViewer = (props: Props) => {
|
||||
[t],
|
||||
)
|
||||
const themeMode = useThemeMode()
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null)
|
||||
const [prevData, setPrevData] = useState('')
|
||||
const [currData, setCurrData] = useState('')
|
||||
const [visualization, setVisualization] = useState(true)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import yaml from 'js-yaml'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import {
|
||||
startTransition,
|
||||
useCallback,
|
||||
@@ -38,11 +36,12 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseSearchBox, VirtualList } from '@/components/base'
|
||||
import { BaseSearchBox, MonacoEditor, VirtualList } from '@/components/base'
|
||||
import { ProxyItem } from '@/components/profile/proxy-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import parseUri from '@/utils/uri-parser'
|
||||
|
||||
@@ -58,7 +57,7 @@ export const ProxiesEditorViewer = (props: Props) => {
|
||||
const { profileUid, property, open, onClose, onSave } = props
|
||||
const { t } = useTranslation()
|
||||
const themeMode = useThemeMode()
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null)
|
||||
const [prevData, setPrevData] = useState('')
|
||||
const [currData, setCurrData] = useState('')
|
||||
const [visualization, setVisualization] = useState(true)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext, sortableKeyboardCoordinates } from '@dnd-kit/sortable'
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import {
|
||||
VerticalAlignBottomRounded,
|
||||
VerticalAlignTopRounded,
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import yaml from 'js-yaml'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import {
|
||||
startTransition,
|
||||
useCallback,
|
||||
@@ -40,12 +38,18 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseSearchBox, Switch, VirtualList } from '@/components/base'
|
||||
import {
|
||||
BaseSearchBox,
|
||||
MonacoEditor,
|
||||
Switch,
|
||||
VirtualList,
|
||||
} from '@/components/base'
|
||||
import { RuleItem } from '@/components/profile/rule-item'
|
||||
import { readProfileFile, saveProfileFile } from '@/services/cmds'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { TranslationKey } from '@/types/generated/i18n-keys'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import getSystem from '@/utils/get-system'
|
||||
import { isValidIpCidr } from '@/utils/network'
|
||||
|
||||
@@ -253,7 +257,7 @@ export const RulesEditorViewer = (props: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const themeMode = useThemeMode()
|
||||
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null)
|
||||
|
||||
const [prevData, setPrevData] = useState('')
|
||||
const [currData, setCurrData] = useState('')
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
DragIndicator,
|
||||
Link,
|
||||
LinkOff,
|
||||
WarningRounded,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Alert,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
selectNodeForGroup,
|
||||
} from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { TooltipIcon } from '@/components/base'
|
||||
import { useAppRefreshers, useProxiesData } from '@/providers/app-data-context'
|
||||
import { updateProxyChainConfigInRuntime } from '@/services/cmds'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
@@ -251,6 +253,7 @@ export const ProxyChain = ({
|
||||
}: ProxyChainProps) => {
|
||||
const theme = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const chainWarning = t('proxies.page.chain.warning')
|
||||
const { proxies } = useProxiesData()
|
||||
const { refreshProxy } = useAppRefreshers()
|
||||
const [isConnecting, setIsConnecting] = useState(false)
|
||||
@@ -499,7 +502,15 @@ export const ProxyChain = ({
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{t('proxies.page.chain.header')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
||||
<Typography variant="h6">{t('proxies.page.chain.header')}</Typography>
|
||||
<TooltipIcon
|
||||
title={chainWarning}
|
||||
icon={WarningRounded}
|
||||
color="warning"
|
||||
sx={{ p: 0.25 }}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{proxyChain.length > 0 && (
|
||||
<IconButton
|
||||
|
||||
@@ -9,8 +9,9 @@ import {
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import {
|
||||
type Key,
|
||||
@@ -18,11 +19,13 @@ import {
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation } from 'react-router'
|
||||
import { delayGroup, healthcheckProxyProvider } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { BaseEmpty } from '@/components/base'
|
||||
@@ -65,6 +68,7 @@ interface ProxyChainItem {
|
||||
|
||||
export const ProxyGroups = (props: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { pathname } = useLocation()
|
||||
const { mode, isChainMode = false, chainConfigData } = props
|
||||
|
||||
// Drive 3s polling on the shared TQ cache; data is read via granular context below
|
||||
@@ -158,8 +162,40 @@ export const ProxyGroups = (props: Props) => {
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const scrollPositionRef = useRef<Record<string, number>>({})
|
||||
const scrollTopRef = useRef(0)
|
||||
const showScrollTopRef = useRef(false)
|
||||
const activeStickyIndexRef = useRef<number | null>(null)
|
||||
const restoredScrollKeyRef = useRef<string | null>(null)
|
||||
const [showScrollTop, setShowScrollTop] = useState(false)
|
||||
const scrollPositionKey = useMemo(
|
||||
() =>
|
||||
isChainMode
|
||||
? `${mode}:chain:${activeSelectedGroup ?? 'all'}`
|
||||
: `${mode}:normal`,
|
||||
[activeSelectedGroup, isChainMode, mode],
|
||||
)
|
||||
const stickyGroupIndexes = useMemo(
|
||||
() =>
|
||||
renderList.flatMap((item, index) =>
|
||||
item.type === 0 && !item.group.hidden ? [index] : [],
|
||||
),
|
||||
[renderList],
|
||||
)
|
||||
|
||||
const rangeExtractor = useCallback(
|
||||
(range: Parameters<typeof defaultRangeExtractor>[0]) => {
|
||||
const activeStickyIndex = [...stickyGroupIndexes]
|
||||
.reverse()
|
||||
.find((index) => index <= range.startIndex)
|
||||
activeStickyIndexRef.current = activeStickyIndex ?? null
|
||||
|
||||
const indexes = defaultRangeExtractor(range)
|
||||
return activeStickyIndex == null || indexes.includes(activeStickyIndex)
|
||||
? indexes
|
||||
: [activeStickyIndex, ...indexes]
|
||||
},
|
||||
[stickyGroupIndexes],
|
||||
)
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: renderList.length,
|
||||
@@ -167,58 +203,49 @@ export const ProxyGroups = (props: Props) => {
|
||||
estimateSize: () => 56,
|
||||
overscan: 15,
|
||||
getItemKey: (index) => renderList[index]?.key ?? index,
|
||||
rangeExtractor,
|
||||
})
|
||||
const virtualItems = virtualizer.getVirtualItems()
|
||||
const stickyGroupByIndex = useMemo(
|
||||
() => getStickyGroupByIndex(renderList),
|
||||
[renderList],
|
||||
)
|
||||
const stickyGroupItem = getStickyGroupItem(
|
||||
stickyGroupByIndex,
|
||||
virtualItems,
|
||||
virtualizer.scrollOffset ?? 0,
|
||||
)
|
||||
const activeStickyIndex = activeStickyIndexRef.current
|
||||
|
||||
// 从 localStorage 恢复滚动位置
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (renderList.length === 0) return
|
||||
|
||||
let restoreTimer: ReturnType<typeof setTimeout> | null = null
|
||||
const node = parentRef.current
|
||||
if (!node) return
|
||||
if (
|
||||
restoredScrollKeyRef.current === scrollPositionKey &&
|
||||
node.scrollTop === scrollTopRef.current
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const savedPositions = localStorage.getItem('proxy-scroll-positions')
|
||||
if (savedPositions) {
|
||||
const positions = JSON.parse(savedPositions)
|
||||
scrollPositionRef.current = positions
|
||||
const savedPosition = positions[mode]
|
||||
const savedPosition = positions[scrollPositionKey]
|
||||
|
||||
if (savedPosition !== undefined) {
|
||||
restoreTimer = setTimeout(() => {
|
||||
if (parentRef.current) {
|
||||
parentRef.current.scrollTop = savedPosition
|
||||
const nextShowScrollTop = savedPosition > 100
|
||||
showScrollTopRef.current = nextShowScrollTop
|
||||
setShowScrollTop(nextShowScrollTop)
|
||||
}
|
||||
}, 100)
|
||||
node.scrollTop = savedPosition
|
||||
scrollTopRef.current = savedPosition
|
||||
const nextShowScrollTop = savedPosition > 100
|
||||
showScrollTopRef.current = nextShowScrollTop
|
||||
queueMicrotask(() => setShowScrollTop(nextShowScrollTop))
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error restoring scroll position:', e)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (restoreTimer) {
|
||||
clearTimeout(restoreTimer)
|
||||
}
|
||||
}
|
||||
}, [mode, renderList.length])
|
||||
restoredScrollKeyRef.current = scrollPositionKey
|
||||
}, [pathname, renderList.length, scrollPositionKey])
|
||||
|
||||
// 改为使用节流函数保存滚动位置
|
||||
const saveScrollPosition = useCallback(
|
||||
(scrollTop: number) => {
|
||||
try {
|
||||
scrollPositionRef.current[mode] = scrollTop
|
||||
scrollPositionRef.current[scrollPositionKey] = scrollTop
|
||||
localStorage.setItem(
|
||||
'proxy-scroll-positions',
|
||||
JSON.stringify(scrollPositionRef.current),
|
||||
@@ -227,7 +254,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
console.error('Error saving scroll position:', e)
|
||||
}
|
||||
},
|
||||
[mode],
|
||||
[scrollPositionKey],
|
||||
)
|
||||
|
||||
const saveScrollPositionThrottled = useMemo(
|
||||
@@ -240,6 +267,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
const target = event.target as HTMLElement | null
|
||||
const nextScrollTop = target?.scrollTop ?? 0
|
||||
const nextShowScrollTop = nextScrollTop > 100
|
||||
scrollTopRef.current = nextScrollTop
|
||||
|
||||
if (showScrollTopRef.current !== nextShowScrollTop) {
|
||||
showScrollTopRef.current = nextShowScrollTop
|
||||
@@ -262,9 +290,12 @@ export const ProxyGroups = (props: Props) => {
|
||||
node.addEventListener('scroll', listener, options)
|
||||
|
||||
return () => {
|
||||
if (restoredScrollKeyRef.current === scrollPositionKey) {
|
||||
saveScrollPosition(scrollTopRef.current)
|
||||
}
|
||||
node.removeEventListener('scroll', listener, options)
|
||||
}
|
||||
}, [handleScroll])
|
||||
}, [handleScroll, saveScrollPosition, scrollPositionKey])
|
||||
|
||||
// 滚动到顶部
|
||||
const scrollToTop = useCallback(() => {
|
||||
@@ -272,6 +303,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
scrollTopRef.current = 0
|
||||
saveScrollPosition(0)
|
||||
}, [saveScrollPosition])
|
||||
|
||||
@@ -453,7 +485,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
totalSize={virtualizer.getTotalSize()}
|
||||
virtualItems={virtualItems}
|
||||
renderList={renderList}
|
||||
stickyItem={stickyGroupItem}
|
||||
activeStickyIndex={activeStickyIndex}
|
||||
indent={mode === 'rule' || mode === 'script'}
|
||||
isChainMode={isChainMode}
|
||||
measureElement={virtualizer.measureElement}
|
||||
@@ -564,7 +596,7 @@ interface ProxyVirtualListProps {
|
||||
totalSize: number
|
||||
virtualItems: VirtualListItem[]
|
||||
renderList: IRenderItem[]
|
||||
stickyItem: IRenderItem | null
|
||||
activeStickyIndex: number | null
|
||||
indent: boolean
|
||||
isChainMode?: boolean
|
||||
measureElement: (node: Element | null) => void
|
||||
@@ -728,7 +760,7 @@ function ProxyVirtualList({
|
||||
totalSize,
|
||||
virtualItems,
|
||||
renderList,
|
||||
stickyItem,
|
||||
activeStickyIndex,
|
||||
indent,
|
||||
isChainMode,
|
||||
measureElement,
|
||||
@@ -737,39 +769,12 @@ function ProxyVirtualList({
|
||||
onHeadState,
|
||||
onChangeProxy,
|
||||
}: ProxyVirtualListProps) {
|
||||
const theme = useTheme()
|
||||
const stickyBackground =
|
||||
theme.palette.mode === 'dark' ? '#1e1f27' : 'var(--background-color)'
|
||||
|
||||
return (
|
||||
<div ref={parentRef} style={{ height, overflow: 'auto' }}>
|
||||
{stickyItem && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
height: 0,
|
||||
zIndex: 5,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
pointerEvents: 'auto',
|
||||
'& .MuiListItemButton-root': {
|
||||
boxShadow: 3,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProxyRender
|
||||
item={stickyItem}
|
||||
indent={indent}
|
||||
onLocation={onLocation}
|
||||
onCheckAll={onCheckAll}
|
||||
onHeadState={onHeadState}
|
||||
onChangeProxy={onChangeProxy}
|
||||
isChainMode={isChainMode}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<div style={{ height: totalSize, position: 'relative' }}>
|
||||
{virtualItems.map((virtualItem) => (
|
||||
<div
|
||||
@@ -777,11 +782,24 @@ function ProxyVirtualList({
|
||||
data-index={virtualItem.index}
|
||||
ref={measureElement}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position:
|
||||
virtualItem.index === activeStickyIndex ? 'sticky' : 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: virtualItem.index === activeStickyIndex ? 5 : undefined,
|
||||
display:
|
||||
virtualItem.index === activeStickyIndex
|
||||
? 'flow-root'
|
||||
: undefined,
|
||||
backgroundColor:
|
||||
virtualItem.index === activeStickyIndex
|
||||
? stickyBackground
|
||||
: undefined,
|
||||
width: '100%',
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
transform:
|
||||
virtualItem.index === activeStickyIndex
|
||||
? undefined
|
||||
: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
>
|
||||
<ProxyRender
|
||||
@@ -801,30 +819,6 @@ function ProxyVirtualList({
|
||||
)
|
||||
}
|
||||
|
||||
function getStickyGroupByIndex(renderList: IRenderItem[]) {
|
||||
let stickyGroup: IRenderItem | null = null
|
||||
|
||||
return renderList.map((item) => {
|
||||
if (item?.type === 0 && item.group && !item.group.hidden) {
|
||||
stickyGroup = item
|
||||
}
|
||||
return stickyGroup
|
||||
})
|
||||
}
|
||||
|
||||
function getStickyGroupItem(
|
||||
stickyGroupByIndex: (IRenderItem | null)[],
|
||||
virtualItems: VirtualListItem[],
|
||||
scrollTop: number,
|
||||
) {
|
||||
const firstVisibleItem =
|
||||
virtualItems.find((item) => item.end > scrollTop + 1) ?? virtualItems[0]
|
||||
|
||||
if (!firstVisibleItem) return null
|
||||
|
||||
return stickyGroupByIndex[firstVisibleItem.index] ?? null
|
||||
}
|
||||
|
||||
// 替换简单防抖函数为更优的节流函数
|
||||
function throttle<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
@@ -832,23 +826,30 @@ function throttle<T extends (...args: any[]) => any>(
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
let previous = 0
|
||||
let lastArgs: Parameters<T> | null = null
|
||||
|
||||
const run = (args: Parameters<T>) => {
|
||||
previous = Date.now()
|
||||
timer = null
|
||||
lastArgs = null
|
||||
func(...args)
|
||||
}
|
||||
|
||||
return function (...args: Parameters<T>) {
|
||||
const now = Date.now()
|
||||
const remaining = wait - (now - previous)
|
||||
lastArgs = args
|
||||
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
previous = now
|
||||
func(...args)
|
||||
run(args)
|
||||
} else if (!timer) {
|
||||
timer = setTimeout(() => {
|
||||
previous = Date.now()
|
||||
timer = null
|
||||
func(...args)
|
||||
if (lastArgs) {
|
||||
run(lastArgs)
|
||||
}
|
||||
}, remaining)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import MonacoEditor from '@monaco-editor/react'
|
||||
import { RestartAltRounded } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import yaml from 'js-yaml'
|
||||
import type { editor } from 'monaco-editor'
|
||||
import type { Ref } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
@@ -28,10 +26,11 @@ import {
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { BaseDialog, DialogRef, Switch } from '@/components/base'
|
||||
import { BaseDialog, DialogRef, MonacoEditor, Switch } from '@/components/base'
|
||||
import { useClash } from '@/hooks/use-clash'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useThemeMode } from '@/services/states'
|
||||
import type { MonacoEditorInstance } from '@/types/monaco'
|
||||
import { debugLog } from '@/utils/debug'
|
||||
import getSystem from '@/utils/get-system'
|
||||
|
||||
@@ -190,7 +189,7 @@ export function DnsViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [visualization, setVisualization] = useState(true)
|
||||
const skipYamlSyncRef = useRef(false)
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const editorRef = useRef<MonacoEditorInstance | null>(null)
|
||||
const [values, setValues] = useState<{
|
||||
enable: boolean
|
||||
listen: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, Button, LinearProgress } from '@mui/material'
|
||||
import { alpha, Box, Button, LinearProgress } from '@mui/material'
|
||||
import { relaunch } from '@tauri-apps/plugin-process'
|
||||
import { open as openUrl } from '@tauri-apps/plugin-shell'
|
||||
import type { DownloadEvent } from '@tauri-apps/plugin-updater'
|
||||
@@ -15,6 +15,95 @@ import { portableFlag } from '@/pages/_layout'
|
||||
import { showNotice } from '@/services/notice-service'
|
||||
import { useSetUpdateState, useUpdateState } from '@/services/states'
|
||||
|
||||
type MarkdownNode = {
|
||||
type: string
|
||||
value?: string
|
||||
children?: MarkdownNode[]
|
||||
data?: {
|
||||
hProperties?: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
const GITHUB_ALERTS = {
|
||||
note: { label: 'Note', color: '#0969da' },
|
||||
tip: { label: 'Tip', color: '#1a7f37' },
|
||||
important: { label: 'Important', color: '#8250df' },
|
||||
warning: { label: 'Warning', color: '#9a6700' },
|
||||
caution: { label: 'Caution', color: '#cf222e' },
|
||||
} as const
|
||||
|
||||
type GitHubAlertType = keyof typeof GITHUB_ALERTS
|
||||
|
||||
const GITHUB_ALERT_PATTERN =
|
||||
/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][\t ]*\n?/i
|
||||
const GITHUB_ALERT_CLASS_PATTERN =
|
||||
/markdown-alert-(note|tip|important|warning|caution)/
|
||||
|
||||
const getAlertTypeFromClassName = (
|
||||
className: unknown,
|
||||
): GitHubAlertType | null => {
|
||||
const value = Array.isArray(className)
|
||||
? className.join(' ')
|
||||
: typeof className === 'string'
|
||||
? className
|
||||
: ''
|
||||
const match = value.match(GITHUB_ALERT_CLASS_PATTERN)
|
||||
return match?.[1] as GitHubAlertType | null
|
||||
}
|
||||
|
||||
const findFirstTextNode = (node: MarkdownNode): MarkdownNode | null => {
|
||||
if (node.type === 'text') return node
|
||||
for (const child of node.children ?? []) {
|
||||
const result = findFirstTextNode(child)
|
||||
if (result) return result
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const remarkGitHubAlerts = () => {
|
||||
const visit = (node: MarkdownNode) => {
|
||||
for (const child of node.children ?? []) {
|
||||
visit(child)
|
||||
}
|
||||
|
||||
if (node.type !== 'blockquote') return
|
||||
|
||||
const firstTextNode = findFirstTextNode(node)
|
||||
const match = firstTextNode?.value?.match(GITHUB_ALERT_PATTERN)
|
||||
if (!firstTextNode?.value || !match) return
|
||||
|
||||
const alertType = match[1].toLowerCase() as GitHubAlertType
|
||||
firstTextNode.value = firstTextNode.value
|
||||
.replace(GITHUB_ALERT_PATTERN, '')
|
||||
.replace(/^\n+/, '')
|
||||
|
||||
node.data = {
|
||||
...(node.data ?? {}),
|
||||
hProperties: {
|
||||
...(node.data?.hProperties ?? {}),
|
||||
className: ['markdown-alert', `markdown-alert-${alertType}`],
|
||||
},
|
||||
}
|
||||
|
||||
node.children?.unshift({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
hProperties: {
|
||||
className: 'markdown-alert-title',
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: GITHUB_ALERTS[alertType].label,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
return visit
|
||||
}
|
||||
|
||||
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -112,44 +201,224 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
{t('settings.modals.update.title', {
|
||||
version: updateInfo?.version ?? '',
|
||||
})}
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
openUrl(
|
||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('settings.modals.update.actions.goToRelease')}
|
||||
</Button>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{t('settings.modals.update.title', {
|
||||
version: updateInfo?.version ?? '',
|
||||
})}
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
onClick={() => {
|
||||
openUrl(
|
||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
{t('settings.modals.update.actions.goToRelease')}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{ minWidth: 360, maxWidth: 400, height: '50vh' }}
|
||||
contentSx={{
|
||||
width: { xs: 'calc(100vw - 56px)', sm: 560 },
|
||||
maxWidth: 'calc(100vw - 56px)',
|
||||
height: 'min(64vh, 680px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
pb: 1,
|
||||
}}
|
||||
okBtn={t('settings.modals.update.actions.update')}
|
||||
cancelBtn={t('shared.actions.cancel')}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onUpdate}
|
||||
>
|
||||
<Box sx={{ height: 'calc(100% - 10px)', overflow: 'auto' }}>
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
overflow: 'auto',
|
||||
pr: 1.5,
|
||||
mr: -1,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.65,
|
||||
color: 'text.primary',
|
||||
overflowWrap: 'break-word',
|
||||
'& > :first-child': {
|
||||
mt: 0,
|
||||
},
|
||||
'& > :last-child': {
|
||||
mb: 0,
|
||||
},
|
||||
'& h1': {
|
||||
mt: 0,
|
||||
mb: 1.5,
|
||||
fontSize: 24,
|
||||
lineHeight: 1.25,
|
||||
},
|
||||
'& h2': {
|
||||
mt: 2.25,
|
||||
mb: 1,
|
||||
fontSize: 19,
|
||||
lineHeight: 1.3,
|
||||
},
|
||||
'& h3': {
|
||||
mt: 2,
|
||||
mb: 0.75,
|
||||
fontSize: 16,
|
||||
lineHeight: 1.35,
|
||||
},
|
||||
'& h4, & h5, & h6': {
|
||||
mt: 1.5,
|
||||
mb: 0.75,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
'& p': {
|
||||
my: 1,
|
||||
},
|
||||
'& ul, & ol': {
|
||||
my: 1,
|
||||
pl: 2.75,
|
||||
},
|
||||
'& li': {
|
||||
my: 0.35,
|
||||
pl: 0.25,
|
||||
},
|
||||
'& a': {
|
||||
color: 'primary.main',
|
||||
overflowWrap: 'anywhere',
|
||||
},
|
||||
'& strong': {
|
||||
fontWeight: 700,
|
||||
},
|
||||
'& code': {
|
||||
px: 0.5,
|
||||
py: 0.125,
|
||||
borderRadius: 0.5,
|
||||
bgcolor: 'action.hover',
|
||||
fontSize: '0.92em',
|
||||
},
|
||||
'& pre': {
|
||||
my: 1.5,
|
||||
p: 1.5,
|
||||
overflow: 'auto',
|
||||
borderRadius: 1,
|
||||
bgcolor: 'action.hover',
|
||||
},
|
||||
'& pre code': {
|
||||
p: 0,
|
||||
bgcolor: 'transparent',
|
||||
fontSize: '0.9em',
|
||||
},
|
||||
'& table': {
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
my: 1.5,
|
||||
overflowX: 'auto',
|
||||
borderCollapse: 'collapse',
|
||||
},
|
||||
'& th, & td': {
|
||||
px: 1,
|
||||
py: 0.75,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
'& th': {
|
||||
bgcolor: 'action.hover',
|
||||
fontWeight: 700,
|
||||
},
|
||||
'& hr': {
|
||||
my: 2,
|
||||
border: 0,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
},
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: 1,
|
||||
},
|
||||
'& blockquote:not(.markdown-alert)': {
|
||||
m: '12px 0 18px',
|
||||
pl: 2,
|
||||
color: 'text.secondary',
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'divider',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGitHubAlerts]}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
components={{
|
||||
a: ({ ...props }) => {
|
||||
const { children } = props
|
||||
return (
|
||||
<a {...props} target="_blank">
|
||||
<a {...props} target="_blank" rel="noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
blockquote: ({ className, children }) => {
|
||||
const alertType = getAlertTypeFromClassName(className)
|
||||
|
||||
if (!alertType) {
|
||||
return <blockquote className={className}>{children}</blockquote>
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="blockquote"
|
||||
className={className}
|
||||
sx={(theme) => {
|
||||
const color = GITHUB_ALERTS[alertType].color
|
||||
return {
|
||||
m: '12px 0 18px',
|
||||
px: 2,
|
||||
py: 1,
|
||||
borderLeft: `4px solid ${color}`,
|
||||
borderRadius: 1,
|
||||
bgcolor: alpha(
|
||||
color,
|
||||
theme.palette.mode === 'dark' ? 0.16 : 0.08,
|
||||
),
|
||||
'& p': {
|
||||
my: 0.75,
|
||||
},
|
||||
'& .markdown-alert-title': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.4,
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
}}
|
||||
>
|
||||
{markdownContent}
|
||||
@@ -159,7 +428,7 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||
<LinearProgress
|
||||
variant={total > 0 ? 'determinate' : 'indeterminate'}
|
||||
value={progress}
|
||||
sx={{ marginTop: '5px' }}
|
||||
sx={{ mt: 1 }}
|
||||
/>
|
||||
)}
|
||||
</BaseDialog>
|
||||
|
||||
@@ -53,16 +53,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="initial-loading-overlay"></div>
|
||||
<script>
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
window.__TAURI_INTERNALS__
|
||||
.invoke('plugin:window|show', { label: 'main' })
|
||||
.catch(function () {});
|
||||
window.__TAURI_INTERNALS__
|
||||
.invoke('plugin:window|set_focus', { label: 'main' })
|
||||
.catch(function () {});
|
||||
}
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "체인 프록시 구성",
|
||||
"empty": "구성된 프록시 체인이 없습니다",
|
||||
"warning": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
|
||||
"empty": "체인 프록시는 네트워크 속도를 크게 저하시킬 수 있습니다. 명확한 업무상 필요가 없거나 이 구성을 이해하지 못한다면 비활성화 상태로 두세요.",
|
||||
"instruction": "프록시 체인에 추가하려면 순서대로 노드를 클릭하세요",
|
||||
"minimumNodes": "체인 프록시는 최소 2개의 노드가 필요합니다",
|
||||
"minimumNodesHint": "체인 프록시는 최소 2개의 노드가 필요합니다. 하나 더 추가하세요.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Цепочка прокси",
|
||||
"empty": "Цепочка прокси не настроена",
|
||||
"warning": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
|
||||
"empty": "Цепочка прокси может значительно снизить скорость сети. Если у вас нет явной рабочей необходимости или вы не понимаете эту настройку, оставьте ее отключенной.",
|
||||
"instruction": "Нажимайте на узлы по порядку, чтобы добавить их в цепочку",
|
||||
"minimumNodes": "Для цепочки требуется минимум 2 узла",
|
||||
"minimumNodesHint": "Для цепочки требуется минимум 2 узла. Добавьте ещё один узел.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "Chain Proxy Config",
|
||||
"empty": "No proxy chain configured",
|
||||
"warning": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"empty": "Chain proxy will significantly reduce network speed. If you do not have a clear business need, or do not understand this configuration, keep it disabled.",
|
||||
"instruction": "Click nodes in order to add to proxy chain",
|
||||
"minimumNodes": "Chain proxy requires at least 2 nodes",
|
||||
"minimumNodesHint": "Chain proxy requires at least 2 nodes. Please add one more node.",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "代理链配置",
|
||||
"empty": "暂无代理链配置",
|
||||
"warning": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
|
||||
"empty": "链式代理会显著降低网速。如果您没有明确的业务需求,或不了解此项配置,请保持关闭",
|
||||
"instruction": "顺序点击节点添加到代理链中",
|
||||
"minimumNodes": "链式代理至少需要 2 个节点",
|
||||
"minimumNodesHint": "链式代理至少需要 2 个节点,请再添加一个节点。",
|
||||
|
||||
@@ -43,7 +43,8 @@
|
||||
},
|
||||
"chain": {
|
||||
"header": "鏈式代理設定",
|
||||
"empty": "暫無鏈式代理設定",
|
||||
"warning": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
|
||||
"empty": "鏈式代理會顯著降低網速。如果您沒有明確的業務需求,或不了解此項設定,請保持關閉",
|
||||
"instruction": "依序點擊節點新增到鏈式代理中",
|
||||
"minimumNodes": "鏈式代理至少需要 2 個節點",
|
||||
"minimumNodesHint": "鏈式代理至少需要 2 個節點,請再新增一個節點。",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import './assets/styles/index.scss'
|
||||
import './services/monaco'
|
||||
|
||||
import { ResizeObserver } from '@juggle/resize-observer'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
@@ -208,7 +208,11 @@ const Layout = () => {
|
||||
const customTitlebar = useMemo(
|
||||
() =>
|
||||
!decorated ? (
|
||||
<div className="the_titlebar" data-tauri-drag-region="true">
|
||||
<div className="the_titlebar">
|
||||
<div
|
||||
className="the_titlebar-drag-region"
|
||||
data-tauri-drag-region="true"
|
||||
/>
|
||||
<WindowControls ref={windowControlsRef} />
|
||||
</div>
|
||||
) : null,
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
DeleteForeverRounded,
|
||||
TableChartRounded,
|
||||
TableRowsRounded,
|
||||
ViewColumnRounded,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
Fab,
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
Zoom,
|
||||
} from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
@@ -228,6 +230,18 @@ const ConnectionsPage = () => {
|
||||
>
|
||||
<BaseSearchBox onSearch={handleSearch} />
|
||||
</Box>
|
||||
{isTableLayout && hasTableData && (
|
||||
<Tooltip title={t('connections.components.columnManager.title')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={t('connections.components.columnManager.title')}
|
||||
onClick={() => setIsColumnManagerOpen(true)}
|
||||
sx={{ flex: '0 0 auto' }}
|
||||
>
|
||||
<ViewColumnRounded fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!hasTableData ? (
|
||||
@@ -239,7 +253,6 @@ const ConnectionsPage = () => {
|
||||
detailRef.current?.open(detail, connectionsType === 'closed')
|
||||
}
|
||||
columnManagerOpen={isTableLayout && isColumnManagerOpen}
|
||||
onOpenColumnManager={() => setIsColumnManagerOpen(true)}
|
||||
onCloseColumnManager={() => setIsColumnManagerOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
|
||||
+20
-5
@@ -1,11 +1,11 @@
|
||||
import { LanOutlined, LanRounded } from '@mui/icons-material'
|
||||
import { LanOutlined, LanRounded, WarningRounded } from '@mui/icons-material'
|
||||
import { Box, Button, ButtonGroup } from '@mui/material'
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useReducer, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { closeAllConnections } from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { BasePage } from '@/components/base'
|
||||
import { BasePage, TooltipIcon } from '@/components/base'
|
||||
import { ProviderButton } from '@/components/proxy/provider-button'
|
||||
import { ProxyGroups } from '@/components/proxy/proxy-groups'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
@@ -54,6 +54,7 @@ const ProxyPage = () => {
|
||||
|
||||
const normalizedMode = clashConfig?.mode?.toLowerCase()
|
||||
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
|
||||
const chainWarning = t('proxies.page.chain.warning')
|
||||
|
||||
const onChangeMode = useLockFn(async (mode: Mode) => {
|
||||
// 断开连接
|
||||
@@ -134,9 +135,23 @@ const ProxyPage = () => {
|
||||
full
|
||||
contentStyle={{ height: '101.5%' }}
|
||||
title={
|
||||
isChainMode
|
||||
? t('proxies.page.title.chainMode')
|
||||
: t('proxies.page.title.default')
|
||||
isChainMode ? (
|
||||
<Box
|
||||
component="span"
|
||||
data-tauri-drag-region="true"
|
||||
sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}
|
||||
>
|
||||
{t('proxies.page.title.chainMode')}
|
||||
<TooltipIcon
|
||||
title={chainWarning}
|
||||
icon={WarningRounded}
|
||||
color="warning"
|
||||
sx={{ p: 0.25 }}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
t('proxies.page.title.default')
|
||||
)
|
||||
}
|
||||
header={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
|
||||
+79
-56
@@ -1,70 +1,93 @@
|
||||
import { loader } from '@monaco-editor/react'
|
||||
import metaSchema from 'meta-json-schema/schemas/meta-json-schema.json'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
|
||||
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
|
||||
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
|
||||
import { configureMonacoYaml, JSONSchema } from 'monaco-yaml'
|
||||
import pac from 'types-pac/pac.d.ts?raw'
|
||||
type MonacoModule = typeof import('monaco-editor')
|
||||
type MonacoEditorPackage = typeof import('@monaco-editor/react')
|
||||
|
||||
import yamlWorker from '@/utils/yaml.worker?worker'
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
switch (label) {
|
||||
case 'css':
|
||||
case 'less':
|
||||
case 'scss':
|
||||
return new cssWorker()
|
||||
case 'typescript':
|
||||
case 'javascript':
|
||||
return new tsWorker()
|
||||
case 'yaml':
|
||||
return new yamlWorker()
|
||||
default:
|
||||
return new editorWorker()
|
||||
}
|
||||
},
|
||||
type MonacoEditorBundle = {
|
||||
Editor: MonacoEditorPackage['default']
|
||||
beforeEditorMount: () => void
|
||||
}
|
||||
|
||||
loader.config({ monaco })
|
||||
let loadPromise: Promise<MonacoEditorBundle> | undefined
|
||||
let monacoConfigured = false
|
||||
|
||||
// Work around https://github.com/remcohaszing/monaco-yaml/issues/272.
|
||||
const patchCreateWebWorker = () => {
|
||||
const oldCreateWebWorker = monaco.editor.createWebWorker
|
||||
const patchCreateWebWorker = (monaco: MonacoModule) => {
|
||||
const createWebWorker = monaco.editor.createWebWorker
|
||||
|
||||
monaco.editor.createWebWorker = (
|
||||
options: monaco.IWebWorkerOptions | monaco.editor.IInternalWebWorkerOptions,
|
||||
) => {
|
||||
if ('worker' in options) {
|
||||
return oldCreateWebWorker(options)
|
||||
}
|
||||
type CreateWebWorker = typeof createWebWorker
|
||||
type WorkerOptions = Parameters<CreateWebWorker>[0] & { worker?: unknown }
|
||||
|
||||
return monaco.createWebWorker(options)
|
||||
}
|
||||
monaco.editor.createWebWorker = ((options: WorkerOptions) =>
|
||||
'worker' in options
|
||||
? createWebWorker(options)
|
||||
: monaco.createWebWorker(options)) as CreateWebWorker
|
||||
}
|
||||
|
||||
let mounted = false
|
||||
export const loadMonacoEditor = () => {
|
||||
loadPromise ??= Promise.all([
|
||||
import('@monaco-editor/react'),
|
||||
import('monaco-editor'),
|
||||
import('monaco-editor/esm/vs/editor/editor.worker?worker'),
|
||||
import('monaco-editor/esm/vs/language/css/css.worker?worker'),
|
||||
import('monaco-editor/esm/vs/language/typescript/ts.worker?worker'),
|
||||
import('monaco-yaml'),
|
||||
import('meta-json-schema/schemas/meta-json-schema.json'),
|
||||
import('types-pac/pac.d.ts?raw'),
|
||||
import('@/utils/yaml.worker?worker'),
|
||||
]).then(
|
||||
([
|
||||
editorModule,
|
||||
monaco,
|
||||
{ default: EditorWorker },
|
||||
{ default: CssWorker },
|
||||
{ default: TsWorker },
|
||||
{ configureMonacoYaml },
|
||||
{ default: metaSchema },
|
||||
{ default: pac },
|
||||
{ default: YamlWorker },
|
||||
]) => {
|
||||
const workers = {
|
||||
css: CssWorker,
|
||||
less: CssWorker,
|
||||
scss: CssWorker,
|
||||
typescript: TsWorker,
|
||||
javascript: TsWorker,
|
||||
yaml: YamlWorker,
|
||||
}
|
||||
|
||||
export const beforeEditorMount = () => {
|
||||
if (mounted) return
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
return new (workers[label as keyof typeof workers] ?? EditorWorker)()
|
||||
},
|
||||
}
|
||||
|
||||
patchCreateWebWorker()
|
||||
editorModule.loader.config({ monaco })
|
||||
|
||||
monaco.typescript.javascriptDefaults.addExtraLib(pac, 'pac.d.ts')
|
||||
return {
|
||||
Editor: editorModule.default,
|
||||
beforeEditorMount: () => {
|
||||
if (monacoConfigured) return
|
||||
|
||||
configureMonacoYaml(monaco, {
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
completion: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://example.com/meta-json-schema.json',
|
||||
fileMatch: ['**/*.yaml', '**/*.yml'],
|
||||
schema: metaSchema as unknown as JSONSchema, // JSON import is inferred as a literal type
|
||||
},
|
||||
],
|
||||
})
|
||||
patchCreateWebWorker(monaco)
|
||||
monaco.typescript.javascriptDefaults.addExtraLib(pac, 'pac.d.ts')
|
||||
|
||||
mounted = true
|
||||
configureMonacoYaml(monaco, {
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
completion: true,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://example.com/meta-json-schema.json',
|
||||
fileMatch: ['**/*.yaml', '**/*.yml'],
|
||||
schema:
|
||||
metaSchema as unknown as import('monaco-yaml').JSONSchema,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
monacoConfigured = true
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
@@ -238,6 +238,7 @@ export const translationKeys = [
|
||||
'proxies.page.tooltips.filter',
|
||||
'proxies.page.placeholders.delayCheckUrl',
|
||||
'proxies.page.chain.header',
|
||||
'proxies.page.chain.warning',
|
||||
'proxies.page.chain.empty',
|
||||
'proxies.page.chain.instruction',
|
||||
'proxies.page.chain.minimumNodes',
|
||||
|
||||
@@ -439,6 +439,7 @@ export interface TranslationResources {
|
||||
instruction: string
|
||||
minimumNodes: string
|
||||
minimumNodesHint: string
|
||||
warning: string
|
||||
}
|
||||
labels: {
|
||||
delayCheckReset: string
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export type MonacoEditorInstance =
|
||||
import('monaco-editor').editor.IStandaloneCodeEditor
|
||||
|
||||
export type MonacoMarker = import('monaco-editor').editor.IMarker
|
||||
@@ -31,6 +31,7 @@ export const parseHotkey = (keyEvent: KeyboardEvent) => {
|
||||
return 'ALT'
|
||||
}
|
||||
case 'META':
|
||||
case 'OS':
|
||||
return 'CMD'
|
||||
case ' ':
|
||||
return 'SPACE'
|
||||
|
||||
Reference in New Issue
Block a user