Compare commits
5 Commits
dev
...
updater-au
40
.github/workflows/autobuild.yml
vendored
40
.github/workflows/autobuild.yml
vendored
@@ -493,6 +493,43 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
publish-updater-manifests:
|
||||||
|
name: Publish Updater Manifests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
[
|
||||||
|
update_tag,
|
||||||
|
autobuild-x86-windows-macos-linux,
|
||||||
|
autobuild-arm-linux,
|
||||||
|
autobuild-x86-arm-windows_webview2,
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm i
|
||||||
|
|
||||||
|
- name: Publish updater manifests
|
||||||
|
run: pnpm updater
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish WebView2 updater manifests
|
||||||
|
run: pnpm updater-fixed-webview2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
notify-telegram:
|
notify-telegram:
|
||||||
name: Notify Telegram
|
name: Notify Telegram
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -502,6 +539,7 @@ jobs:
|
|||||||
autobuild-x86-windows-macos-linux,
|
autobuild-x86-windows-macos-linux,
|
||||||
autobuild-arm-linux,
|
autobuild-arm-linux,
|
||||||
autobuild-x86-arm-windows_webview2,
|
autobuild-x86-arm-windows_webview2,
|
||||||
|
publish-updater-manifests,
|
||||||
]
|
]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -578,7 +616,7 @@ jobs:
|
|||||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_amd64_linux.deb) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_arm64.deb) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}_armhf.deb)
|
||||||
|
|
||||||
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
#### RPM包(Redhat系) 使用 dnf ./路径 安装
|
||||||
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge_${{ env.VERSION }}-1.armhfp.rpm)
|
- [64位](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.x86_64_linux.rpm) | [ARM64](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.aarch64.rpm) | [ARMv7](${{ env.DOWNLOAD_URL }}/Clash.Verge-${{ env.VERSION }}-1.armhfp.rpm)
|
||||||
|
|
||||||
### FAQ
|
### FAQ
|
||||||
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
- [常见问题](https://clash-verge-rev.github.io/faq/windows.html)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
- 修复 macOS 从 Dock 栏退出轻量模式状态不同步
|
||||||
- 修复 Linux 系统主题切换不生效
|
- 修复 Linux 系统主题切换不生效
|
||||||
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
- 修复 `允许自动更新` 字段使手动订阅刷新失效
|
||||||
|
- 修复轻量模式托盘状态不同步
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
@@ -54,6 +55,7 @@
|
|||||||
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
- 托盘 `更多` 中新增 `关闭所有连接` 按钮
|
||||||
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
- 新增左侧菜单栏的排序功能(右键点击左侧菜单栏)
|
||||||
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
- 托盘 `打开目录` 中新增 `应用日志` 和 `内核日志`
|
||||||
|
- 支持更新通道切换 (Stable / Autobuild)
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"@tauri-apps/cli": "2.9.2",
|
"@tauri-apps/cli": "2.9.2",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.9.2",
|
"@types/node": "^24.10.0",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.2",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@vitejs/plugin-legacy": "^7.2.1",
|
"@vitejs/plugin-legacy": "^7.2.1",
|
||||||
|
|||||||
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -154,8 +154,8 @@ importers:
|
|||||||
specifier: ^4.17.12
|
specifier: ^4.17.12
|
||||||
version: 4.17.12
|
version: 4.17.12
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.9.2
|
specifier: ^24.10.0
|
||||||
version: 24.9.2
|
version: 24.10.0
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.2
|
specifier: 19.2.2
|
||||||
version: 19.2.2
|
version: 19.2.2
|
||||||
@@ -164,10 +164,10 @@ importers:
|
|||||||
version: 19.2.2(@types/react@19.2.2)
|
version: 19.2.2(@types/react@19.2.2)
|
||||||
'@vitejs/plugin-legacy':
|
'@vitejs/plugin-legacy':
|
||||||
specifier: ^7.2.1
|
specifier: ^7.2.1
|
||||||
version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
version: 7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@vitejs/plugin-react-swc':
|
'@vitejs/plugin-react-swc':
|
||||||
specifier: ^4.2.0
|
specifier: ^4.2.0
|
||||||
version: 4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
version: 4.2.0(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
adm-zip:
|
adm-zip:
|
||||||
specifier: ^0.5.16
|
specifier: ^0.5.16
|
||||||
version: 0.5.16
|
version: 0.5.16
|
||||||
@@ -248,16 +248,16 @@ importers:
|
|||||||
version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
version: 8.46.2(eslint@9.39.0(jiti@2.6.1))(typescript@5.9.3)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.12
|
specifier: ^7.1.12
|
||||||
version: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
version: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
vite-plugin-monaco-editor-esm:
|
vite-plugin-monaco-editor-esm:
|
||||||
specifier: ^2.0.2
|
specifier: ^2.0.2
|
||||||
version: 2.0.2(monaco-editor@0.54.0)
|
version: 2.0.2(monaco-editor@0.54.0)
|
||||||
vite-plugin-svgr:
|
vite-plugin-svgr:
|
||||||
specifier: ^4.5.0
|
specifier: ^4.5.0
|
||||||
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.6
|
specifier: ^4.0.6
|
||||||
version: 4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
version: 4.0.6(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1821,8 +1821,8 @@ packages:
|
|||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
'@types/node@24.9.2':
|
'@types/node@24.10.0':
|
||||||
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
|
resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==}
|
||||||
|
|
||||||
'@types/parse-json@4.0.2':
|
'@types/parse-json@4.0.2':
|
||||||
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
|
||||||
@@ -5982,7 +5982,7 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node@24.9.2':
|
'@types/node@24.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
@@ -6160,7 +6160,7 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
'@vitejs/plugin-legacy@7.2.1(terser@5.44.0)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4)
|
'@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.4)
|
||||||
@@ -6175,15 +6175,15 @@ snapshots:
|
|||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
systemjs: 6.15.1
|
systemjs: 6.15.1
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
'@vitejs/plugin-react-swc@4.2.0(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.43
|
'@rolldown/pluginutils': 1.0.0-beta.43
|
||||||
'@swc/core': 1.14.0
|
'@swc/core': 1.14.0
|
||||||
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
|
|
||||||
@@ -6196,13 +6196,13 @@ snapshots:
|
|||||||
chai: 6.2.0
|
chai: 6.2.0
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
|
|
||||||
'@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
'@vitest/mocker@4.0.6(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.0.6
|
'@vitest/spy': 4.0.6
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.19
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.0.6':
|
'@vitest/pretty-format@4.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8844,18 +8844,18 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
monaco-editor: 0.54.0
|
monaco-editor: 0.54.0
|
||||||
|
|
||||||
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)):
|
vite-plugin-svgr@4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
|
'@rollup/pluginutils': 5.2.0(rollup@4.46.2)
|
||||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
|
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
|
||||||
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
fdir: 6.5.0(picomatch@4.0.3)
|
fdir: 6.5.0(picomatch@4.0.3)
|
||||||
@@ -8864,17 +8864,17 @@ snapshots:
|
|||||||
rollup: 4.46.2
|
rollup: 4.46.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 24.9.2
|
'@types/node': 24.10.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
sass: 1.93.3
|
sass: 1.93.3
|
||||||
terser: 5.44.0
|
terser: 5.44.0
|
||||||
yaml: 2.8.1
|
yaml: 2.8.1
|
||||||
|
|
||||||
vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
vitest@4.0.6(@types/debug@4.1.12)(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.6
|
'@vitest/expect': 4.0.6
|
||||||
'@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
'@vitest/mocker': 4.0.6(vite@7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1))
|
||||||
'@vitest/pretty-format': 4.0.6
|
'@vitest/pretty-format': 4.0.6
|
||||||
'@vitest/runner': 4.0.6
|
'@vitest/runner': 4.0.6
|
||||||
'@vitest/snapshot': 4.0.6
|
'@vitest/snapshot': 4.0.6
|
||||||
@@ -8891,11 +8891,11 @@ snapshots:
|
|||||||
tinyexec: 0.3.2
|
tinyexec: 0.3.2
|
||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
vite: 7.1.12(@types/node@24.10.0)(jiti@2.6.1)(sass@1.93.3)(terser@5.44.0)(yaml@2.8.1)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 24.9.2
|
'@types/node': 24.10.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
* pnpm release-version <version>
|
* pnpm release-version <version>
|
||||||
*
|
*
|
||||||
* <version> can be:
|
* <version> can be:
|
||||||
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3+build)
|
* - A full semver version (e.g., 1.2.3, v1.2.3, 1.2.3-beta, v1.2.3-rc.1)
|
||||||
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
* - A tag: "alpha", "beta", "rc", "autobuild", "autobuild-latest", or "deploytest"
|
||||||
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
* - "alpha", "beta", "rc": Appends the tag to the current base version (e.g., 1.2.3-beta)
|
||||||
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3+autobuild.2406101530)
|
* - "autobuild": Appends a timestamped autobuild tag (e.g., 1.2.3-autobuild.0610.cc39b2.r2)
|
||||||
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3+autobuild.0614.a1b2c3d)
|
* - "autobuild-latest": Appends an autobuild tag with latest Tauri commit (e.g., 1.2.3-autobuild.0610.a1b2c3d.r2)
|
||||||
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3+deploytest.2406101530)
|
* - "deploytest": Appends a timestamped deploytest tag (e.g., 1.2.3-deploytest.0610.cc39b2.r2)
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* pnpm release-version 1.2.3
|
* pnpm release-version 1.2.3
|
||||||
@@ -30,10 +30,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import { program } from "commander";
|
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
|
import process from "node:process";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
|
import { program } from "commander";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前 git 短 commit hash
|
* 获取当前 git 短 commit hash
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
@@ -73,41 +75,91 @@ function getLatestTauriCommit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成短时间戳(格式:MMDD)或带 commit(格式:MMDD.cc39b27)
|
* 获取 Asia/Shanghai 时区的日期片段
|
||||||
* 使用 Asia/Shanghai 时区
|
|
||||||
* @param {boolean} withCommit 是否带 commit
|
|
||||||
* @param {boolean} useTauriCommit 是否使用 Tauri 相关的 commit(仅当 withCommit 为 true 时有效)
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function generateShortTimestamp(withCommit = false, useTauriCommit = false) {
|
function getLocalDatePart() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
const dateFormatter = new Intl.DateTimeFormat("en-CA", {
|
||||||
timeZone: "Asia/Shanghai",
|
timeZone: "Asia/Shanghai",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
});
|
});
|
||||||
|
const dateParts = Object.fromEntries(
|
||||||
|
dateFormatter.formatToParts(now).map((part) => [part.type, part.value]),
|
||||||
|
);
|
||||||
|
|
||||||
const parts = formatter.formatToParts(now);
|
const month = dateParts.month ?? "00";
|
||||||
const month = parts.find((part) => part.type === "month").value;
|
const day = dateParts.day ?? "00";
|
||||||
const day = parts.find((part) => part.type === "day").value;
|
|
||||||
|
|
||||||
if (withCommit) {
|
|
||||||
const gitShort = useTauriCommit
|
|
||||||
? getLatestTauriCommit()
|
|
||||||
: getGitShortCommit();
|
|
||||||
return `${month}${day}.${gitShort}`;
|
|
||||||
}
|
|
||||||
return `${month}${day}`;
|
return `${month}${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 GitHub Actions 运行编号(若存在)
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function getRunIdentifier() {
|
||||||
|
const attempt = process.env.GITHUB_RUN_ATTEMPT;
|
||||||
|
if (attempt && /^[0-9]+$/.test(attempt)) {
|
||||||
|
const attemptNumber = Number.parseInt(attempt, 10);
|
||||||
|
if (!Number.isNaN(attemptNumber)) {
|
||||||
|
return `r${attemptNumber.toString(36)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runNumber = process.env.GITHUB_RUN_NUMBER;
|
||||||
|
if (runNumber && /^[0-9]+$/.test(runNumber)) {
|
||||||
|
const runNum = Number.parseInt(runNumber, 10);
|
||||||
|
if (!Number.isNaN(runNum)) {
|
||||||
|
return `r${runNum.toString(36)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成用于自动构建类渠道的版本后缀
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {boolean} [options.includeCommit=false]
|
||||||
|
* @param {"current"|"tauri"} [options.commitSource="current"]
|
||||||
|
* @param {boolean} [options.includeRun=true]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function generateChannelSuffix({
|
||||||
|
includeCommit = false,
|
||||||
|
commitSource = "current",
|
||||||
|
includeRun = true,
|
||||||
|
} = {}) {
|
||||||
|
const segments = [];
|
||||||
|
const date = getLocalDatePart();
|
||||||
|
segments.push(date);
|
||||||
|
|
||||||
|
if (includeCommit) {
|
||||||
|
const commit =
|
||||||
|
commitSource === "tauri" ? getLatestTauriCommit() : getGitShortCommit();
|
||||||
|
segments.push(commit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeRun) {
|
||||||
|
const run = getRunIdentifier();
|
||||||
|
if (run) {
|
||||||
|
segments.push(run);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.join(".");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证版本号格式
|
* 验证版本号格式
|
||||||
* @param {string} version
|
* @param {string} version
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function isValidVersion(version) {
|
function isValidVersion(version) {
|
||||||
return /^v?\d+\.\d+\.\d+(-(alpha|beta|rc)(\.\d+)?)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?$/i.test(
|
return /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
||||||
version,
|
version,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,13 +174,14 @@ function normalizeVersion(version) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取基础版本号(去掉所有 -tag 和 +build 部分)
|
* 提取基础版本号(去掉所有 pre-release 和 build metadata)
|
||||||
* @param {string} version
|
* @param {string} version
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
function getBaseVersion(version) {
|
function getBaseVersion(version) {
|
||||||
let base = version.replace(/-(alpha|beta|rc)(\.\d+)?/i, "");
|
const cleaned = version.startsWith("v") ? version.slice(1) : version;
|
||||||
base = base.replace(/\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*/g, "");
|
const withoutBuild = cleaned.split("+")[0];
|
||||||
|
const [base] = withoutBuild.split("-");
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,17 +326,23 @@ async function main(versionArg) {
|
|||||||
const baseVersion = getBaseVersion(currentVersion);
|
const baseVersion = getBaseVersion(currentVersion);
|
||||||
|
|
||||||
if (versionArg.toLowerCase() === "autobuild") {
|
if (versionArg.toLowerCase() === "autobuild") {
|
||||||
// 格式: 2.3.0+autobuild.1004.cc39b27
|
// 格式: 2.3.0-autobuild.0610.cc39b2.r2
|
||||||
// 使用 Tauri 相关的最新 commit hash
|
newVersion = `${baseVersion}-autobuild.${generateChannelSuffix({
|
||||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp(true, true)}`;
|
includeCommit: true,
|
||||||
|
commitSource: "tauri",
|
||||||
|
})}`;
|
||||||
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
} else if (versionArg.toLowerCase() === "autobuild-latest") {
|
||||||
// 格式: 2.3.0+autobuild.1004.a1b2c3d (使用最新 Tauri 提交)
|
// 格式: 2.3.0-autobuild.0610.a1b2c3d.r2 (使用最新 Tauri 提交)
|
||||||
const latestTauriCommit = getLatestTauriCommit();
|
newVersion = `${baseVersion}-autobuild.${generateChannelSuffix({
|
||||||
newVersion = `${baseVersion}+autobuild.${generateShortTimestamp()}.${latestTauriCommit}`;
|
includeCommit: true,
|
||||||
|
commitSource: "tauri",
|
||||||
|
})}`;
|
||||||
} else if (versionArg.toLowerCase() === "deploytest") {
|
} else if (versionArg.toLowerCase() === "deploytest") {
|
||||||
// 格式: 2.3.0+deploytest.1004.cc39b27
|
// 格式: 2.3.0-deploytest.0610.cc39b2.r2
|
||||||
// 使用 Tauri 相关的最新 commit hash
|
newVersion = `${baseVersion}-deploytest.${generateChannelSuffix({
|
||||||
newVersion = `${baseVersion}+deploytest.${generateShortTimestamp(true, true)}`;
|
includeCommit: true,
|
||||||
|
commitSource: "tauri",
|
||||||
|
})}`;
|
||||||
} else {
|
} else {
|
||||||
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
newVersion = `${baseVersion}-${versionArg.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import fetch from "node-fetch";
|
import process from "node:process";
|
||||||
|
|
||||||
import { getOctokit, context } from "@actions/github";
|
import { getOctokit, context } from "@actions/github";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
import { resolveUpdateLog, resolveUpdateLogDefault } from "./updatelog.mjs";
|
||||||
|
|
||||||
// Add stable update JSON filenames
|
// Add stable update JSON filenames
|
||||||
@@ -10,6 +13,11 @@ const UPDATE_JSON_PROXY = "update-proxy.json";
|
|||||||
const ALPHA_TAG_NAME = "updater-alpha";
|
const ALPHA_TAG_NAME = "updater-alpha";
|
||||||
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
const ALPHA_UPDATE_JSON_FILE = "update.json";
|
||||||
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
const ALPHA_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
|
// Add autobuild update JSON filenames
|
||||||
|
const AUTOBUILD_SOURCE_TAG_NAME = "autobuild";
|
||||||
|
const AUTOBUILD_TAG_NAME = "updater-autobuild";
|
||||||
|
const AUTOBUILD_UPDATE_JSON_FILE = "update.json";
|
||||||
|
const AUTOBUILD_UPDATE_JSON_PROXY = "update-proxy.json";
|
||||||
|
|
||||||
/// generate update.json
|
/// generate update.json
|
||||||
/// upload to update tag's release asset
|
/// upload to update tag's release asset
|
||||||
@@ -48,12 +56,12 @@ async function resolveUpdater() {
|
|||||||
|
|
||||||
// More flexible tag detection with regex patterns
|
// More flexible tag detection with regex patterns
|
||||||
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
const stableTagRegex = /^v\d+\.\d+\.\d+$/; // Matches vX.Y.Z format
|
||||||
// const preReleaseRegex = /^v\d+\.\d+\.\d+-(alpha|beta|rc|pre)/i; // Matches vX.Y.Z-alpha/beta/rc format
|
|
||||||
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
const preReleaseRegex = /^(alpha|beta|rc|pre)$/i; // Matches exact alpha/beta/rc/pre tags
|
||||||
|
|
||||||
// Get the latest stable tag and pre-release tag
|
// Get tags for known channels
|
||||||
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
const stableTag = tags.find((t) => stableTagRegex.test(t.name));
|
||||||
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
const preReleaseTag = tags.find((t) => preReleaseRegex.test(t.name));
|
||||||
|
const autobuildTag = tags.find((t) => t.name === AUTOBUILD_SOURCE_TAG_NAME);
|
||||||
|
|
||||||
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
console.log("All tags:", tags.map((t) => t.name).join(", "));
|
||||||
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
console.log("Stable tag:", stableTag ? stableTag.name : "None found");
|
||||||
@@ -61,32 +69,79 @@ async function resolveUpdater() {
|
|||||||
"Pre-release tag:",
|
"Pre-release tag:",
|
||||||
preReleaseTag ? preReleaseTag.name : "None found",
|
preReleaseTag ? preReleaseTag.name : "None found",
|
||||||
);
|
);
|
||||||
|
console.log(
|
||||||
|
"Autobuild tag:",
|
||||||
|
autobuildTag ? autobuildTag.name : "None found",
|
||||||
|
);
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
// Process stable release
|
const channels = [
|
||||||
if (stableTag) {
|
{
|
||||||
await processRelease(github, options, stableTag, false);
|
name: "stable",
|
||||||
}
|
tagName: stableTag?.name,
|
||||||
|
updateReleaseTag: UPDATE_TAG_NAME,
|
||||||
|
jsonFile: UPDATE_JSON_FILE,
|
||||||
|
proxyFile: UPDATE_JSON_PROXY,
|
||||||
|
prerelease: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "alpha",
|
||||||
|
tagName: preReleaseTag?.name,
|
||||||
|
updateReleaseTag: ALPHA_TAG_NAME,
|
||||||
|
jsonFile: ALPHA_UPDATE_JSON_FILE,
|
||||||
|
proxyFile: ALPHA_UPDATE_JSON_PROXY,
|
||||||
|
prerelease: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "autobuild",
|
||||||
|
tagName: autobuildTag?.name ?? AUTOBUILD_SOURCE_TAG_NAME,
|
||||||
|
updateReleaseTag: AUTOBUILD_TAG_NAME,
|
||||||
|
jsonFile: AUTOBUILD_UPDATE_JSON_FILE,
|
||||||
|
proxyFile: AUTOBUILD_UPDATE_JSON_PROXY,
|
||||||
|
prerelease: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Process pre-release if found
|
for (const channel of channels) {
|
||||||
if (preReleaseTag) {
|
if (!channel.tagName) {
|
||||||
await processRelease(github, options, preReleaseTag, true);
|
console.log(`[${channel.name}] tag not found, skipping...`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
await processRelease(github, options, channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process a release (stable or alpha) and generate update files
|
// Process a release and generate update files for the specified channel
|
||||||
async function processRelease(github, options, tag, isAlpha) {
|
async function processRelease(github, options, channelConfig) {
|
||||||
if (!tag) return;
|
if (!channelConfig) return;
|
||||||
|
|
||||||
|
const {
|
||||||
|
tagName,
|
||||||
|
name: channelName,
|
||||||
|
updateReleaseTag,
|
||||||
|
jsonFile,
|
||||||
|
proxyFile,
|
||||||
|
prerelease,
|
||||||
|
} = channelConfig;
|
||||||
|
|
||||||
|
const channelLabel =
|
||||||
|
channelName.charAt(0).toUpperCase() + channelName.slice(1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: tag.name,
|
tag: tagName,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const releaseTagName = release.tag_name ?? tagName;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[${channelName}] Preparing update metadata from release "${releaseTagName}"`,
|
||||||
|
);
|
||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: tag.name,
|
name: releaseTagName,
|
||||||
notes: await resolveUpdateLog(tag.name).catch(() =>
|
notes: await resolveUpdateLog(releaseTagName).catch(() =>
|
||||||
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
resolveUpdateLogDefault().catch(() => "No changelog available"),
|
||||||
),
|
),
|
||||||
pub_date: new Date().toISOString(),
|
pub_date: new Date().toISOString(),
|
||||||
@@ -186,13 +241,15 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await Promise.allSettled(promises);
|
await Promise.allSettled(promises);
|
||||||
console.log(updateData);
|
console.log(`[${channelName}] Update data snapshot:`, updateData);
|
||||||
|
|
||||||
// maybe should test the signature as well
|
// maybe should test the signature as well
|
||||||
// delete the null field
|
// delete the null field
|
||||||
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
Object.entries(updateData.platforms).forEach(([key, value]) => {
|
||||||
if (!value.url) {
|
if (!value.url) {
|
||||||
console.log(`[Error]: failed to parse release for "${key}"`);
|
console.log(
|
||||||
|
`[${channelName}] [Error]: failed to parse release for "${key}"`,
|
||||||
|
);
|
||||||
delete updateData.platforms[key];
|
delete updateData.platforms[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -205,15 +262,14 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
updateDataNew.platforms[key].url =
|
updateDataNew.platforms[key].url =
|
||||||
"https://download.clashverge.dev/" + value.url;
|
"https://download.clashverge.dev/" + value.url;
|
||||||
} else {
|
} else {
|
||||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
console.log(
|
||||||
|
`[${channelName}] [Error]: updateDataNew.platforms.${key} is null`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get the appropriate updater release based on isAlpha flag
|
|
||||||
const releaseTag = isAlpha ? ALPHA_TAG_NAME : UPDATE_TAG_NAME;
|
|
||||||
console.log(
|
console.log(
|
||||||
`Processing ${isAlpha ? "alpha" : "stable"} release:`,
|
`[${channelName}] Processing update release target "${updateReleaseTag}"`,
|
||||||
releaseTag,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -223,30 +279,28 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
// Try to get the existing release
|
// Try to get the existing release
|
||||||
const response = await github.rest.repos.getReleaseByTag({
|
const response = await github.rest.repos.getReleaseByTag({
|
||||||
...options,
|
...options,
|
||||||
tag: releaseTag,
|
tag: updateReleaseTag,
|
||||||
});
|
});
|
||||||
updateRelease = response.data;
|
updateRelease = response.data;
|
||||||
console.log(
|
console.log(
|
||||||
`Found existing ${releaseTag} release with ID: ${updateRelease.id}`,
|
`[${channelName}] Found existing ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If release doesn't exist, create it
|
// If release doesn't exist, create it
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
console.log(
|
console.log(
|
||||||
`Release with tag ${releaseTag} not found, creating new release...`,
|
`[${channelName}] Release with tag ${updateReleaseTag} not found, creating new release...`,
|
||||||
);
|
);
|
||||||
const createResponse = await github.rest.repos.createRelease({
|
const createResponse = await github.rest.repos.createRelease({
|
||||||
...options,
|
...options,
|
||||||
tag_name: releaseTag,
|
tag_name: updateReleaseTag,
|
||||||
name: isAlpha
|
name: `Auto-update ${channelLabel} Channel`,
|
||||||
? "Auto-update Alpha Channel"
|
body: `This release contains the update information for the ${channelName} channel.`,
|
||||||
: "Auto-update Stable Channel",
|
prerelease,
|
||||||
body: `This release contains the update information for ${isAlpha ? "alpha" : "stable"} channel.`,
|
|
||||||
prerelease: isAlpha,
|
|
||||||
});
|
});
|
||||||
updateRelease = createResponse.data;
|
updateRelease = createResponse.data;
|
||||||
console.log(
|
console.log(
|
||||||
`Created new ${releaseTag} release with ID: ${updateRelease.id}`,
|
`[${channelName}] Created new ${updateReleaseTag} release with ID: ${updateRelease.id}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If it's another error, throw it
|
// If it's another error, throw it
|
||||||
@@ -255,11 +309,8 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// File names based on release type
|
// File names based on release type
|
||||||
const jsonFile = isAlpha ? ALPHA_UPDATE_JSON_FILE : UPDATE_JSON_FILE;
|
|
||||||
const proxyFile = isAlpha ? ALPHA_UPDATE_JSON_PROXY : UPDATE_JSON_PROXY;
|
|
||||||
|
|
||||||
// Delete existing assets with these names
|
// Delete existing assets with these names
|
||||||
for (let asset of updateRelease.assets) {
|
for (const asset of updateRelease.assets) {
|
||||||
if (asset.name === jsonFile) {
|
if (asset.name === jsonFile) {
|
||||||
await github.rest.repos.deleteReleaseAsset({
|
await github.rest.repos.deleteReleaseAsset({
|
||||||
...options,
|
...options,
|
||||||
@@ -270,7 +321,12 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
if (asset.name === proxyFile) {
|
if (asset.name === proxyFile) {
|
||||||
await github.rest.repos
|
await github.rest.repos
|
||||||
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
.deleteReleaseAsset({ ...options, asset_id: asset.id })
|
||||||
.catch(console.error); // do not break the pipeline
|
.catch((deleteError) =>
|
||||||
|
console.error(
|
||||||
|
`[${channelName}] Failed to delete existing proxy asset:`,
|
||||||
|
deleteError.message,
|
||||||
|
),
|
||||||
|
); // do not break the pipeline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,20 +346,22 @@ async function processRelease(github, options, tag, isAlpha) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Successfully uploaded ${isAlpha ? "alpha" : "stable"} update files to ${releaseTag}`,
|
`[${channelName}] Successfully uploaded update files to ${updateReleaseTag}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to process ${isAlpha ? "alpha" : "stable"} release:`,
|
`[${channelName}] Failed to process update release:`,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
console.log(`Release not found for tag: ${tag.name}, skipping...`);
|
console.log(
|
||||||
|
`[${channelName}] Release not found for tag: ${tagName}, skipping...`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to get release for tag: ${tag.name}`,
|
`[${channelName}] Failed to get release for tag: ${tagName}`,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1158,6 +1158,7 @@ dependencies = [
|
|||||||
"tauri-plugin-window-state",
|
"tauri-plugin-window-state",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"url",
|
||||||
"users",
|
"users",
|
||||||
"warp",
|
"warp",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ clash_verge_service_ipc = { version = "2.0.21", features = [
|
|||||||
"client",
|
"client",
|
||||||
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
|
||||||
arc-swap = "1.7.1"
|
arc-swap = "1.7.1"
|
||||||
|
url = "2.5.4"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
runas = "=1.2.0"
|
runas = "=1.2.0"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub mod runtime;
|
|||||||
pub mod save_profile;
|
pub mod save_profile;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod system;
|
pub mod system;
|
||||||
|
pub mod updater;
|
||||||
pub mod uwp;
|
pub mod uwp;
|
||||||
pub mod validate;
|
pub mod validate;
|
||||||
pub mod verge;
|
pub mod verge;
|
||||||
@@ -34,6 +35,7 @@ pub use runtime::*;
|
|||||||
pub use save_profile::*;
|
pub use save_profile::*;
|
||||||
pub use service::*;
|
pub use service::*;
|
||||||
pub use system::*;
|
pub use system::*;
|
||||||
|
pub use updater::*;
|
||||||
pub use uwp::*;
|
pub use uwp::*;
|
||||||
pub use validate::*;
|
pub use validate::*;
|
||||||
pub use verge::*;
|
pub use verge::*;
|
||||||
|
|||||||
149
src-tauri/src/cmd/updater.rs
Normal file
149
src-tauri/src/cmd/updater.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use tauri::{Manager, ResourceId, Runtime, webview::Webview};
|
||||||
|
use tauri_plugin_updater::UpdaterExt;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use super::{CmdResult, String};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateMetadata {
|
||||||
|
rid: ResourceId,
|
||||||
|
current_version: String,
|
||||||
|
version: String,
|
||||||
|
date: Option<String>,
|
||||||
|
body: Option<String>,
|
||||||
|
raw_json: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum UpdateChannel {
|
||||||
|
Stable,
|
||||||
|
Autobuild,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for UpdateChannel {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
"stable" => Ok(Self::Stable),
|
||||||
|
"autobuild" => Ok(Self::Autobuild),
|
||||||
|
other => Err(String::from(format!("Unsupported channel \"{other}\""))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANNEL_RELEASE_TAGS: &[(UpdateChannel, &str)] = &[
|
||||||
|
(UpdateChannel::Stable, "updater"),
|
||||||
|
(UpdateChannel::Autobuild, "updater-autobuild"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const CHANNEL_ENDPOINT_TEMPLATES: &[&str] = &[
|
||||||
|
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
||||||
|
"https://gh-proxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update-proxy.json",
|
||||||
|
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/{release}/update.json",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn resolve_release_tag(channel: UpdateChannel) -> CmdResult<&'static str> {
|
||||||
|
CHANNEL_RELEASE_TAGS
|
||||||
|
.iter()
|
||||||
|
.find_map(|(entry_channel, tag)| (*entry_channel == channel).then_some(*tag))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
String::from(format!(
|
||||||
|
"No release tag registered for update channel \"{channel:?}\""
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_channel_endpoints(channel: UpdateChannel) -> CmdResult<Vec<Url>> {
|
||||||
|
let release_tag = resolve_release_tag(channel)?;
|
||||||
|
CHANNEL_ENDPOINT_TEMPLATES
|
||||||
|
.iter()
|
||||||
|
.map(|template| {
|
||||||
|
let endpoint = template.replace("{release}", release_tag);
|
||||||
|
Url::parse(&endpoint).map_err(|err| {
|
||||||
|
String::from(format!(
|
||||||
|
"Failed to parse updater endpoint \"{endpoint}\": {err}"
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn check_update_channel<R: Runtime>(
|
||||||
|
webview: Webview<R>,
|
||||||
|
channel: String,
|
||||||
|
headers: Option<Vec<(String, String)>>,
|
||||||
|
timeout: Option<u64>,
|
||||||
|
proxy: Option<String>,
|
||||||
|
target: Option<String>,
|
||||||
|
allow_downgrades: Option<bool>,
|
||||||
|
) -> CmdResult<Option<UpdateMetadata>> {
|
||||||
|
let channel_enum = UpdateChannel::try_from(channel.as_str())?;
|
||||||
|
let endpoints = resolve_channel_endpoints(channel_enum)?;
|
||||||
|
|
||||||
|
let mut builder = webview
|
||||||
|
.updater_builder()
|
||||||
|
.endpoints(endpoints)
|
||||||
|
.map_err(|err| String::from(err.to_string()))?;
|
||||||
|
|
||||||
|
if let Some(headers) = headers {
|
||||||
|
for (key, value) in headers {
|
||||||
|
builder = builder
|
||||||
|
.header(key.as_str(), value.as_str())
|
||||||
|
.map_err(|err| String::from(err.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(timeout) = timeout {
|
||||||
|
builder = builder.timeout(std::time::Duration::from_millis(timeout));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(proxy) = proxy {
|
||||||
|
let proxy_url = Url::parse(&proxy)
|
||||||
|
.map_err(|err| String::from(format!("Invalid proxy URL \"{proxy}\": {err}")))?;
|
||||||
|
builder = builder.proxy(proxy_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(target) = target {
|
||||||
|
builder = builder.target(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allow_downgrades = allow_downgrades.unwrap_or(channel_enum != UpdateChannel::Stable);
|
||||||
|
|
||||||
|
if allow_downgrades {
|
||||||
|
builder = builder.version_comparator(|current, update| update.version != current);
|
||||||
|
}
|
||||||
|
|
||||||
|
let updater = builder
|
||||||
|
.build()
|
||||||
|
.map_err(|err| String::from(err.to_string()))?;
|
||||||
|
|
||||||
|
let update = updater
|
||||||
|
.check()
|
||||||
|
.await
|
||||||
|
.map_err(|err| String::from(err.to_string()))?;
|
||||||
|
|
||||||
|
let Some(update) = update else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted_date = update
|
||||||
|
.date
|
||||||
|
.as_ref()
|
||||||
|
.map(|date| String::from(date.to_string()));
|
||||||
|
|
||||||
|
let metadata = UpdateMetadata {
|
||||||
|
rid: webview.resources_table().add(update.clone()),
|
||||||
|
current_version: String::from(update.current_version.clone()),
|
||||||
|
version: String::from(update.version.clone()),
|
||||||
|
date: formatted_date,
|
||||||
|
body: update.body.clone().map(Into::into),
|
||||||
|
raw_json: update.raw_json.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(metadata))
|
||||||
|
}
|
||||||
@@ -174,6 +174,7 @@ mod app_init {
|
|||||||
cmd::get_runtime_logs,
|
cmd::get_runtime_logs,
|
||||||
cmd::get_runtime_proxy_chain_config,
|
cmd::get_runtime_proxy_chain_config,
|
||||||
cmd::update_proxy_chain_config_in_runtime,
|
cmd::update_proxy_chain_config_in_runtime,
|
||||||
|
cmd::check_update_channel,
|
||||||
cmd::invoke_uwp_tool,
|
cmd::invoke_uwp_tool,
|
||||||
cmd::copy_clash_env,
|
cmd::copy_clash_env,
|
||||||
cmd::sync_tray_proxy_selection,
|
cmd::sync_tray_proxy_selection,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useServiceInstaller } from "@/hooks/useServiceInstaller";
|
|||||||
import { getSystemInfo } from "@/services/cmds";
|
import { getSystemInfo } from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||||
|
import { useUpdateChannel } from "@/services/updateChannel";
|
||||||
import { version as appVersion } from "@root/package.json";
|
import { version as appVersion } from "@root/package.json";
|
||||||
|
|
||||||
import { EnhancedCard } from "./enhanced-card";
|
import { EnhancedCard } from "./enhanced-card";
|
||||||
@@ -59,6 +60,7 @@ export const SystemInfoCard = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { isAdminMode, isSidecarMode } = useSystemState();
|
const { isAdminMode, isSidecarMode } = useSystemState();
|
||||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||||
|
const [updateChannel] = useUpdateChannel();
|
||||||
|
|
||||||
// 系统信息状态
|
// 系统信息状态
|
||||||
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
const [systemState, dispatchSystemState] = useReducer(systemStateReducer, {
|
||||||
@@ -117,7 +119,7 @@ export const SystemInfoCard = () => {
|
|||||||
|
|
||||||
timeoutId = window.setTimeout(() => {
|
timeoutId = window.setTimeout(() => {
|
||||||
if (verge?.auto_check_update) {
|
if (verge?.auto_check_update) {
|
||||||
checkUpdate().catch(console.error);
|
checkUpdate(updateChannel).catch(console.error);
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
@@ -126,11 +128,11 @@ export const SystemInfoCard = () => {
|
|||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [verge?.auto_check_update, dispatchSystemState]);
|
}, [verge?.auto_check_update, dispatchSystemState, updateChannel]);
|
||||||
|
|
||||||
// 自动检查更新逻辑
|
// 自动检查更新逻辑
|
||||||
useSWR(
|
useSWR(
|
||||||
verge?.auto_check_update ? "checkUpdate" : null,
|
verge?.auto_check_update ? ["checkUpdate", updateChannel] : null,
|
||||||
async () => {
|
async () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem("last_check_update", now.toString());
|
localStorage.setItem("last_check_update", now.toString());
|
||||||
@@ -138,7 +140,7 @@ export const SystemInfoCard = () => {
|
|||||||
type: "set-last-check-update",
|
type: "set-last-check-update",
|
||||||
payload: new Date(now).toLocaleString(),
|
payload: new Date(now).toLocaleString(),
|
||||||
});
|
});
|
||||||
return await checkUpdate();
|
return await checkUpdate(updateChannel);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
revalidateOnFocus: false,
|
revalidateOnFocus: false,
|
||||||
@@ -172,7 +174,7 @@ export const SystemInfoCard = () => {
|
|||||||
// 检查更新
|
// 检查更新
|
||||||
const onCheckUpdate = useLockFn(async () => {
|
const onCheckUpdate = useLockFn(async () => {
|
||||||
try {
|
try {
|
||||||
const info = await checkUpdate();
|
const info = await checkUpdate(updateChannel);
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice("success", t("Currently on the Latest Version"));
|
showNotice("success", t("Currently on the Latest Version"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import useSWR from "swr";
|
|||||||
|
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { checkUpdateSafe } from "@/services/update";
|
import { checkUpdateSafe } from "@/services/update";
|
||||||
|
import { useUpdateChannel } from "@/services/updateChannel";
|
||||||
|
|
||||||
import { DialogRef } from "../base";
|
import { DialogRef } from "../base";
|
||||||
import { UpdateViewer } from "../setting/mods/update-viewer";
|
import { UpdateViewer } from "../setting/mods/update-viewer";
|
||||||
@@ -16,12 +17,14 @@ export const UpdateButton = (props: Props) => {
|
|||||||
const { className } = props;
|
const { className } = props;
|
||||||
const { verge } = useVerge();
|
const { verge } = useVerge();
|
||||||
const { auto_check_update } = verge || {};
|
const { auto_check_update } = verge || {};
|
||||||
|
const [updateChannel] = useUpdateChannel();
|
||||||
|
|
||||||
const viewerRef = useRef<DialogRef>(null);
|
const viewerRef = useRef<DialogRef>(null);
|
||||||
|
|
||||||
|
const shouldCheck = auto_check_update || auto_check_update === null;
|
||||||
const { data: updateInfo } = useSWR(
|
const { data: updateInfo } = useSWR(
|
||||||
auto_check_update || auto_check_update === null ? "checkUpdate" : null,
|
shouldCheck ? ["checkUpdate", updateChannel] : null,
|
||||||
checkUpdateSafe,
|
() => checkUpdateSafe(updateChannel),
|
||||||
{
|
{
|
||||||
errorRetryCount: 2,
|
errorRetryCount: 2,
|
||||||
revalidateIfStale: false,
|
revalidateIfStale: false,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { portableFlag } from "@/pages/_layout";
|
|||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { useSetUpdateState, useUpdateState } from "@/services/states";
|
import { useSetUpdateState, useUpdateState } from "@/services/states";
|
||||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||||
|
import { useUpdateChannel } from "@/services/updateChannel";
|
||||||
|
|
||||||
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -26,12 +27,17 @@ export function UpdateViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
const updateState = useUpdateState();
|
const updateState = useUpdateState();
|
||||||
const setUpdateState = useSetUpdateState();
|
const setUpdateState = useSetUpdateState();
|
||||||
const { addListener } = useListen();
|
const { addListener } = useListen();
|
||||||
|
const [updateChannel] = useUpdateChannel();
|
||||||
|
|
||||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
const { data: updateInfo } = useSWR(
|
||||||
errorRetryCount: 2,
|
["checkUpdate", updateChannel],
|
||||||
revalidateIfStale: false,
|
() => checkUpdate(updateChannel),
|
||||||
focusThrottleInterval: 36e5, // 1 hour
|
{
|
||||||
});
|
errorRetryCount: 2,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
focusThrottleInterval: 36e5, // 1 hour
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const [downloaded, setDownloaded] = useState(0);
|
const [downloaded, setDownloaded] = useState(0);
|
||||||
const [buffer, setBuffer] = useState(0);
|
const [buffer, setBuffer] = useState(0);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from "@/services/cmds";
|
} from "@/services/cmds";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
import { checkUpdateSafe as checkUpdate } from "@/services/update";
|
||||||
|
import { useUpdateChannel } from "@/services/updateChannel";
|
||||||
import { version } from "@root/package.json";
|
import { version } from "@root/package.json";
|
||||||
|
|
||||||
import { BackupViewer } from "./mods/backup-viewer";
|
import { BackupViewer } from "./mods/backup-viewer";
|
||||||
@@ -42,10 +43,11 @@ const SettingVergeAdvanced = ({ onError: _ }: Props) => {
|
|||||||
const updateRef = useRef<DialogRef>(null);
|
const updateRef = useRef<DialogRef>(null);
|
||||||
const backupRef = useRef<DialogRef>(null);
|
const backupRef = useRef<DialogRef>(null);
|
||||||
const liteModeRef = useRef<DialogRef>(null);
|
const liteModeRef = useRef<DialogRef>(null);
|
||||||
|
const [updateChannel] = useUpdateChannel();
|
||||||
|
|
||||||
const onCheckUpdate = async () => {
|
const onCheckUpdate = async () => {
|
||||||
try {
|
try {
|
||||||
const info = await checkUpdate();
|
const info = await checkUpdate(updateChannel);
|
||||||
if (!info?.available) {
|
if (!info?.available) {
|
||||||
showNotice("success", t("Currently on the Latest Version"));
|
showNotice("success", t("Currently on the Latest Version"));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ContentCopyRounded } from "@mui/icons-material";
|
import { ContentCopyRounded } from "@mui/icons-material";
|
||||||
import { Button, Input, MenuItem, Select } from "@mui/material";
|
import {
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
} from "@mui/material";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -11,6 +17,11 @@ import { navItems } from "@/pages/_routers";
|
|||||||
import { copyClashEnv } from "@/services/cmds";
|
import { copyClashEnv } from "@/services/cmds";
|
||||||
import { supportedLanguages } from "@/services/i18n";
|
import { supportedLanguages } from "@/services/i18n";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
import {
|
||||||
|
UPDATE_CHANNEL_OPTIONS,
|
||||||
|
type UpdateChannel,
|
||||||
|
useUpdateChannel,
|
||||||
|
} from "@/services/updateChannel";
|
||||||
import getSystem from "@/utils/get-system";
|
import getSystem from "@/utils/get-system";
|
||||||
|
|
||||||
import { BackupViewer } from "./mods/backup-viewer";
|
import { BackupViewer } from "./mods/backup-viewer";
|
||||||
@@ -69,6 +80,7 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
const layoutRef = useRef<DialogRef>(null);
|
const layoutRef = useRef<DialogRef>(null);
|
||||||
const updateRef = useRef<DialogRef>(null);
|
const updateRef = useRef<DialogRef>(null);
|
||||||
const backupRef = useRef<DialogRef>(null);
|
const backupRef = useRef<DialogRef>(null);
|
||||||
|
const [updateChannel, setUpdateChannel] = useUpdateChannel();
|
||||||
|
|
||||||
const onChangeData = (patch: any) => {
|
const onChangeData = (patch: any) => {
|
||||||
mutateVerge({ ...verge, ...patch }, false);
|
mutateVerge({ ...verge, ...patch }, false);
|
||||||
@@ -79,6 +91,14 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
showNotice("success", t("Copy Success"), 1000);
|
showNotice("success", t("Copy Success"), 1000);
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
|
const onUpdateChannelChange = useCallback(
|
||||||
|
(event: SelectChangeEvent<UpdateChannel>) => {
|
||||||
|
const nextChannel = event.target.value as UpdateChannel;
|
||||||
|
setUpdateChannel(nextChannel);
|
||||||
|
},
|
||||||
|
[setUpdateChannel],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingList title={t("Verge Basic Setting")}>
|
<SettingList title={t("Verge Basic Setting")}>
|
||||||
<ThemeViewer ref={themeRef} />
|
<ThemeViewer ref={themeRef} />
|
||||||
@@ -89,6 +109,21 @@ const SettingVergeBasic = ({ onError }: Props) => {
|
|||||||
<UpdateViewer ref={updateRef} />
|
<UpdateViewer ref={updateRef} />
|
||||||
<BackupViewer ref={backupRef} />
|
<BackupViewer ref={backupRef} />
|
||||||
|
|
||||||
|
<SettingItem label={t("Update Channel")}>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={updateChannel}
|
||||||
|
onChange={onUpdateChannelChange}
|
||||||
|
sx={{ width: 160, "> div": { py: "7.5px" } }}
|
||||||
|
>
|
||||||
|
{UPDATE_CHANNEL_OPTIONS.map((option) => (
|
||||||
|
<MenuItem key={option.value} value={option.value}>
|
||||||
|
{t(option.labelKey)}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem label={t("Language")}>
|
<SettingItem label={t("Language")}>
|
||||||
<GuardState
|
<GuardState
|
||||||
value={language ?? "en"}
|
value={language ?? "en"}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"Proxy detail": "تفاصيل الوكيل",
|
"Proxy detail": "تفاصيل الوكيل",
|
||||||
"Profiles": "الملفات الشخصية",
|
"Profiles": "الملفات الشخصية",
|
||||||
"Update All Profiles": "تحديث جميع الملفات الشخصية",
|
"Update All Profiles": "تحديث جميع الملفات الشخصية",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "عرض تكوين وقت التشغيل",
|
"View Runtime Config": "عرض تكوين وقت التشغيل",
|
||||||
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
|
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
|
||||||
"Paste": "لصق",
|
"Paste": "لصق",
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
"Proxy detail": "Knotendetails anzeigen",
|
"Proxy detail": "Knotendetails anzeigen",
|
||||||
"Profiles": "Abonnement",
|
"Profiles": "Abonnement",
|
||||||
"Update All Profiles": "Alle Abonnements aktualisieren",
|
"Update All Profiles": "Alle Abonnements aktualisieren",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
|
"View Runtime Config": "Laufzeit-Abonnement anzeigen",
|
||||||
"Reactivate Profiles": "Abonnement erneut aktivieren",
|
"Reactivate Profiles": "Abonnement erneut aktivieren",
|
||||||
"Paste": "Einfügen",
|
"Paste": "Einfügen",
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
"Proxy detail": "Proxy detail",
|
"Proxy detail": "Proxy detail",
|
||||||
"Profiles": "Profiles",
|
"Profiles": "Profiles",
|
||||||
"Update All Profiles": "Update All Profiles",
|
"Update All Profiles": "Update All Profiles",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "View Runtime Config",
|
"View Runtime Config": "View Runtime Config",
|
||||||
"Reactivate Profiles": "Reactivate Profiles",
|
"Reactivate Profiles": "Reactivate Profiles",
|
||||||
"Paste": "Paste",
|
"Paste": "Paste",
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
"Proxy detail": "Mostrar detalles del nodo",
|
"Proxy detail": "Mostrar detalles del nodo",
|
||||||
"Profiles": "Suscripciones",
|
"Profiles": "Suscripciones",
|
||||||
"Update All Profiles": "Actualizar todas las suscripciones",
|
"Update All Profiles": "Actualizar todas las suscripciones",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "Ver configuración en tiempo de ejecución",
|
"View Runtime Config": "Ver configuración en tiempo de ejecución",
|
||||||
"Reactivate Profiles": "Reactivar suscripciones",
|
"Reactivate Profiles": "Reactivar suscripciones",
|
||||||
"Paste": "Pegar",
|
"Paste": "Pegar",
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"Proxy detail": "جزئیات پراکسی",
|
"Proxy detail": "جزئیات پراکسی",
|
||||||
"Profiles": "پروفایلها",
|
"Profiles": "پروفایلها",
|
||||||
"Update All Profiles": "بهروزرسانی همه پروفایلها",
|
"Update All Profiles": "بهروزرسانی همه پروفایلها",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
|
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
|
||||||
"Reactivate Profiles": "فعالسازی مجدد پروفایلها",
|
"Reactivate Profiles": "فعالسازی مجدد پروفایلها",
|
||||||
"Paste": "چسباندن",
|
"Paste": "چسباندن",
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"Proxy detail": "Detail Proksi",
|
"Proxy detail": "Detail Proksi",
|
||||||
"Profiles": "Profil",
|
"Profiles": "Profil",
|
||||||
"Update All Profiles": "Perbarui Semua Profil",
|
"Update All Profiles": "Perbarui Semua Profil",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "Lihat Konfigurasi Runtime",
|
"View Runtime Config": "Lihat Konfigurasi Runtime",
|
||||||
"Reactivate Profiles": "Reaktivasi Profil",
|
"Reactivate Profiles": "Reaktivasi Profil",
|
||||||
"Paste": "Tempel",
|
"Paste": "Tempel",
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
"Proxy detail": "ノードの詳細を表示する",
|
"Proxy detail": "ノードの詳細を表示する",
|
||||||
"Profiles": "プロファイル",
|
"Profiles": "プロファイル",
|
||||||
"Update All Profiles": "すべてのプロファイルを更新",
|
"Update All Profiles": "すべてのプロファイルを更新",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "実行時のプロファイルを表示",
|
"View Runtime Config": "実行時のプロファイルを表示",
|
||||||
"Reactivate Profiles": "プロファイルを再アクティブ化",
|
"Reactivate Profiles": "プロファイルを再アクティブ化",
|
||||||
"Paste": "貼り付け",
|
"Paste": "貼り付け",
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
"Proxy detail": "프록시 상세",
|
"Proxy detail": "프록시 상세",
|
||||||
"Profiles": "프로필",
|
"Profiles": "프로필",
|
||||||
"Update All Profiles": "모든 프로필 업데이트",
|
"Update All Profiles": "모든 프로필 업데이트",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "런타임 설정 보기",
|
"View Runtime Config": "런타임 설정 보기",
|
||||||
"Reactivate Profiles": "프로필 재활성화",
|
"Reactivate Profiles": "프로필 재활성화",
|
||||||
"Paste": "붙여넣기",
|
"Paste": "붙여넣기",
|
||||||
|
|||||||
@@ -51,6 +51,9 @@
|
|||||||
"Proxy detail": "Отображать больше сведений о прокси",
|
"Proxy detail": "Отображать больше сведений о прокси",
|
||||||
"Profiles": "Профили",
|
"Profiles": "Профили",
|
||||||
"Update All Profiles": "Обновить все профили",
|
"Update All Profiles": "Обновить все профили",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||||
"Reactivate Profiles": "Перезапустить профиль",
|
"Reactivate Profiles": "Перезапустить профиль",
|
||||||
"Paste": "Вставить",
|
"Paste": "Вставить",
|
||||||
|
|||||||
@@ -46,6 +46,9 @@
|
|||||||
"Proxy detail": "Vekil detayı",
|
"Proxy detail": "Vekil detayı",
|
||||||
"Profiles": "Profiller",
|
"Profiles": "Profiller",
|
||||||
"Update All Profiles": "Tüm Profilleri Güncelle",
|
"Update All Profiles": "Tüm Profilleri Güncelle",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
|
"View Runtime Config": "Çalışma Zamanı Yapılandırmasını Görüntüle",
|
||||||
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
|
"Reactivate Profiles": "Profilleri Yeniden Etkinleştir",
|
||||||
"Paste": "Yapıştır",
|
"Paste": "Yapıştır",
|
||||||
|
|||||||
@@ -43,6 +43,9 @@
|
|||||||
"Proxy detail": "Прокси турында тулы мәгълүмат",
|
"Proxy detail": "Прокси турында тулы мәгълүмат",
|
||||||
"Profiles": "Профильләр",
|
"Profiles": "Профильләр",
|
||||||
"Update All Profiles": "Барлык профильләрне яңарту",
|
"Update All Profiles": "Барлык профильләрне яңарту",
|
||||||
|
"Update Channel": "Update Channel",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "Кулланылган конфигурацияне карау",
|
"View Runtime Config": "Кулланылган конфигурацияне карау",
|
||||||
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
|
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
|
||||||
"Paste": "Кую",
|
"Paste": "Кую",
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
"Proxy detail": "展示节点细节",
|
"Proxy detail": "展示节点细节",
|
||||||
"Profiles": "订阅",
|
"Profiles": "订阅",
|
||||||
"Update All Profiles": "更新所有订阅",
|
"Update All Profiles": "更新所有订阅",
|
||||||
|
"Update Channel": "更新通道",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "查看运行时订阅",
|
"View Runtime Config": "查看运行时订阅",
|
||||||
"Reactivate Profiles": "重新激活订阅",
|
"Reactivate Profiles": "重新激活订阅",
|
||||||
"Paste": "粘贴",
|
"Paste": "粘贴",
|
||||||
|
|||||||
@@ -59,6 +59,9 @@
|
|||||||
"Proxy detail": "展示節點細節",
|
"Proxy detail": "展示節點細節",
|
||||||
"Profiles": "訂閱",
|
"Profiles": "訂閱",
|
||||||
"Update All Profiles": "更新所有訂閱",
|
"Update All Profiles": "更新所有訂閱",
|
||||||
|
"Update Channel": "更新通道",
|
||||||
|
"Update Channel Stable": "Stable",
|
||||||
|
"Update Channel Autobuild": "Autobuild",
|
||||||
"View Runtime Config": "查看執行時訂閱",
|
"View Runtime Config": "查看執行時訂閱",
|
||||||
"Reactivate Profiles": "重新啟用訂閱",
|
"Reactivate Profiles": "重新啟用訂閱",
|
||||||
"Paste": "貼上",
|
"Paste": "貼上",
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import {
|
|||||||
compareVersions,
|
compareVersions,
|
||||||
ensureSemver,
|
ensureSemver,
|
||||||
extractSemver,
|
extractSemver,
|
||||||
|
isPrereleaseVersion,
|
||||||
normalizeVersion,
|
normalizeVersion,
|
||||||
resolveRemoteVersion,
|
resolveRemoteVersion,
|
||||||
|
shouldRejectUpdate,
|
||||||
splitVersion,
|
splitVersion,
|
||||||
} from "@/services/update";
|
} from "@/services/update";
|
||||||
import type { VersionParts } from "@/services/update";
|
import type { VersionParts } from "@/services/update";
|
||||||
@@ -138,3 +140,53 @@ describe("resolveRemoteVersion", () => {
|
|||||||
expect(resolveRemoteVersion(update)).toBeNull();
|
expect(resolveRemoteVersion(update)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isPrereleaseVersion", () => {
|
||||||
|
it("returns true when version has prerelease identifiers", () => {
|
||||||
|
expect(isPrereleaseVersion("1.2.3-beta.1")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for release versions or missing input", () => {
|
||||||
|
expect(isPrereleaseVersion("1.2.3")).toBe(false);
|
||||||
|
expect(isPrereleaseVersion(null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shouldRejectUpdate", () => {
|
||||||
|
const localStable = "2.4.3";
|
||||||
|
const remoteAutobuild = "2.4.3-autobuild.1122.qwerty.r1a";
|
||||||
|
|
||||||
|
it("rejects when comparison cannot proceed in downgrade-safe way on stable channel", () => {
|
||||||
|
expect(shouldRejectUpdate("stable", -1, "2.4.2", localStable)).toBe(true);
|
||||||
|
expect(shouldRejectUpdate("stable", 0, "2.4.3", localStable)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows prerelease downgrade on autobuild channel", () => {
|
||||||
|
expect(
|
||||||
|
shouldRejectUpdate("autobuild", -1, remoteAutobuild, localStable),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects prerelease downgrade when base version is older", () => {
|
||||||
|
expect(
|
||||||
|
shouldRejectUpdate("autobuild", -1, "2.3.0-autobuild.1", localStable),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects downgrade when both versions are prereleases", () => {
|
||||||
|
expect(
|
||||||
|
shouldRejectUpdate(
|
||||||
|
"autobuild",
|
||||||
|
-1,
|
||||||
|
"2.4.3-autobuild.1122.qwerty.r1a",
|
||||||
|
"2.4.3-autobuild.1127.qwerty.r1a",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects downgrade when remote release is older even on autobuild channel", () => {
|
||||||
|
expect(shouldRejectUpdate("autobuild", -1, "2.4.2", localStable)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
import {
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
check,
|
import { Update, type CheckOptions } from "@tauri-apps/plugin-updater";
|
||||||
type CheckOptions,
|
|
||||||
type Update,
|
|
||||||
} from "@tauri-apps/plugin-updater";
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_UPDATE_CHANNEL,
|
||||||
|
getStoredUpdateChannel,
|
||||||
|
type UpdateChannel,
|
||||||
|
} from "@/services/updateChannel";
|
||||||
import { version as appVersion } from "@root/package.json";
|
import { version as appVersion } from "@root/package.json";
|
||||||
|
|
||||||
|
type NativeUpdateMetadata = {
|
||||||
|
rid: number;
|
||||||
|
currentVersion: string;
|
||||||
|
version: string;
|
||||||
|
date?: string;
|
||||||
|
body?: string | null;
|
||||||
|
rawJson: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
export type VersionParts = {
|
export type VersionParts = {
|
||||||
main: number[];
|
main: number[];
|
||||||
pre: (number | string)[];
|
pre: (number | string)[];
|
||||||
@@ -131,16 +142,92 @@ export const resolveRemoteVersion = (update: Update): string | null => {
|
|||||||
|
|
||||||
const localVersionNormalized = normalizeVersion(appVersion);
|
const localVersionNormalized = normalizeVersion(appVersion);
|
||||||
|
|
||||||
export const checkUpdateSafe = async (
|
export const isPrereleaseVersion = (version: string | null): boolean => {
|
||||||
|
const parts = splitVersion(version);
|
||||||
|
return Boolean(parts?.pre.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldRejectUpdate = (
|
||||||
|
channel: UpdateChannel,
|
||||||
|
comparison: number | null,
|
||||||
|
remoteVersion: string | null,
|
||||||
|
localVersion: string | null,
|
||||||
|
): boolean => {
|
||||||
|
if (comparison === null) return false;
|
||||||
|
if (comparison === 0) return true;
|
||||||
|
if (comparison > 0) return false;
|
||||||
|
|
||||||
|
if (channel !== "stable") {
|
||||||
|
const remoteIsPrerelease = isPrereleaseVersion(remoteVersion);
|
||||||
|
const localIsPrerelease = isPrereleaseVersion(localVersion);
|
||||||
|
if (remoteIsPrerelease && !localIsPrerelease) {
|
||||||
|
const remoteParts = splitVersion(remoteVersion);
|
||||||
|
const localParts = splitVersion(localVersion);
|
||||||
|
if (!remoteParts || !localParts) return true;
|
||||||
|
|
||||||
|
const mainComparison = compareVersionParts(
|
||||||
|
{ main: remoteParts.main, pre: [] },
|
||||||
|
{ main: localParts.main, pre: [] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mainComparison < 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeHeaders = (
|
||||||
|
headers?: HeadersInit,
|
||||||
|
): Array<[string, string]> | undefined => {
|
||||||
|
if (!headers) return undefined;
|
||||||
|
const pairs = Array.from(new Headers(headers).entries());
|
||||||
|
return pairs.length > 0 ? pairs : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkUpdateForChannel = async (
|
||||||
|
channel: UpdateChannel = DEFAULT_UPDATE_CHANNEL,
|
||||||
options?: CheckOptions,
|
options?: CheckOptions,
|
||||||
): Promise<Update | null> => {
|
): Promise<Update | null> => {
|
||||||
const result = await check({ ...(options ?? {}), allowDowngrades: false });
|
const allowDowngrades = channel !== "stable";
|
||||||
|
|
||||||
|
const metadata = await invoke<NativeUpdateMetadata | null>(
|
||||||
|
"check_update_channel",
|
||||||
|
{
|
||||||
|
channel,
|
||||||
|
headers: normalizeHeaders(options?.headers),
|
||||||
|
timeout: options?.timeout,
|
||||||
|
proxy: options?.proxy,
|
||||||
|
target: options?.target,
|
||||||
|
allowDowngrades,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!metadata) return null;
|
||||||
|
|
||||||
|
const result = new Update({
|
||||||
|
...metadata,
|
||||||
|
body:
|
||||||
|
typeof metadata.body === "string"
|
||||||
|
? metadata.body
|
||||||
|
: metadata.body === null
|
||||||
|
? undefined
|
||||||
|
: metadata.body,
|
||||||
|
});
|
||||||
|
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|
||||||
const remoteVersion = resolveRemoteVersion(result);
|
const remoteVersion = resolveRemoteVersion(result);
|
||||||
const comparison = compareVersions(remoteVersion, localVersionNormalized);
|
const comparison = compareVersions(remoteVersion, localVersionNormalized);
|
||||||
|
if (
|
||||||
if (comparison !== null && comparison <= 0) {
|
shouldRejectUpdate(
|
||||||
|
channel,
|
||||||
|
comparison,
|
||||||
|
remoteVersion,
|
||||||
|
localVersionNormalized,
|
||||||
|
)
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await result.close();
|
await result.close();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -152,4 +239,13 @@ export const checkUpdateSafe = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkUpdateSafe = async (
|
||||||
|
channel?: UpdateChannel,
|
||||||
|
options?: CheckOptions,
|
||||||
|
): Promise<Update | null> => {
|
||||||
|
const resolvedChannel = channel ?? getStoredUpdateChannel();
|
||||||
|
return checkUpdateForChannel(resolvedChannel, options);
|
||||||
|
};
|
||||||
|
|
||||||
export type { CheckOptions };
|
export type { CheckOptions };
|
||||||
|
export type { UpdateChannel } from "@/services/updateChannel";
|
||||||
|
|||||||
57
src/services/updateChannel.ts
Normal file
57
src/services/updateChannel.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useLocalStorage } from "foxact/use-local-storage";
|
||||||
|
|
||||||
|
export type UpdateChannel = "stable" | "autobuild";
|
||||||
|
|
||||||
|
export const UPDATE_CHANNEL_STORAGE_KEY = "update-channel";
|
||||||
|
|
||||||
|
export const DEFAULT_UPDATE_CHANNEL: UpdateChannel = "stable";
|
||||||
|
|
||||||
|
export const UPDATE_CHANNEL_OPTIONS: Array<{
|
||||||
|
value: UpdateChannel;
|
||||||
|
labelKey: string;
|
||||||
|
}> = [
|
||||||
|
{ value: "stable", labelKey: "Update Channel Stable" },
|
||||||
|
{ value: "autobuild", labelKey: "Update Channel Autobuild" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isValidChannel = (value: unknown): value is UpdateChannel => {
|
||||||
|
return value === "stable" || value === "autobuild";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateChannel = () =>
|
||||||
|
useLocalStorage<UpdateChannel>(
|
||||||
|
UPDATE_CHANNEL_STORAGE_KEY,
|
||||||
|
DEFAULT_UPDATE_CHANNEL,
|
||||||
|
{
|
||||||
|
serializer: JSON.stringify,
|
||||||
|
deserializer: (value) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return isValidChannel(parsed) ? parsed : DEFAULT_UPDATE_CHANNEL;
|
||||||
|
} catch (ignoreErr) {
|
||||||
|
return DEFAULT_UPDATE_CHANNEL;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getStoredUpdateChannel = (): UpdateChannel => {
|
||||||
|
if (
|
||||||
|
typeof window === "undefined" ||
|
||||||
|
typeof window.localStorage === "undefined"
|
||||||
|
) {
|
||||||
|
return DEFAULT_UPDATE_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = window.localStorage.getItem(UPDATE_CHANNEL_STORAGE_KEY);
|
||||||
|
if (raw === null) {
|
||||||
|
return DEFAULT_UPDATE_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return isValidChannel(parsed) ? parsed : DEFAULT_UPDATE_CHANNEL;
|
||||||
|
} catch (ignoreErr) {
|
||||||
|
return DEFAULT_UPDATE_CHANNEL;
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user