diff --git "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\345\273\272\350\256\256.yml" "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\345\273\272\350\256\256.yml" new file mode 100644 index 000000000..aacd9e3f3 --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\345\273\272\350\256\256.yml" @@ -0,0 +1,74 @@ +name: 功能建议 +description: 改进或新功能等 +title: "[FR] " +body: + - type: dropdown + id: request-type + attributes: + label: 建议类型 + description: 请选择最接近的类型。 + multiple: false + options: + - 新功能 + - 现有功能改进 + - UI/交互优化 + - 文档/示例改进 + - 其他 + validations: + required: true + - type: dropdown + id: runtime + attributes: + label: 运行环境 + description: 请选择这个建议涉及的运行环境。 + multiple: false + options: + - Node.js + - Android + - Docker + - 自部署 + - Surge + - Loon + - Quantumult X + - Stash + - Shadowrocket + - 其他 + validations: + required: true + - type: dropdown + id: affected-areas + attributes: + label: 影响范围 + description: 可多选,请选择这个建议涉及的部分。 + multiple: true + options: + - 后端 + - 前端 + - 订阅转换 + - 节点/策略组 + - 同步/缓存 + - 配置/部署 + - 代理 App 兼容 + - 文档 + - 其他 + validations: + required: true + - type: textarea + id: requirement + attributes: + label: 需求描述 + description: 建议把需求讲清楚,不要一下子跳到一个解决方案。 + validations: + required: true + - type: textarea + id: proposed-solution + attributes: + label: 解决方案 + description: 建议先把上面的需求讲清楚,然后根据需求提解决方案。 + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: 其他信息 + description: 例如参考链接、截图、替代方案或额外背景。 diff --git "a/.github/ISSUE_TEMPLATE/\351\227\256\351\242\230\345\217\215\351\246\210.yml" "b/.github/ISSUE_TEMPLATE/\351\227\256\351\242\230\345\217\215\351\246\210.yml" new file mode 100644 index 000000000..ca1b097db --- /dev/null +++ "b/.github/ISSUE_TEMPLATE/\351\227\256\351\242\230\345\217\215\351\246\210.yml" @@ -0,0 +1,104 @@ +name: 问题反馈 +description: 先读完排查方法再反馈 +title: "[BUG] " +body: + - type: markdown + attributes: + value: | + **先读我** + + [排查方法](https://t.me/zhetengsha/218) + - type: checkboxes + id: read-troubleshooting + attributes: + label: 阅读确认 + options: + - label: 我已阅读上方排查方法 + required: true + - type: dropdown + id: runtime + attributes: + label: 运行环境 + description: 请选择主要出现问题的运行环境。 + multiple: false + options: + - Node.js + - Android + - Docker + - 自部署 + - Surge + - Loon + - Quantumult X + - Stash + - Shadowrocket + - 其他 + validations: + required: true + - type: dropdown + id: affected-areas + attributes: + label: 影响范围 + description: 可多选,请选择和问题相关的部分。 + multiple: true + options: + - 后端 + - 前端 + - 订阅转换 + - 节点/策略组 + - 同步/缓存 + - 配置/环境变量 + - 代理 App + - 其他 + validations: + required: true + - type: input + id: runtime-version + attributes: + label: 上述运行环境版本/App 版本 + description: 例如 Node.js 版本、Android 版本、Docker 镜像版本或代理 App 版本。 + validations: + required: true + - type: input + id: backend-version + attributes: + label: 后端版本 + description: 填写当前使用的后端版本。 + validations: + required: true + - type: input + id: frontend-version + attributes: + label: 前端版本 + description: 填写当前使用的前端版本。 + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: 复现方式 + description: 请按步骤说明如何复现。 + value: | + 1. + 2. + 3. + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: 期望表现 + description: 描述你认为应该发生什么。 + validations: + required: true + - type: textarea + id: actual-behavior + attributes: + label: 实际表现 + description: 描述实际发生了什么。 + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: 其他信息 + description: 例如截图、日志、配置片段或相关链接。 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 879145c2b..0cd05e256 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,6 @@ name: build on: + workflow_dispatch: push: branches: - master @@ -35,10 +36,11 @@ jobs: with: vercel-token: ${{ secrets.VERCEL_TOKEN }} # Required # github-token: ${{ secrets.GITHUB_TOKEN }} # Optional - vercel-args: "--prod" # Optional + vercel-args: "--prod --debug" # Optional vercel-org-id: ${{ secrets.ORG_ID}} # Required vercel-project-id: ${{ secrets.PROJECT_ID}} # Required working-directory: dist + vercel-version: '48.9.0' - name: zip run: | zip -r dist.zip dist diff --git a/.github/workflows/update-vercel-project-settings.yml b/.github/workflows/update-vercel-project-settings.yml new file mode 100644 index 000000000..1f019f18d --- /dev/null +++ b/.github/workflows/update-vercel-project-settings.yml @@ -0,0 +1,16 @@ +# 该 workflow 用于更新 Vercel 项目的 Node.js 版本 +name: Update Vercel project settings +on: + workflow_dispatch: + +jobs: + update-nodejs-version: + runs-on: ubuntu-latest + steps: + - name: Update Vercel project Node.js version + run: | + curl --request PATCH \ + --url https://api.vercel.com/v9/projects/${{ secrets.PROJECT_ID }} \ + --header 'Authorization: Bearer ${{ secrets.VERCEL_TOKEN }}' \ + --header 'Content-Type: application/json' \ + --data '{ "nodeVersion":"22.x"}' \ No newline at end of file diff --git a/.gitignore b/.gitignore index fc5ae9f0c..b3c829abc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Logs logs +!src/api/logs/ +!src/api/logs/** *.log npm-debug.log* yarn-debug.log* @@ -23,3 +25,7 @@ dist-ssr *.sln *.sw? .vercel +CODEMAP.md +CONTEXT_BUNDLE.md +mydocs +.github/copilot-instructions.md \ No newline at end of file diff --git a/README.md b/README.md index ab1f67c93..84b52a851 100644 --- a/README.md +++ b/README.md @@ -16,54 +16,7 @@ Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket. Core functionalities: -1. Conversion among various formats. -2. Subscription formatting. -3. Collect multiple subscriptions in one URL. - -## 1. Subscription Conversion - -### Supported Input Formats - -- [x] SS URI -- [x] SSR URI -- [x] SSD URI -- [x] V2RayN URI -- [x] QX (SS, SSR, VMess, Trojan, HTTP) -- [x] Loon (SS, SSR, VMess, Trojan, HTTP) -- [x] Surge (SS, VMess, Trojan, HTTP) -- [x] Stash & Clash (SS, SSR, VMess, Trojan, HTTP) - -### Supported Target Platforms - -- [x] QX -- [x] Loon -- [x] Surge -- [x] Stash & Clash -- [x] ShadowRocket - -## 2. Subscription Formatting - -### Filtering - -- [x] **Regex filter** -- [x] **Discard regex filter** -- [x] **Region filter** -- [x] **Type filter** -- [x] **Useless proxies filter** -- [x] **Script filter** - -### Proxy Operations - -- [x] **Set property operator**: set some proxy properties such as `udp`,`tfo` - , `skip-cert-verify` etc. -- [x] **Flag operator**: add flags or remove flags for proxies. -- [x] **Sort operator**: sort proxies by name. -- [x] **Regex sort operator**: sort proxies by keywords (fallback to normal - sort). -- [x] **Regex rename operator**: replace by regex in proxy names. -- [x] **Regex delete operator**: delete by regex in proxy names. -- [x] **Script operator**: modify proxy by script. - +[Sub-Store](https://github.com/sub-store-org/Sub-Store) ### Development diff --git a/docs/brainstorms/2026-05-04-collection-first-sub-flow-requirements.md b/docs/brainstorms/2026-05-04-collection-first-sub-flow-requirements.md new file mode 100644 index 000000000..10184c190 --- /dev/null +++ b/docs/brainstorms/2026-05-04-collection-first-sub-flow-requirements.md @@ -0,0 +1,47 @@ +--- +date: 2026-05-04 +topic: collection-first-sub-flow +--- + +# 组合订阅单条订阅流量透传 + +## Summary + +在组合订阅编辑页增加一个集合级开关,用来控制是否透传单条订阅的流量信息,并保持现有默认开启行为。 + +## Problem Frame + +组合订阅当前默认透传第一个单条订阅的流量信息。用户如果不希望继承这份流量信息,需要一个可见、可按组合订阅单独配置的控制项,而不是依赖隐藏数据修改或脚本。 + +## Requirements + +- R1. 组合订阅编辑页展示一个名为“透传单条订阅流量信息”的开关。 +- R2. 新建组合订阅时默认开启;已有组合订阅未存储该值时也按开启处理。 +- R3. 开关开启时,组合订阅下载继续透传第一个单条订阅的流量信息。 +- R4. 开关关闭时,组合订阅下载不透传单条订阅流量信息,但组合订阅自身手动配置的流量信息仍然生效。 +- R5. 开关提供 tips,说明默认透传第一个单条订阅流量信息;如需合并组合订阅中所有单条订阅的流量,可使用 https://t.me/zhetengsha/3070 的脚本。 +- R6. tips 包含“查看”按钮,点击后在新窗口打开 https://t.me/zhetengsha/3070。 + +## Acceptance Examples + +- AE1. **Covers R1, R2.** 用户新建组合订阅时,编辑页可见该开关,并且开关处于开启状态。 +- AE2. **Covers R2, R3.** 已有组合订阅未存储该配置时,下载行为保持现状,继续透传第一个单条订阅的流量信息。 +- AE3. **Covers R4.** 组合订阅关闭该开关后,下载时不透传单条订阅流量信息。 +- AE4. **Covers R5, R6.** 用户打开开关 tips 后,点击“查看”会打开 Telegram 脚本链接。 + +## Success Criteria + +- 用户能在组合订阅编辑页直接发现并修改流量透传行为。 +- 旧组合订阅在用户主动关闭开关前保持原有行为。 +- 需求边界足够清晰,不需要规划或评审时再发明内置合并所有单条订阅流量的行为。 + +## Scope Boundaries + +- 不内置实现合并所有单条订阅流量信息。 +- 不修改单条订阅编辑页。 +- 不重构现有流量信息处理链路。 + +## Key Decisions + +- 复用后端已有行为字段作为组合订阅开关,前端只暴露当前运行时能力,不引入并行配置。 +- 默认开启,以保证向后兼容。 diff --git a/index.html b/index.html index 62c6d2cf4..08cd4d09e 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,7 @@ + +
+ // import GlobalNotify from '@/components/GlobalNotify.vue'; +import SideBar from "@/components/SideBar.vue"; import NavBar from "@/components/NavBar.vue"; import MagicPathDialog from "@/components/MagicPathDialog.vue"; +import LogsOverlay from "@/components/LogsOverlay.vue"; +import { useWideScreenNarrowMode } from "@/hooks/useWideScreenNarrowMode"; import { useThemes } from "@/hooks/useThemes"; import { useGlobalStore } from "@/store/global"; import { useSubsStore } from "@/store/subs"; import { getFlowsUrlList } from "@/utils/getFlowsUrlList"; import { initStores } from "@/utils/initApp"; import { storeToRefs } from "pinia"; -import { ref, watchEffect, onMounted } from "vue"; +import { ref, watchEffect, onMounted, computed } from "vue"; import { useHostAPI } from "@/hooks/useHostAPI"; //onMounted import { useRoute, useRouter } from "vue-router"; +import { useI18n } from "vue-i18n"; const subsStore = useSubsStore(); const globalStore = useGlobalStore(); const route = useRoute(); const router = useRouter(); +const { t } = useI18n(); +const { shouldShowSideBar } = useWideScreenNarrowMode(); const { subs, flows } = storeToRefs(subsStore); @@ -66,7 +74,8 @@ globalStore.setBottomSafeArea( const { handleUrlQuery } = useHostAPI(); const urlApiConfigSuccess = ref(false); -const urlApiError = ref(''); +const urlApiErrorKey = ref(''); +const urlApiError = computed(() => urlApiErrorKey.value ? t(urlApiErrorKey.value) : ''); const urlApiValue = ref(''); const processUrlApiConfig = async () => { @@ -96,14 +105,14 @@ const processUrlApiConfig = async () => { if (hasApiParam) { const apiValue = decodeURIComponent(hasApiParam[1]).replace(/\/$/, ''); // 去除末尾斜杠; urlApiValue.value = apiValue; - urlApiError.value = '通过 URL 参数指定的 API 地址连接失败,请检查地址是否正确'; + urlApiErrorKey.value = 'magicPath.errors.urlApiConnection'; hasUrlParams = true; } else if (hasMagicPathParam) { const magicPath = decodeURIComponent(hasMagicPathParam[1]); const currentHost = window.location.origin; const apiUrl = `${currentHost}/${magicPath.replace(/^\/+/, '')}`; urlApiValue.value = apiUrl; - urlApiError.value = '通过 URL 参数指定的 magicpath 连接失败,请检查路径是否正确'; + urlApiErrorKey.value = 'magicPath.errors.urlMagicPathConnection'; hasUrlParams = true; } } @@ -158,7 +167,7 @@ const processUrlApiConfig = async () => { if (fetchResult) { // 连接成功,清除错误信息并隐藏弹窗 - urlApiError.value = ''; + urlApiErrorKey.value = ''; showMagicPathDialog.value = false; } else if (hasUrlParams && skippedCycle !== connectionCheckCycle.value) { // 连接失败但已添加到后端列表,显示错误信息 diff --git a/src/api/archive/index.ts b/src/api/archive/index.ts new file mode 100644 index 000000000..100f00169 --- /dev/null +++ b/src/api/archive/index.ts @@ -0,0 +1,33 @@ +import type { AxiosPromise } from 'axios'; + +import request from '@/api'; + +export function useArchiveApi() { + return { + getEntries: (): AxiosPromise => { + return request({ + url: '/api/archives', + method: 'get', + }); + }, + restoreEntry: (id: string): AxiosPromise => { + return request({ + url: `/api/archives/${encodeURIComponent(id)}/restore`, + method: 'post', + }); + }, + deleteEntry: (id: string): AxiosPromise => { + return request({ + url: `/api/archives/${encodeURIComponent(id)}`, + method: 'delete', + }); + }, + sortEntries: (data: string[]): AxiosPromise => { + return request({ + url: '/api/sort/archives', + method: 'post', + data, + }); + }, + }; +} diff --git a/src/api/artifacts/index.ts b/src/api/artifacts/index.ts index 96ff46567..c12539536 100644 --- a/src/api/artifacts/index.ts +++ b/src/api/artifacts/index.ts @@ -35,10 +35,14 @@ export function useArtifactsApi() { data, }); }, - deleteArtifact: (name: string): AxiosPromise => { + deleteArtifact: ( + name: string, + mode?: DeleteMode, + ): AxiosPromise => { return request({ url: `/api/artifact/${encodeURIComponent(name)}`, method: 'delete', + params: mode ? { mode } : undefined, }); }, syncAllArtifact: (): AxiosPromise => { diff --git a/src/api/env/index.ts b/src/api/env/index.ts index 60e9bf0b5..2df448aa3 100644 --- a/src/api/env/index.ts +++ b/src/api/env/index.ts @@ -6,14 +6,19 @@ export function useEnvApi() { const localStorageKey = 'envCache'; // env 读取加入缓存 重启会自动清理 return { - getEnv: (): AxiosPromise => { - const cachedData = localStorage.getItem(localStorageKey); + getEnv: (options?: { bypassCache?: boolean }): AxiosPromise => { + const bypassCache = options?.bypassCache === true; + const cachedData = bypassCache ? null : localStorage.getItem(localStorageKey); - if (cachedData) { - const parsedCachedData = JSON.parse(cachedData); + if (cachedData && !bypassCache) { + try { + const parsedCachedData = JSON.parse(cachedData); - if (parsedCachedData.expiry > Date.now()) { - return Promise.resolve(parsedCachedData.data); + if (parsedCachedData.expiry > Date.now()) { + return Promise.resolve(parsedCachedData.data); + } + } catch (error) { + console.error('Failed to parse env cache', error); } } diff --git a/src/api/files/index.ts b/src/api/files/index.ts index 2a798f2f3..84309e3cf 100644 --- a/src/api/files/index.ts +++ b/src/api/files/index.ts @@ -41,10 +41,14 @@ export function useFilesApi() { data, }); }, - deleteFile: (name: string): AxiosPromise => { + deleteFile: ( + name: string, + mode?: DeleteMode, + ): AxiosPromise => { return request({ url: `/api/file/${encodeURIComponent(name)}`, method: 'delete', + params: mode ? { mode } : undefined, }); }, }; diff --git a/src/api/index.ts b/src/api/index.ts index d37a9b6a7..58439fb02 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -28,28 +28,27 @@ service.interceptors.response.use( if (e.config.url.startsWith('/api/sub/flow') || e.config.url.startsWith('https://api.github.com/')) return Promise.resolve(e.response); - if (appNotifyStore) { - // 如果是网络错误,则提示网络错误 - if (e.response.status === 0) { - appNotifyStore.showNotify({ - title: '网络错误或后端异常,无法连接后端服务\n', - content: 'code: ' + e.response.status + ' msg: ' + e.message, - ...notifyConfig, - }); - return Promise.reject(e.response); - } else { - let content = 'type: ' + e.response.data.error?.type; - if (e.response.data.error?.details) - content += '\n' + e.response.data.error.details; - appNotifyStore.showNotify({ - title: e.response.data.error?.message, - content, - ...notifyConfig, - }); - return Promise.resolve(e.response); - } - } else { + if (!appNotifyStore) appNotifyStore = useAppNotifyStore(); + + // 如果是网络错误,则提示网络错误 + if (e.response.status === 0) { + appNotifyStore.showNotify({ + title: '网络错误或后端异常,无法连接后端服务\n', + content: 'code: ' + e.response.status + ' msg: ' + e.message, + ...notifyConfig, + }); + return Promise.reject(e.response); + } else { + let content = 'type: ' + e.response.data.error?.type; + if (e.response.data.error?.details) + content += '\n' + e.response.data.error.details; + appNotifyStore.showNotify({ + title: e.response.data.error?.message, + content, + ...notifyConfig, + }); + return Promise.resolve(e.response); } } ); diff --git a/src/api/logs/index.ts b/src/api/logs/index.ts new file mode 100644 index 000000000..25725ddaf --- /dev/null +++ b/src/api/logs/index.ts @@ -0,0 +1,21 @@ +import request from '@/api'; +import { AxiosPromise } from 'axios'; + +export function useLogsApi() { + return { + getLogs: (params: DebugLogQuery = {}): AxiosPromise => { + return request({ + url: '/api/logs', + method: 'get', + params, + }); + }, + clearLogs: (params?: { traceId?: string }): AxiosPromise => { + return request({ + url: '/api/logs', + method: 'delete', + params, + }); + }, + }; +} diff --git a/src/api/share/index.ts b/src/api/share/index.ts index 82bd8d9a6..24f46ebbf 100644 --- a/src/api/share/index.ts +++ b/src/api/share/index.ts @@ -11,10 +11,16 @@ export function useShareApi() { data, }); }, - deleteShare: (token: string): AxiosPromise => { + deleteShare: ( + token: string, + type: string, + name: string, + mode?: DeleteMode, + ): AxiosPromise => { return request({ url: `/api/token/${encodeURIComponent(token)}`, method: "delete", + params: mode ? { type, name, mode } : { type, name }, }); }, getShares: (type?: string, name?: string): AxiosPromise => { @@ -24,5 +30,12 @@ export function useShareApi() { params: { type, name }, }); }, + sortShares: (data: string[]): AxiosPromise => { + return request({ + url: `/api/sort/tokens`, + method: "post", + data, + }); + }, }; } diff --git a/src/api/subs/index.ts b/src/api/subs/index.ts index ee1eb3c8e..9b960bf1c 100644 --- a/src/api/subs/index.ts +++ b/src/api/subs/index.ts @@ -43,7 +43,7 @@ export function useSubsApi() { }, createSub: ( type: string, - data: Sub | Collection + data: Sub | Collection, ): AxiosPromise => { return request({ url: `/api/${type}`, @@ -62,10 +62,15 @@ export function useSubsApi() { data, }); }, - deleteSub: (type: string, name: string): AxiosPromise => { + deleteSub: ( + type: string, + name: string, + mode?: DeleteMode, + ): AxiosPromise => { return request({ url: `/api/${type}/${encodeURIComponent(name)}`, method: 'delete', + params: mode ? { mode } : undefined, }); }, compareSub: ( diff --git a/src/assets/icons/concurrency.svg b/src/assets/icons/concurrency.svg new file mode 100644 index 000000000..a20ea37a7 --- /dev/null +++ b/src/assets/icons/concurrency.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/headersCacheTtl.svg b/src/assets/icons/headersCacheTtl.svg new file mode 100644 index 000000000..df8390e54 --- /dev/null +++ b/src/assets/icons/headersCacheTtl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/logsMaxCount.svg b/src/assets/icons/logsMaxCount.svg new file mode 100644 index 000000000..3bbcfd44f --- /dev/null +++ b/src/assets/icons/logsMaxCount.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/resourceCacheTtl.svg b/src/assets/icons/resourceCacheTtl.svg new file mode 100644 index 000000000..215f22bef --- /dev/null +++ b/src/assets/icons/resourceCacheTtl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/scriptCacheTtl.svg b/src/assets/icons/scriptCacheTtl.svg new file mode 100644 index 000000000..2bedb77f9 --- /dev/null +++ b/src/assets/icons/scriptCacheTtl.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/fonts.scss b/src/assets/styles/fonts.scss index 225473145..133852cb8 100644 --- a/src/assets/styles/fonts.scss +++ b/src/assets/styles/fonts.scss @@ -2,8 +2,8 @@ font-family: 'My Roboto'; font-style: normal; font-weight: 400; - src: url('../fonts/roboto-light.eot'); - src: url('../fonts/roboto-light.eot?#iefix') format('embedded-opentype'), + font-display: swap; + src: url('../fonts/roboto-light.woff2') format('woff2'), url('../fonts/roboto-light.woff') format('woff'); } @@ -11,22 +11,22 @@ font-family: 'My Roboto'; font-style: normal; font-weight: bold; - src: url('../fonts/roboto-bold.eot'); - src: url('../fonts/roboto-bold.eot?#iefix') format('embedded-opentype'), + font-display: swap; + src: url('../fonts/roboto-bold.woff2') format('woff2'), url('../fonts/roboto-bold.woff') format('woff'); } @font-face { font-family: 'JB'; - src: url('../fonts/jetbrainsmononl-regular.eot'); - src: url('../fonts/jetbrainsmononl-regular.eot?#iefix') - format('embedded-opentype'), + font-display: swap; + src: url('../fonts/jetbrainsmononl-regular.woff2') format('woff2'), url('../fonts/jetbrainsmononl-regular.woff') format('woff'); } @font-face { font-family: nutui-iconfont; - src:url(../fonts/3x_static_iconfont.ttf)format("woff2"), + font-display: block; + src: url(../fonts/3x_static_iconfont.woff2) format("woff2"), url(../fonts/3x_static_iconfont.woff) format("woff"), url(../fonts/3x_static_iconfont.ttf) format("truetype") }.nutui-iconfont { diff --git a/src/assets/styles/overwritten_css_var.scss b/src/assets/styles/overwritten_css_var.scss index d32bc7759..36056dbe0 100644 --- a/src/assets/styles/overwritten_css_var.scss +++ b/src/assets/styles/overwritten_css_var.scss @@ -91,6 +91,16 @@ img.auto-reverse { filter: brightness(var(--img-brightness)); } +// Keep editor top preview images aligned with the page theme while they reload. +.sticky-title-icon-container { + .nut-image { + .nut-img-loading, + .nut-img-error { + background-color: var(--background-color); + } + } +} + // dialog .auto-dialog { background-color: var(--dialog-color) !important; @@ -162,4 +172,4 @@ textarea::-webkit-input-placeholder { } .fa-circle-question, .fa-location-arrow { cursor: pointer; -} \ No newline at end of file +} diff --git a/src/assets/styles/reduced-motion-fix.scss b/src/assets/styles/reduced-motion-fix.scss new file mode 100644 index 000000000..ae89d625d --- /dev/null +++ b/src/assets/styles/reduced-motion-fix.scss @@ -0,0 +1,40 @@ +/** + * 修复 modern-css-reset 在 prefers-reduced-motion 时的问题 + * + * 问题:modern-css-reset 将动画/过渡时间设置为 0.01ms,这会导致: + * 1. Notify 通知极快闪现 + * 2. 代码编辑器 cmView 滚动不跟手 + * 详见:https://github.com/sub-store-org/Sub-Store/issues/486 + * 解决方案:让动画和过渡完全禁用,针对性处理Notify和代码编辑器样式 + */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0s !important; + animation-iteration-count: 1 !important; + transition-duration: 0s !important; + scroll-behavior: auto !important; + } + + /* 需要保持动画的元素:添加 .keep-motion 类或使用特定前缀 */ + .keep-motion, + .keep-motion *, + [class^="Toastify"], + [class*=" Toastify"], + [class^="nut-toast"], + [class^="nut-overlay"] { + animation-duration: 0.5s !important; + transition-duration: 0.5s !important; + } + + // NutUI Toast.loading 动画 + .nut-toast-loading .nut-toast-icon-wrapper { + animation: rotation 1s linear infinite !important; + } + + .cmviewRef, + .cm-scroller { + scroll-behavior: auto !important; + } +} diff --git a/src/changelogs/2023-08-15.md b/src/changelogs/2023-08-15.md deleted file mode 100644 index 656279c48..000000000 --- a/src/changelogs/2023-08-15.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -date: 2023-08-15 ---- - -- 更新组件库 NutUI v3.3.8、Picker 问题修复 -- 解决反复重启并发消耗资源、降低资源占用内存 -- 移除:Monaco-Editor 组件 - -- 添加新主题、Simple 模式 主页与管理页 -- 自定义后端 HostAPI 更多使用场景 - -- 优化侧滑返回容易失效的问题 -- 修复左右滑动组件的时候 没有阻止会上下滑动 容易误触 -- 修复拖动卡片的时会把快捷方式跟着拖动 -- 脚本操作、正则操作、等平铺放置更容易添加操作 -- 点击订阅左边的图标才会预览,防止误触预览节点 -- 首页订阅页面:卡片左滑呼出快捷方式,可设置右滑呼出。ㅤ ㅤ -- 点击卡片空白处可关闭当前滑块。添加编辑方便修改 - -- 改进 Service Worker 通过将资源预缓存,更快、流畅地加载ㅤ ㅤ -- 网络连接稳定或不可用时仍能够访问程序 -- 增加预览时候的 V2Ray 入口 -- 新增长按卡片拖动排序,前端需 v2.14.6+ 后端 v2.14.13+ -- 首页订阅页面图标默认为黑白,可自定义开启图标为彩色 diff --git a/src/changelogs/2023-08-23.md b/src/changelogs/2023-08-23.md deleted file mode 100644 index 2baec1acb..000000000 --- a/src/changelogs/2023-08-23.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -date: 2023-08-23 ---- - -### 新增开关项 -- 订阅编辑页面 常用配置板块 默认开启 -- 添加简洁模式 卡片开启刷新按钮 默认关闭 \ No newline at end of file diff --git a/src/changelogs/2023-08-27.md b/src/changelogs/2023-08-27.md deleted file mode 100644 index b8d0440e1..000000000 --- a/src/changelogs/2023-08-27.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -date: 2023-08-27 ---- - -### 修复 - -- 修复了当隐藏同步页面时 tab bar 的 active 状态错误 - -### 优化 - -- 优化了订阅列表复制预览等按钮的操作逻辑 - -### 功能 - -- 域名解析服务增加了 Ali 和 Tencent (要求: 后端 >= 2.14.33) -- 新增了后端 API 管理页面,可以方便的在多个后端之间切换 -- 新增了在 url 查询参数传入 api 时可以自动将其加入后端 API 列表并切换至该 API diff --git a/src/changelogs/2023-08-28.md b/src/changelogs/2023-08-28.md deleted file mode 100644 index f95c01364..000000000 --- a/src/changelogs/2023-08-28.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -date: 2023-08-28 ---- - -### 功能 - -- 输出支持 Clash.Meta (要求: 后端 >= 2.14.35) - -### UI - -- 修复错位,标题栏偏移 \ No newline at end of file diff --git a/src/changelogs/2023-09-02.md b/src/changelogs/2023-09-02.md deleted file mode 100644 index f46bd9f22..000000000 --- a/src/changelogs/2023-09-02.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -date: 2023-10-26 ---- - -- 拖动排序失败回退 -- 检测后端版本启用 name 排序接口 -- 我的页面取消禁止滑动 -- 修复重复点击多次保存会提示重复的问题 \ No newline at end of file diff --git a/src/changelogs/2023-10-31.md b/src/changelogs/2023-10-31.md deleted file mode 100644 index e82ab7c50..000000000 --- a/src/changelogs/2023-10-31.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -date: 2023-10-31 ---- - -- 修复订阅/组合编辑编辑报错不显示的问题 -- 修复动态添加节点后无法预览的问题 \ No newline at end of file diff --git a/src/changelogs/2023-11-08.md b/src/changelogs/2023-11-08.md deleted file mode 100644 index 0efe51000..000000000 --- a/src/changelogs/2023-11-08.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -date: 2023-11-08 ---- - -- 为同步订阅项增加了点击预览来源订阅的功能(便于查看确认来源订阅是否正常) \ No newline at end of file diff --git a/src/changelogs/2023-11-25.md b/src/changelogs/2023-11-25.md deleted file mode 100644 index 3998d2c11..000000000 --- a/src/changelogs/2023-11-25.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -date: 2023-11-25 ---- - -- 支持设置默认 User-Agent(后端版本应 > 2.14.101) \ No newline at end of file diff --git a/src/changelogs/2024-01-12.md b/src/changelogs/2024-01-12.md deleted file mode 100644 index 041eb3f41..000000000 --- a/src/changelogs/2024-01-12.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -date: 2024-01-12 ---- - -### 阶段性更新汇总 - -- 同步配置支持文件(后端 > 2.14.144) - -- 新增文件功能 - -- 默认隐藏悬浮刷新按钮(可在设置中切换) - -- 支持 Surfboard - -- 节点操作中的正则支持拖拽排序 - -- 支持忽略失败的远程订阅 - -- 手动下载备份文件和使用备份上传恢复 - -- 支持按顺序合并本地和远程订阅 - -- 远程订阅支持换行输入多个订阅 - -- 预览节点时增加 JSON 数据便于查看复制 \ No newline at end of file diff --git a/src/changelogs/2024-01-29.md b/src/changelogs/2024-01-29.md deleted file mode 100644 index 37a534e13..000000000 --- a/src/changelogs/2024-01-29.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -date: 2024-01-29 ---- - -### UI - -- 优化 iOS PWA 下状态栏样式 - -- 适配桌面端左右滑动 diff --git a/src/changelogs/2024-04-23.md b/src/changelogs/2024-04-23.md deleted file mode 100644 index 1c77349fa..000000000 --- a/src/changelogs/2024-04-23.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -date: 2024-04-23 ---- - -### 阶段性更新汇总 - -此处的更新可能不完整不及时 - -Telegram 频道 https://t.me/cool_scripts 内有每次更新的详细说明 - -- 处理手动删除 Gist 之后, Sub-Store 侧重新同步的逻辑 - -- QX 输出正式支持 VLESS - -- 节点操作支持单项展开/收起 - -- 支持 Loon SOCKS5/SOCKS5-TLS - -- 支持 WireGuard URI 输入和输出 - -- 支持 dialer-proxy, detour - -- fancy-characters 增加 modifier-letter(小写没有 q, 用 ᵠ 替代. 大写缺的太多, 用小写替代) - -- 订阅支持输出哪吒探针兼容响应, 网络监控接口(Loon/Surge 可输出节点延迟) - -- 节点操作增加收起/展开按钮, 方便处理多个操作 - -- 远程订阅 URL 新增参数 validCheck 将检查订阅有效期和剩余流量 - -- 增加脚本说明文档链接 - -- HTML 中增加 version meta - -- 支持导出操作的数据到剪贴板, 从剪贴板数据导入操作 - -- 设置展示订阅进度 - -- 增加提示文案(部分浏览器上 HTTPS 前端无法访问本地 HTTP 后端) - -- 恢复数据后 重新加载页面 - -- 旗帜操作(支持更多选项) - -- 协议类型筛选支持 SSH - -- 增加 User-Agent 说明文案 - -- 预览界面显示保留/过滤节点数量并可跳转 - -- 预览界面增加端口 - -- 域名解析新增 IP4P, 支持禁用缓存 - -- 增加下载缓存阈值 - -- 文件预览界面增加复制预览内容按钮 - -- 订阅列表的流量信息兼容远程和本地合并的情况, 排除设置了不查询订阅信息的链接 - -- 支持显示剩余重置天数 - -- 支持自定义订阅流量信息 - -- 通过代理/节点/策略获取订阅 现已支持 Surge, Loon, Stash, Shadowrocket, QX, Node.js - -- 支持设置查询远程订阅流量信息时的 User-Agent - -- 支持收起/展开订阅 - -- 订阅支持标签分组 - -- 组合订阅中的子订阅支持分组筛选(仅支持筛选, 未实现动态绑定一个分组的所有订阅) - -- 支持参数 hideExpire 隐藏到期时间 - -- 订阅链接支持 `showRemaining` 参数, 此时将显示剩余流量而不是已用流量 - -- 支持设置并在远程订阅失败时读取最近一次成功的缓存 diff --git a/src/changelogs/2024-12-15.md b/src/changelogs/2024-12-15.md deleted file mode 100644 index 5a35c0fde..000000000 --- a/src/changelogs/2024-12-15.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -date: 2024-12-15 ---- - -### 阶段性更新汇总 - -此处的更新可能不完整不及时 - -Telegram 频道 https://t.me/cool_scripts 内有每次更新的详细说明 - -#### 核心功能增强 - -1. 协议支持 - -- 新增支持 Egern 输出 - -- 支持 VLESS mKcp - -- 支持 Juicity, SSH 等新协议 - -- 支持 Trojan, VMess, VLESS httpupgrade - -2. 解析优化 - -- 修复各类 URI 解析问题(SS, VMess, Trojan等) - -- 改进域名解析功能,支持自定义 EDNS 和 DoH - -- 优化无效节点处理逻辑 - -3. 代理与策略 - -- 新增全局代理/策略设置 - -- 支持文件和链接设置代理/策略 - -- 支持 direct 类型节点 - -#### 功能改进 - -1. 缓存与性能 - -- 默认缓存阈值调整为 1024KB - -- 默认超时调整为 8000ms - -- 优化乐观缓存逻辑 - -2. 订阅管理 - -- 支持订阅流量信息自定义 - -- 改进订阅刷新机制 - -- 支持定时处理订阅功能 - -3. 安全性 - -- 支持 JWT 和 token 管理 - -- 支持自定义 share token - -- 支持 insecure 模式不验证证书 - -#### UI/UX 改进 - -- 新增图标仓库功能 - -- 支持标签管理和排序 - -- 改进分享功能,支持二维码 - -- 优化界面样式和交互 - -#### 其他 - -- 增加更多的兼容性支持 - -- 修复各类 bug - -- 完善文档和说明 - -- 支持更多的配置选项 - -这些更新显著提升了软件的功能完整性、稳定性和用户体验。 diff --git a/src/changelogs/2025-03-02.md b/src/changelogs/2025-03-02.md deleted file mode 100644 index 58811fead..000000000 --- a/src/changelogs/2025-03-02.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -date: 2025-03-02 ---- - -### 阶段性更新汇总 - -此处的更新可能不完整不及时 - -Telegram 频道 https://t.me/cool_scripts 内有每次更新的详细说明 - -功能新增 - - • 区域过滤和协议过滤支持保留模式和过滤模式(后端需 >= 2.17.0, 前端需 >= 2.15.0) - - • 订阅管理 - - • 支持环境变量 SUB_STORE_PRODUCE_CRON 在后台定时处理订阅。 - - • 订阅支持开关 passThroughUA 透传请求的 User-Agent。 - - • 组合订阅支持手动设置流量信息,可使用链接,响应内容即为流量信息。 - - • 远程订阅支持透传请求的 User-Agent。 - - • 协议支持 - - • sing-box 及 Egern 支持 anytls 协议。 - - • Egern 和 Stash 可根据 User-Agent 自动包含官方/商店版/未续费订阅不支持的协议。 - - • Egern 正式支持 Shadowsocks 2022。 - - • Loon 正式支持 Shadowsocks 2022 和 Shadow-TLS。 - - • Surge 默认开启 Shadowsocks 2022。 - - • sing-box 及 Egern 支持 Hysteria2 端口跳跃。 - - • VLESS 支持 spx 参数;Trojan 支持 REALITY/XHTTP 结合使用。 - - • Proxy URI Scheme 支持省略端口号(HTTP 默认 80,TLS 默认 443)。 - - • Shadowrocket 的 Shadowsocks 输入支持 Shadow TLS 参数。 - - • Egern 支持 prev_hop 前置代理。 - - • Mihomo 配置 - - • Mihomo 配置支持 覆写 多次使用。 - - • Mihomo 配置的 Snell 版本 < 3 时,强制去除 udp 字段以防止内核报错。 - - • Mihomo 配置文件支持 流量信息链接 设置。 - -优化改进 - - • 解析 & 兼容性 - - • 修复 Shadowsocks URI 解析逻辑,支持 Shadow TLS plugin。 - - • UUID 仅辅助判断,不直接过滤;VMess/VLESS 校验 UUID。 - - • 兼容 v2rayN 非标 TUIC URI,并支持更多 TUIC URI 字段。 - - • Egern 增加默认 SNI。 - - • Loon 排除 XTLS。 - - • 界面优化 - - • 预览界面: - - • 复制分享链接优化,新增一键复制按钮。 - - • 订阅管理界面顶部标签栏始终显示,增加 PWA 判断。 - - • 修复宽屏设备下节点信息面板二维码样式问题。 - - • 文件管理 - - • target 名称适配大小写和别名。 - - • Mihomo 配置中订阅名称选取交互优化。 - - • 订阅流量信息去除空字段,增强兼容性。 - -修复 - - • 修复 Surge 输入的 tfo。 - - • 修复 Loon ip-mode 逻辑。 - - • 修复 Egern VMess tcp 传输层问题。 - - • 修复 TUIC URI 解析问题。 - - • 修复 组合订阅 透传 User-Agent 逻辑。 - - • 修复 Base64 解码合法性判断。 - - • 修复 Clash Pre-processor 逻辑。 - - • 修复 短 ID 正则匹配 问题。 - - • 修复 代理 App 版 target 参数为空的情况。 - -其他 - - • geo 数据更新。 - - • README 文档调整。 - - • pnpm 依赖更新,构建方式调整(使用 esbuild)。 - - • GitHub Actions 流水线优化。 - -这一版本包含了大量新增特性和修复,建议所有用户尽快更新! 🚀 \ No newline at end of file diff --git a/src/components/ArchiveListItem.vue b/src/components/ArchiveListItem.vue new file mode 100644 index 000000000..09a549bca --- /dev/null +++ b/src/components/ArchiveListItem.vue @@ -0,0 +1,539 @@ + + + + + diff --git a/src/components/ArtifactPanel.vue b/src/components/ArtifactPanel.vue index d6b58a44b..282e1e029 100644 --- a/src/components/ArtifactPanel.vue +++ b/src/components/ArtifactPanel.vue @@ -23,7 +23,7 @@ v-model.trim="editPanelData.icon" type="text" left-icon="shop" - @click-left-icon="iconTips" + @click-left-icon="showIconPopup" /> diff --git a/src/components/FileListItem.vue b/src/components/FileListItem.vue index 409484da1..c8b417c71 100644 --- a/src/components/FileListItem.vue +++ b/src/components/FileListItem.vue @@ -2,6 +2,7 @@
@@ -46,16 +48,22 @@

- {{ displayName || name }} + {{ displayName }} + + {{ i }} +

- {{ displayName || name }} + {{ displayName }} + + {{ i }} +

@@ -256,6 +277,7 @@ style="display: none" />
- -
- - - {{ $t(`filePage.ignoreFailedRemoteFile.disabled`) }} - - - {{ $t(`filePage.ignoreFailedRemoteFile.quiet`) }} - - - {{ $t(`filePage.ignoreFailedRemoteFile.enabled`) }} - - - -
+ @@ -371,7 +385,7 @@
@@ -380,12 +394,14 @@ v-if="filePreviewIsVisible" :name="configName" :previewData="previewData" + :showRefresh="true" @closePreview="closePreview" + @refresh="refreshPreview" /> - + -

{{ t(`editorPage.subConfig.sourceNamePicker.emptyTips`) }}

- + + + + diff --git a/src/views/My.vue b/src/views/My.vue index 5df948e0d..07bb21fd4 100644 --- a/src/views/My.vue +++ b/src/views/My.vue @@ -3,7 +3,9 @@
{{$t(`myPage.storage.${i.value}.label`) }} -

{{ $t(`myPage.storage.${storageType}.info`) }}

+
+ +
+ + + +
+
+
+
+

{{ $t(`myPage.requestConfig`) }}

+
+ + + {{ $t(`myPage.btn.cancel`) }} + + + + + {{ !isRequestConfigEditing ? $t(`myPage.btn.edit`) : $t(`myPage.btn.save`) }} + + +
+
+
+ +
+
+
+
+

{{ $t(`myPage.cacheConfig`) }}

+
+ + + {{ $t(`myPage.btn.cancel`) }} + + + + + {{ !isCacheConfigEditing ? $t(`myPage.btn.edit`) : $t(`myPage.btn.save`) }} + + +
+
+
+ + + + + +
+
+
+
+

{{ $t(`myPage.frontEndConfig`) }}

+
+ + + {{ $t(`myPage.btn.cancel`) }} + + + + + {{ !isFrontEndConfigEditing ? $t(`myPage.btn.edit`) : $t(`myPage.btn.save`) }} + + +
+
+
+ + + +
- + + + + + + + + + + @@ -263,7 +519,7 @@
- + { - return !githubUser.value ? avatar : avatarUrl.value; +const DEFAULT_GITHUB_API_URL = "https://api.github.com"; +const avatarLoadFailed = ref(false); +const avatarRenderNonce = ref(0); + +const fallbackGithubAvatarUrl = computed(() => { + const normalizedGithubUser = githubUser.value?.trim(); + + if (!normalizedGithubUser) { + return ""; + } + + return `https://github.com/${encodeURIComponent(normalizedGithubUser)}.png`; +}); + +const remoteAvatarUrl = computed(() => { + if (!githubUser.value?.trim() || avatarLoadFailed.value) { + return ""; + } + + return fallbackGithubAvatarUrl.value; +}); + +const gistProfileTitle = computed(() => { + const normalizedGithubUser = githubUser.value?.trim(); + + if (normalizedGithubUser) { + return normalizedGithubUser; + } + + return gistToken.value ? t(`myPage.storage.gist.label`) : t(`myPage.placeholder.name`); +}); + +const displayAvatarIcon = computed(() => { + return remoteAvatarUrl.value ? "" : avatar; +}); + +const isAvatarFallback = computed(() => { + return !remoteAvatarUrl.value; +}); + +const avatarDisplayKey = computed(() => { + return `${remoteAvatarUrl.value || "fallback"}:${avatarRenderNonce.value}`; +}); + +const resetAvatarState = (forceRerender = false) => { + avatarLoadFailed.value = false; + + if (forceRerender) { + avatarRenderNonce.value += 1; + } +}; + +const handleAvatarError = () => { + if (!remoteAvatarUrl.value) { + return; + } + + avatarLoadFailed.value = true; + avatarRenderNonce.value += 1; +}; + +watch([githubUser], () => { + resetAvatarState(true); }); const { icon, env } = useBackend(); +const githubUrlRewriter = computed(() => { + return createGithubProxyUrlRewriter(githubProxy.value, githubProxyRegex.value); +}); +const displayBackendIcon = computed(() => { + return githubUrlRewriter.value( + env.value?.meta?.node?.env?.SUB_STORE_BACKEND_CUSTOM_ICON || icon.value, + ) || icon.value; +}); const shareBtnVisible = computed(() => { return env.value?.feature?.share; }); +const archiveVisible = computed(() => { + return env.value?.feature?.archive; +}); const onClickAPISetting = () => { router.push(`/settings/api`); }; +const onClickLogs = () => { + router.push(`/logs`); +}; const onClickShareManage = () => { router.push(`/shares`); }; +const onClickArchive = () => { + router.push(`/archives`); +}; const onClickMore = () => { router.push(`/settings/more`); }; @@ -344,47 +685,151 @@ const syncPlatformInput = ref(""); const userInput = ref(""); const tokenInput = ref(""); const githubProxyInput = ref(""); +const githubApiUrlInput = ref(""); +const githubProxyRegexInput = ref(""); const uaInput = ref(""); +const flowUaInput = ref(""); const proxyInput = ref(""); const timeoutInput = ref(""); const cacheThresholdInput = ref(""); -const isEditing = ref(false); +const resourceCacheTtlInput = ref(""); +const headersCacheTtlInput = ref(""); +const scriptCacheTtlInput = ref(""); +const logsMaxCountInput = ref(""); +const concurrencyInput = ref(""); +const apiCheckTimeoutInput = ref(""); +const isGitHubConfigEditing = ref(false); +const isRequestConfigEditing = ref(false); +const isCacheConfigEditing = ref(false); +const isFrontEndConfigEditing = ref(false); const isEditLoading = ref(false); const isInit = ref(false); const storageType = ref('gist'); const fileInput = ref(null); -const toggleEditMode = async () => { +const toggleEditMode = async (type) => { isEditLoading.value = true; - if (isEditing.value) { - await settingsStore.editGistSettings({ - syncPlatform: syncPlatformInput.value, - githubUser: userInput.value, - gistToken: tokenInput.value, - githubProxy: githubProxyInput.value, - defaultUserAgent: uaInput.value, - defaultProxy: proxyInput.value, - defaultTimeout: timeoutInput.value, - cacheThreshold: cacheThresholdInput.value, + try { + if ((type === 'github' && isGitHubConfigEditing.value) || (type === 'request' && isRequestConfigEditing.value) || (type === 'cache' && isCacheConfigEditing.value)) { + const saveSucceeded = await settingsStore.changeSettings({ + syncPlatform: syncPlatformInput.value, + githubUser: userInput.value, + gistToken: tokenInput.value, + githubProxy: githubProxyInput.value, + githubApiUrl: githubApiUrlInput.value, + githubProxyRegex: githubProxyRegexInput.value, + defaultUserAgent: uaInput.value, + defaultFlowUserAgent: flowUaInput.value, + defaultProxy: proxyInput.value, + defaultTimeout: timeoutInput.value, + cacheThreshold: cacheThresholdInput.value, + resourceCacheTtl: resourceCacheTtlInput.value, + headersCacheTtl: headersCacheTtlInput.value, + scriptCacheTtl: scriptCacheTtlInput.value, + logsMaxCount: logsMaxCountInput.value, + }); + + if (saveSucceeded && type === 'github') { + resetAvatarState(true); + await settingsStore.fetchSettings(); + resetAvatarState(true); + } + + if (saveSucceeded) { + setDisplayInfo(); + } + } else { + syncPlatformInput.value = syncPlatform.value; + userInput.value = githubUser.value; + tokenInput.value = gistToken.value; + githubProxyInput.value = githubProxy.value; + githubApiUrlInput.value = githubApiUrl.value || ""; + githubProxyRegexInput.value = githubProxyRegex.value; + uaInput.value = defaultUserAgent.value; + flowUaInput.value = defaultFlowUserAgent.value || ""; + proxyInput.value = defaultProxy.value; + timeoutInput.value = defaultTimeout.value; + cacheThresholdInput.value = cacheThreshold.value; + resourceCacheTtlInput.value = resourceCacheTtl.value; + headersCacheTtlInput.value = headersCacheTtl.value; + scriptCacheTtlInput.value = scriptCacheTtl.value; + logsMaxCountInput.value = logsMaxCount.value; + } + if (type === 'frontEnd' && isFrontEndConfigEditing.value) { + const apiCheckTimeout = Number(apiCheckTimeoutInput.value); + if (!isNaN(apiCheckTimeout)) { + if (apiCheckTimeout > 0) { + console.log(`设置超时 ${apiCheckTimeout}`) + localStorage.setItem('timeout', apiCheckTimeout.toString()); + } else { + console.log(`清除超时设置`) + localStorage.removeItem('timeout'); + } + }else { + console.log(`清除超时设置`) + localStorage.removeItem('timeout'); + } + const concurrency = parseInt(concurrencyInput.value, 10); + if (!isNaN(concurrency)) { + if (concurrency >= 1) { + console.log(`设置并发数 ${concurrency}`) + localStorage.setItem('concurrency', concurrency.toString()); + } else { + console.log(`清除并发数设置`) + localStorage.removeItem('concurrency'); + } + } else { + console.log(`清除并发数设置`) + localStorage.removeItem('concurrency'); + } + setTimeout(() => { + window.location.reload(); + }, 100); + } else { + const storedTimeout = localStorage.getItem('timeout'); + if (storedTimeout) { + apiCheckTimeoutInput.value = storedTimeout; + } else { + apiCheckTimeoutInput.value = ''; + } + const storedConcurrency = localStorage.getItem('concurrency'); + if (storedConcurrency) { + concurrencyInput.value = storedConcurrency; + } else { + concurrencyInput.value = ''; + } + } + if (type === 'github' && !isGitHubConfigEditing.value) { + isGitHubConfigEditing.value = !isGitHubConfigEditing.value; + } else if (type === 'cache' && !isCacheConfigEditing.value) { + isCacheConfigEditing.value = !isCacheConfigEditing.value; + } else if (type === 'request' && !isRequestConfigEditing.value) { + isRequestConfigEditing.value = !isRequestConfigEditing.value; + } else if (type === 'frontEnd' && !isFrontEndConfigEditing.value) { + isFrontEndConfigEditing.value = !isFrontEndConfigEditing.value; + } + } catch (e) { + showNotify({ + title: `更新配置失败`, + type: "danger", }); - setDisplayInfo(); - } else { - syncPlatformInput.value = syncPlatform.value; - userInput.value = githubUser.value; - tokenInput.value = gistToken.value; - githubProxyInput.value = githubProxy.value; - uaInput.value = defaultUserAgent.value; - proxyInput.value = defaultProxy.value; - timeoutInput.value = defaultTimeout.value; - cacheThresholdInput.value = cacheThreshold.value; + console.error(e); + } finally { + isEditLoading.value = false; } - isEditLoading.value = false; - isEditing.value = !isEditing.value; }; -const exitEditMode = () => { +const exitEditMode = (type) => { setDisplayInfo(); - isEditing.value = false; + if (type === 'github') { + isGitHubConfigEditing.value = false; + } else if (type === 'cache') { + isCacheConfigEditing.value = false; + } else if (type === 'request') { + isRequestConfigEditing.value = false; + } else { + isFrontEndConfigEditing.value = false; + } isEditLoading.value = false; }; const toggleSyncPlatform = () => { @@ -417,13 +862,18 @@ const setDisplayInfo = () => { syncPlatformInput.value = syncPlatform.value || ""; userInput.value = githubUser.value || ""; githubProxyInput.value = githubProxy.value || ""; - tokenInput.value = gistToken.value - ? `${gistToken.value.slice(0, 6)}************` - : ""; + githubApiUrlInput.value = githubApiUrl.value || ""; + githubProxyRegexInput.value = githubProxyRegex.value || ""; + tokenInput.value = gistToken.value || ""; uaInput.value = defaultUserAgent.value || ""; + flowUaInput.value = defaultFlowUserAgent.value || ""; proxyInput.value = defaultProxy.value || ""; timeoutInput.value = defaultTimeout.value || ""; cacheThresholdInput.value = cacheThreshold.value || ""; + resourceCacheTtlInput.value = resourceCacheTtl.value || ""; + headersCacheTtlInput.value = headersCacheTtl.value || ""; + scriptCacheTtlInput.value = scriptCacheTtl.value || ""; + logsMaxCountInput.value = logsMaxCount.value ?? ""; }; // 同步 上传 @@ -434,13 +884,12 @@ const syncIsDisabled = computed(() => { return ( uploadIsLoading.value || downloadIsLoading.value || - !gistToken.value || - !githubUser.value + !gistToken.value ); }); const desText = computed(() => { - if (!gistToken.value || !githubUser.value) { + if (!gistToken.value) { return [t(`myPage.placeholder.des`), ""]; } else { if (!syncTime.value) return [t(`myPage.placeholder.haveNotDownload`), ""]; @@ -463,7 +912,23 @@ const fileChange = async (event) => { type: "success", title: t(`myPage.notify.restore.succeed`), }); - window.location.reload() + + if ("serviceWorker" in navigator) { + const registrations = await navigator.serviceWorker.getRegistrations(); + for (let registration of registrations) { + await registration.unregister(); + } + } + if ("caches" in window) { + const cacheNames = await caches.keys(); + for (let cacheName of cacheNames) { + await caches.delete(cacheName); + } + } + + setTimeout(() => { + window.location.reload(); + }, 1000); } else { throw new Error('restore failed') } @@ -517,7 +982,22 @@ const sync = async (query: "download" | "upload", options?: { keep?: string[], e title: t(`myPage.notify.${query}.succeed`), }); if (query === "download") { - window.location.reload() + if ("serviceWorker" in navigator) { + const registrations = await navigator.serviceWorker.getRegistrations(); + for (let registration of registrations) { + await registration.unregister(); + } + } + if ("caches" in window) { + const cacheNames = await caches.keys(); + for (let cacheName of cacheNames) { + await caches.delete(cacheName); + } + } + + setTimeout(() => { + window.location.reload(); + }, 1000); } } @@ -526,26 +1006,28 @@ const sync = async (query: "download" | "upload", options?: { keep?: string[], e }; const uploadBtn = () => { - Dialog({ - title: '请选择', - content: '若选择明文, 将不会保留 GitHub Token. 若选择 Base64 编码, 将完整保留数据(后端版本必须 >= 2.19.85)', - footerDirection: 'vertical', - onCancel: () => { - sync('upload', { - encode: 'plaintext' - }); - }, - cancelText: '明文(将不会保留 GitHub Token)', - okText: 'Base64 编码上传', - onOk: () => { - sync('upload', { - encode: 'base64' - }); - }, - popClass: "auto-dialog", - closeOnPopstate: true, - lockScroll: false, - }); + const encode = gistUpload.value || 'base64'; + sync('upload', { encode }); + // Dialog({ + // title: '请选择', + // content: '若选择明文, 将不会保留 GitHub Token. 若选择 Base64 编码, 将完整保留数据(后端版本必须 >= 2.19.85)', + // footerDirection: 'vertical', + // onCancel: () => { + // sync('upload', { + // encode: 'plaintext' + // }); + // }, + // cancelText: '明文(将不会保留 GitHub Token)', + // okText: 'Base64 编码上传', + // onOk: () => { + // sync('upload', { + // encode: 'base64' + // }); + // }, + // popClass: "auto-dialog", + // closeOnPopstate: true, + // lockScroll: false, + // }); } const downloadBtn = () => { Dialog({ @@ -570,11 +1052,44 @@ const downloadBtn = () => { const githubProxyTips = () => { Dialog({ title: '请填写完整 GitHub 加速代理地址', - content: '后端需 >= 2.19.97\n\n1. 仅用于上传/下载 Gist 和获取 GitHub 头像\n\n2. 请填写完整 如 https://a.com\n\n3. 需支持代理 https://api.github.com/users/* 和 https://api.github.com/gists\n\n测试方式:\n浏览器打开\nhttps://a.com/https://api.github.com/gists?per_page=1&page=1\n和\nhttps://a.com/https://api.github.com/users/xream\n有正常的响应\n\n4. 使用此方式时, 自行注意安全隐私问题', + content: '后端需 >= 2.21.75 才可完整使用下方正则匹配能力\n\n1. 该代理用于上传/下载 GitHub Gist\n\n2. 请填写完整地址, 如 https://a.com\n\n3. 需支持代理 https://api.github.com/gists\n\n4. 若同时配置下方的 GitHub 加速代理匹配正则, 匹配的所有远程资源 URL 会改写为\nhttps://a.com/原始URL\n\n测试方式:\n浏览器打开\nhttps://a.com/https://api.github.com/gists?per_page=1&page=1\n有正常的响应\n\n5. 使用自定义 GitHub API 地址后, GitHub 加速代理不会作用于 Gist API 请求\n\n6. 使用此方式时, 自行注意安全隐私问题', + popClass: 'auto-dialog', + textAlign: 'left', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const githubApiUrlTips = () => { + Dialog({ + title: 'GitHub API 地址', + content: `后端需 >= 2.22.21\n\n1. 默认为 ${DEFAULT_GITHUB_API_URL}\n\n2. 需使用 GitHub Gist 兼容接口\n\n3. 此时 GitHub 令牌里填入的是对应服务的 API Key 之类的\n\n4. 使用自定义 GitHub API 地址后, GitHub 加速代理不会作用于此处\n\n5. 项目推荐:\nEdgeGist: https://github.com/xream/EdgeGist\nLiteGist: https://github.com/lockcp/LiteGist`, popClass: 'auto-dialog', textAlign: 'left', okText: 'OK', + cancelText: '查看 LiteGist', noCancelBtn: true, + // onCancel: () => { + // window.open('https://github.com/lockcp/LiteGist'); + // }, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const githubProxyRegexExample = '^https?:\\/\\/.+\\.(githubusercontent|github)\\.com($|\\/)'; +const githubProxyRegexTips = () => { + Dialog({ + title: '按正则匹配下载链接', + content: `后端需 >= 2.21.75\n\n1. 需先配置上方 GitHub 加速代理, 本项才会生效\n\n2. 影响所有远程资源 URL, 不影响 Gist API 和头像逻辑\n\n3. 默认忽略大小写, 例如\n${githubProxyRegexExample}\n\n4. 会把匹配的下载地址改写为\nhttps://a.com/原始URL\n\n5. 使用此方式时, 自行注意安全隐私问题`, + popClass: 'auto-dialog', + textAlign: 'left', + okText: 'OK', + cancelText: '填入示例正则', + onCancel: () => { + githubProxyRegexInput.value = githubProxyRegexExample; + Toast.text('已填入示例正则,请自行保存', { duration: 3000 }); + }, closeOnPopstate: true, lockScroll: false, }); @@ -582,7 +1097,7 @@ const githubProxyTips = () => { const proxyTips = () => { Dialog({ title: '通过代理/节点/策略进行下载', - content: '1. Surge(参数 policy/policy-descriptor)\n\n可设置节点代理 例: Test = snell, 1.2.3.4, 80, psk=password, version=4\n\n或设置策略/节点 例: 国外加速\n\n2. Loon(参数 node)\n\nLoon 官方文档: \n\n指定该请求使用哪一个节点或者策略组(可以使节点名称、策略组名称,也可以说是一个Loon格式的节点描述,如:shadowsocksr,example.com,1070,chacha20-ietf,"password",protocol=auth_aes128_sha1,protocol-param=test,obfs=plain,obfs-param=edge.microsoft.com)\n\n3. Stash(参数 headers["X-Surge-Policy"])/Shadowrocket(参数 headers.X-Surge-Policy)/QX(参数 opts.policy)\n\n可设置策略/节点\n\n4. Node.js 版(http/https/socks5):\n\n例: socks5://a:b@127.0.0.1:7890\n\n※ 优先级由高到低: 单条订阅, 组合订阅, 默认配置', + content: '1. Surge/Egern(参数 policy/policy-descriptor)\n\n可设置节点代理 例: Test = snell, 1.2.3.4, 80, psk=password, version=4\n\n或设置策略/节点 例: 国外加速\n\n2. Loon(参数 node)\n\nLoon 官方文档: \n\n指定该请求使用哪一个节点或者策略组(可以是节点名称、策略组名称,也可以是一个 Loon 格式的节点描述,如:shadowsocksr,example.com,1070,chacha20-ietf,"password",protocol=auth_aes128_sha1,protocol-param=test,obfs=plain,obfs-param=edge.microsoft.com)\n\n3. Stash(参数 headers["X-Surge-Policy"])/Shadowrocket(参数 headers.X-Surge-Policy)/QX(参数 opts.policy)\n\n可设置策略/节点\n\n4. Node.js 版(http/https/socks5):\n\n例: socks5://a:b@127.0.0.1:7890\n\n※ 优先级由高到低: 单条订阅, 组合订阅, 默认配置\n\n完整说明 请查看 https://t.me/zhetengsha/1843', popClass: 'auto-dialog', textAlign: 'left', okText: 'OK', @@ -594,7 +1109,18 @@ const proxyTips = () => { const uaTips = () => { Dialog({ title: '默认为 clash.meta', - content: '可尝试设置为 clash-verge/v1.5.1 等客户端的 User-Agent 让机场后端下发更多协议', + content: '可尝试设置为 clash-verge/v2.4.6, v2rayNG 等客户端的 User-Agent 让机场后端下发更多协议(可根据实际情况改成最新版本号)。也可在单条订阅里设置单独的 User-Agent', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const flowUaTips = () => { + Dialog({ + title: '查询订阅流量信息 的 User-Agent', + content: '若机场后端不给默认 UA 下发订阅流量信息, 可改为 "Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)"。也可在单条订阅里的远程链接参数里设置单独的 flowUserAgent', popClass: 'auto-dialog', okText: 'OK', noCancelBtn: true, @@ -624,6 +1150,72 @@ const cacheThresholdTips = () => { lockScroll: false, }); }; +const resourceCacheTtlTips = () => { + Dialog({ + title: '资源缓存时间 (秒)', + content: '主要涉及下载订阅/下载脚本/域名解析等功能', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const headersCacheTtlTips = () => { + Dialog({ + title: '响应头缓存时间 (秒)', + content: '主要涉及订阅流量信息等功能', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const scriptCacheTtlTips = () => { + Dialog({ + title: '脚本缓存时间 (秒)', + content: '主要涉及在脚本中使用的 scriptResourceCache 缓存', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const logsMaxCountTips = () => { + Dialog({ + title: '最大保存日志条数', + content: '默认 0,即关闭日志缓存读写。设为大于 0 后,后端会把日志写入持久化缓存;数值越大占用的缓存空间越多,也可能影响性能。', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const concurrencyTips = () => { + Dialog({ + title: '并发数', + content: 'Shadowrocket 并发可能会爆内存, 可设为 1', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; +const apiCheckTimeoutTips = () => { + Dialog({ + title: 'API 检测超时', + content: '某些版本的 Mac 上 QX https://sub.store/api/utils/env 可能会超时, JS 一直活跃中, 可设为 8000', + popClass: 'auto-dialog', + okText: 'OK', + noCancelBtn: true, + closeOnPopstate: true, + lockScroll: false, + }); +}; // store 刷新数据完成后 复制内容给 input 绑定 const { isLoading: storeIsLoading, env: backendEnv } = storeToRefs(useGlobalStore()); watchEffect(() => { @@ -668,23 +1260,23 @@ const setTag = (current) => { border-bottom: 1px solid var(--primary-color); color: var(--primary-color); } - .storage-info { + .storage-language-switch { margin-left: auto; - font-size: 12px; - color: var(--lowest-text-color); + flex-shrink: 0; } } .config-card { - margin-top: 20px; + margin-top: 10px; width: 100%; - padding: 12px; + padding: 6px 12px 6px 6px; border-radius: var(--item-card-radios); color: var(--second-text-color); background: var(--card-color); .title-wrapper { + cursor: pointer; display: flex; justify-content: space-between; align-items: center; @@ -746,15 +1338,23 @@ const setTag = (current) => { justify-content: space-between; align-items: center; padding: 12px 0 0 0; + margin-bottom: 20px; .avatar-wrapper { display: flex; align-items: center; max-width: 64%; - .avatar-normal { - :deep(img) { + :deep(.nut-avatar) { + background: var(--card-color); + } + + .avatar-fallback { + :deep(img), + :deep(.nut-icon__img) { width: 72%; + height: 72%; + object-fit: contain; } } @@ -809,7 +1409,7 @@ const setTag = (current) => { } } - .change-themes { + .right-icon { // color: var(--comment-text-color); box-shadow: none; font-weight: bold; diff --git a/src/views/Sub.vue b/src/views/Sub.vue index f35c097a5..cbe2fe3d2 100644 --- a/src/views/Sub.vue +++ b/src/views/Sub.vue @@ -104,7 +104,7 @@ -
+
@@ -123,7 +123,9 @@ diff --git a/src/views/editor/components/FilterSelect.vue b/src/views/editor/components/FilterSelect.vue index 360a07858..8f99d43aa 100644 --- a/src/views/editor/components/FilterSelect.vue +++ b/src/views/editor/components/FilterSelect.vue @@ -72,7 +72,11 @@ 'hysteria2', 'juicity', 'mieru', + 'sudoku', + 'masque', 'anytls', + 'trusttunnel', + 'tailscale', 'wireguard', 'ssh', 'external', diff --git a/src/views/editor/components/HandleDuplicate.vue b/src/views/editor/components/HandleDuplicate.vue index c5cb34826..9fe9bd94d 100644 --- a/src/views/editor/components/HandleDuplicate.vue +++ b/src/views/editor/components/HandleDuplicate.vue @@ -1,5 +1,44 @@ - - diff --git a/src/views/icon/IconPopup.vue b/src/views/icon/IconPopup.vue index a6ca4bb03..6952cc4e9 100644 --- a/src/views/icon/IconPopup.vue +++ b/src/views/icon/IconPopup.vue @@ -17,90 +17,123 @@ round @close="close" > -
- {{ $t(`iconCollectionPage.iconCollection`) }} -
-
-
-

{{ form.iconCollectionName }}

-

{{ form.iconCollectionDesc }}

-
-
-
- {{ $t(`iconCollectionPage.more`) }} - +
+
+
+ {{ $t(`iconCollectionPage.iconCollection`) }}
-
- {{ $t(`iconCollectionPage.resetDefaultIconCollection`) }} - +
+
+

{{ form.iconCollectionName }}

+

{{ form.iconCollectionDesc }}

+
+
+
+ {{ $t(`iconCollectionPage.more`) }} + +
+
+ {{ $t(`iconCollectionPage.resetDefaultIconCollection`) }} + +
+
+
+
+
+ + + {{ $t(`iconCollectionPage.useCustomIconCollection`) }} + +
+
+
+
+ + + + + + + + +
-
-
-
- - - {{ $t(`iconCollectionPage.useCustomIconCollection`) }} - -
-
-
-
- - - - - - - - -
-
-
-
- -

{{ icon.name }}

+
+
+
+ +

{{ $t(`iconCollectionPage.loadingTitle`) }}

+

{{ $t(`iconCollectionPage.loadingDesc`) }}

+
+
+
+
+ + + + + {{ $t(`iconCollectionPage.retryBtn`) }} + +
+
+
+
+ +

{{ icon.name }}

+
+
+
+ + + +
- - - - + > @@ -137,6 +135,7 @@ :deep(.nut-cell__value) { font-weight: normal; color: var(--primary-color); + cursor: pointer; } .bclass { @@ -166,6 +165,10 @@ margin: 0 var(--safe-area-side); border-radius: var(--item-card-radios); overflow: hidden; + a { + color: var(--primary-color); + font-weight: normal; + } } .about-wrapper { diff --git a/src/views/settings/moreSetting.vue b/src/views/settings/moreSetting.vue index ee84c430f..219844618 100644 --- a/src/views/settings/moreSetting.vue +++ b/src/views/settings/moreSetting.vue @@ -1,250 +1,188 @@ diff --git a/src/views/share/ShareEditorPage.vue b/src/views/share/ShareEditorPage.vue new file mode 100644 index 000000000..4e1d9b0c7 --- /dev/null +++ b/src/views/share/ShareEditorPage.vue @@ -0,0 +1,1651 @@ + + + + + diff --git a/src/views/share/SharePopup.vue b/src/views/share/SharePopup.vue deleted file mode 100644 index 92efd0b0e..000000000 --- a/src/views/share/SharePopup.vue +++ /dev/null @@ -1,640 +0,0 @@ - - - - - - diff --git a/vite.config.ts b/vite.config.ts index 5d505fef7..460527337 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -128,16 +128,16 @@ const viteConfig = defineConfig((mode: ConfigEnv) => { }, }, { - urlPattern: /.*\.(?:png|svg|ico|woff|ttf|eot)/i, + urlPattern: /https:\/\/avatars\.githubusercontent\.com\/u\/\d+(?:\?.*)?$|.*\.(?:png|svg|ico|jpe?g|webp|avif|gif|woff2?|ttf|eot|otf)(?:\?.*)?$/i, handler: "CacheFirst", options: { cacheName: "sub-store-res-cache", expiration: { - maxEntries: 30, + maxEntries: 300, maxAgeSeconds: 60 * 60 * 24 * 365, }, cacheableResponse: { - statuses: [200], + statuses: [0, 200], }, }, },