├── .env ├── public └── web-extension │ ├── _locales │ └── zh_CN │ │ └── messages.json │ ├── logo.png │ └── logo_grey.png ├── src ├── assets │ ├── logo.png │ ├── logo-rect.jpg │ └── logo-rect.png ├── vite-env.d.ts ├── utils │ ├── sleep.ts │ ├── complementZero.ts │ ├── fileNameParse.ts │ ├── render.ts │ ├── reactFiber.ts │ ├── is.ts │ ├── querySelector.ts │ ├── useVersion.ts │ ├── message.ts │ └── icons.ts ├── test │ ├── utils │ │ ├── fileNameParse.test.ts │ │ ├── sleep.test.ts │ │ ├── complementZero.test.ts │ │ ├── message.test.ts │ │ ├── querySelector.test.ts │ │ ├── is.test.ts │ │ └── reactFiber.test.ts │ ├── components │ │ └── Component │ │ │ ├── ComponentRadio.test.ts │ │ │ ├── ComponentCheckbox.test.ts │ │ │ ├── ComponentInput.test.ts │ │ │ └── ComponentIcon.test.ts │ └── provider │ │ ├── getWebSocketDebuggerUrl.ts │ │ ├── openChrome.sh │ │ └── index.test.ts ├── App.vue ├── provider │ ├── index.ts │ ├── lib │ │ ├── pikpak │ │ │ ├── EnterComponent.vue │ │ │ └── index.ts │ │ ├── baidu │ │ │ ├── EnterComponent.vue │ │ │ └── index.ts │ │ ├── ali │ │ │ ├── EnterComponent.vue │ │ │ └── index.ts │ │ └── quark │ │ │ ├── EnterComponent.vue │ │ │ └── index.ts │ └── interface.ts ├── components │ ├── Component │ │ ├── ComponentIcon.vue │ │ ├── ComponentLoading.vue │ │ ├── ComponentCheckbox.vue │ │ ├── ComponentRadio.vue │ │ ├── ComponentInput.vue │ │ └── ComponentMessage.vue │ ├── RenamePanel.vue │ ├── RenameControl.vue │ └── RenamePreview.vue ├── style │ ├── message.css │ ├── transition.css │ └── index.css ├── main.ts └── background.ts ├── assets └── logo_large.png ├── image ├── readme │ ├── screenshot_enter_button.png │ ├── screenshot_enter_button_ali.png │ ├── screenshot_main_panel_series.png │ ├── screenshot_enter_button_baidu.png │ ├── screenshot_enter_button_pikpak.png │ ├── screenshot_enter_button_quark.png │ ├── screenshot_main_panel_pattern.png │ ├── screenshot_main_panel_series_check.png │ ├── screenshot_main_panel_series_error.png │ ├── screenshot_main_panel_series_example.png │ ├── screenshot_main_panel_pattern_example.png │ ├── screenshot_main_panel_sort_checked_example_1.png │ ├── screenshot_main_panel_sort_checked_example_2.png │ └── index.html └── webExtension │ ├── screenshot_enter_button.png │ ├── screenshot_main_panel_pattern.png │ ├── screenshot_main_panel_series.png │ ├── screenshot_main_panel_series_check.png │ ├── screenshot_main_panel_series_error.png │ ├── screenshot_main_panel_pattern_example.png │ ├── screenshot_main_panel_series_example.png │ ├── screenshot_main_panel_sort_checked_example_1.png │ └── screenshot_main_panel_sort_checked_example_2.png ├── .prettierrc.json ├── .env.web-extension-dev ├── .env.web-extension-build ├── .gitignore ├── .env.tamper-monkey ├── .eslintrc.cjs ├── tsconfig.json ├── LICENSE ├── package.json ├── README.md └── vite.config.ts /.env: -------------------------------------------------------------------------------- 1 | VITE_VERSION = "1.3.0" 2 | -------------------------------------------------------------------------------- /public/web-extension/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /assets/logo_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/assets/logo_large.png -------------------------------------------------------------------------------- /src/assets/logo-rect.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/src/assets/logo-rect.jpg -------------------------------------------------------------------------------- /src/assets/logo-rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/src/assets/logo-rect.png -------------------------------------------------------------------------------- /public/web-extension/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/public/web-extension/logo.png -------------------------------------------------------------------------------- /public/web-extension/logo_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/public/web-extension/logo_grey.png -------------------------------------------------------------------------------- /image/readme/screenshot_enter_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_enter_button.png -------------------------------------------------------------------------------- /image/readme/screenshot_enter_button_ali.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_enter_button_ali.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_series.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_series.png -------------------------------------------------------------------------------- /image/readme/screenshot_enter_button_baidu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_enter_button_baidu.png -------------------------------------------------------------------------------- /image/readme/screenshot_enter_button_pikpak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_enter_button_pikpak.png -------------------------------------------------------------------------------- /image/readme/screenshot_enter_button_quark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_enter_button_quark.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_pattern.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_enter_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_enter_button.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_series_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_series_check.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_series_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_series_error.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_series_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_series_example.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_pattern.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_series.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_series.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_pattern_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_pattern_example.png -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | //// 4 | -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_series_check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_series_check.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_series_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_series_error.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_pattern_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_pattern_example.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_series_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_series_example.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_sort_checked_example_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_sort_checked_example_1.png -------------------------------------------------------------------------------- /image/readme/screenshot_main_panel_sort_checked_example_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/readme/screenshot_main_panel_sort_checked_example_2.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_sort_checked_example_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_sort_checked_example_1.png -------------------------------------------------------------------------------- /image/webExtension/screenshot_main_panel_sort_checked_example_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afeireal/cloud-disk-plugin/HEAD/image/webExtension/screenshot_main_panel_sort_checked_example_2.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "printWidth": 100, 6 | "singleQuote": false, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | const sleep = (timeout: number) => { 2 | return timeout > 0 3 | ? new Promise((resolve) => { 4 | setTimeout(resolve, timeout, timeout); 5 | }) 6 | : Promise.resolve(timeout); 7 | }; 8 | 9 | export default sleep; 10 | -------------------------------------------------------------------------------- /.env.web-extension-dev: -------------------------------------------------------------------------------- 1 | VITE_PLUGIN_MODE = "web-extension" 2 | 3 | VITE_PLUGIN_META_URL = "https://github.com/afeireal/cloud-disk-plugin/releases/download/v0.0.0/web-extension/updates.xml" 4 | 5 | VITE_PLUGIN_UPDATE_URL = "https://github.com/afeireal/cloud-disk-plugin/releases/download/v0.0.0/web-extension/cloud-disk-plugin-web-extension-0.0.0.crx" -------------------------------------------------------------------------------- /.env.web-extension-build: -------------------------------------------------------------------------------- 1 | VITE_PLUGIN_MODE = "web-extension" 2 | 3 | VITE_PLUGIN_META_URL = "https://github.com/afeireal/cloud-disk-plugin/releases/download/v0.0.0/web-extension/updates.xml" 4 | 5 | VITE_PLUGIN_UPDATE_URL = "https://github.com/afeireal/cloud-disk-plugin/releases/download/v0.0.0/web-extension/cloud-disk-plugin-web-extension-0.0.0.crx" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.env.tamper-monkey: -------------------------------------------------------------------------------- 1 | VITE_PLUGIN_MODE = "tamper-monkey" 2 | 3 | VITE_PLUGIN_META_URL = "https://update.greasyfork.org/scripts/488421/%E7%BD%91%E7%9B%98%E6%96%87%E4%BB%B6%E6%89%B9%E9%87%8F%E9%87%8D%E5%91%BD%E5%90%8D.meta.js" 4 | 5 | VITE_PLUGIN_UPDATE_URL = "https://update.greasyfork.org/scripts/488421/%E7%BD%91%E7%9B%98%E6%96%87%E4%BB%B6%E6%89%B9%E9%87%8F%E9%87%8D%E5%91%BD%E5%90%8D.user.js" 6 | -------------------------------------------------------------------------------- /src/utils/complementZero.ts: -------------------------------------------------------------------------------- 1 | import { isString } from "@/utils/is"; 2 | 3 | // 补零 4 | export const complementZero = (payload: number | string, maxLength: number = 2): string => { 5 | if (isString(payload)) { 6 | payload = parseInt(payload); 7 | if (isNaN(payload)) { 8 | return ""; 9 | } 10 | } 11 | return "0".repeat(maxLength - (payload + "").length) + payload; 12 | }; 13 | 14 | export default complementZero; 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | root: true, 6 | rules: { 7 | // "no-unused-vars": 0, 8 | // "vue/multi-word-component-names": 1, 9 | }, 10 | extends: [ 11 | "plugin:vue/vue3-essential", 12 | "eslint:recommended", 13 | "@vue/eslint-config-typescript", 14 | "@vue/eslint-config-prettier/skip-formatting" 15 | ], 16 | parserOptions: { 17 | ecmaVersion: "latest" 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/test/utils/fileNameParse.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | 3 | import fileNameParse from "@/utils/fileNameParse"; 4 | 5 | describe("fileNameParse", () => { 6 | test(`fileNameParse("test") to equal "{ ext: "", fileName: "test" }"`, () => { 7 | expect(fileNameParse("test")).toEqual({ ext: "", fileName: "test" }); 8 | }); 9 | test(`fileNameParse("Hello, World!.mp4") to equal "{ ext: "mp4", fileName: "Hello, World!" }"`, () => { 10 | expect(fileNameParse("Hello, World!.mp4")).toEqual({ ext: "mp4", fileName: "Hello, World!" }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/utils/fileNameParse.ts: -------------------------------------------------------------------------------- 1 | const regexp = /^((.|\n)+)\.([^.]+)$/; 2 | 3 | export const fileNameParse = (payload: string): { ext: string; fileName: string } => { 4 | const matchResult = payload.match(regexp); 5 | return { 6 | ext: matchResult?.[3] || "", 7 | fileName: matchResult?.[1] || payload, 8 | }; 9 | }; 10 | 11 | export const getFileName = (payload: string): string => { 12 | return payload.match(regexp)?.[1] || payload; 13 | }; 14 | 15 | export const getExt = (payload: string): string => { 16 | return payload.match(regexp)?.[3] || ""; 17 | }; 18 | 19 | export default fileNameParse; 20 | -------------------------------------------------------------------------------- /src/test/utils/sleep.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | 3 | import sleep from "@/utils/sleep"; 4 | 5 | describe("sleep", () => { 6 | test("should resolve after specified timeout", async () => { 7 | const timeout = 1000; // 1 second 8 | const start = Date.now(); 9 | await sleep(timeout); 10 | const elapsed = Date.now() - start; 11 | expect(elapsed).toBeGreaterThanOrEqual(timeout); 12 | }); 13 | 14 | test("should resolve immediately if timeout is 0", async () => { 15 | const timeout = 0; 16 | const start = Date.now(); 17 | await sleep(timeout); 18 | const elapsed = Date.now() - start; 19 | expect(elapsed).toEqual(timeout); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/test/components/Component/ComponentRadio.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import { test, expect, describe } from "vitest"; 4 | import { mount } from "@vue/test-utils"; 5 | import ComponentRadio from "@/components/Component/ComponentRadio.vue"; 6 | 7 | describe("ComponentRadio", () => { 8 | test(`ComponentRadio setValue "label"`, async () => { 9 | const wrapper: any = mount(ComponentRadio, { 10 | props: { 11 | label: "label", 12 | "onUpdate:modelValue": (val) => wrapper.setProps({ modelValue: val }), 13 | }, 14 | }); 15 | await wrapper.find("input.component-radio-input-original").setValue("label"); 16 | expect(wrapper.props("modelValue")).toEqual("label"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/test/components/Component/ComponentCheckbox.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import { test, expect, describe } from "vitest"; 4 | import { mount } from "@vue/test-utils"; 5 | import ComponentCheckbox from "@/components/Component/ComponentCheckbox.vue"; 6 | 7 | describe("ComponentCheckbox", () => { 8 | test(`ComponentCheckbox setValue true`, async () => { 9 | const wrapper: any = mount(ComponentCheckbox, { 10 | props: { 11 | label: "label", 12 | "onUpdate:modelValue": (val) => wrapper.setProps({ modelValue: val }), 13 | }, 14 | }); 15 | await wrapper.find("input.component-checkbox-input-original").setValue(true); 16 | expect(wrapper.props("modelValue")).toEqual(true); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/test/components/Component/ComponentInput.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import { test, expect, describe } from "vitest"; 4 | import { mount } from "@vue/test-utils"; 5 | import ComponentInput from "@/components/Component/ComponentInput.vue"; 6 | 7 | describe("ComponentInput", () => { 8 | test(`ComponentInput setValue "Hello, World!"`, async () => { 9 | const wrapper: any = mount(ComponentInput, { 10 | props: { 11 | type: "input", 12 | label: "label", 13 | "onUpdate:modelValue": (val) => wrapper.setProps({ modelValue: val }), 14 | }, 15 | }); 16 | await wrapper.find("input.component-input-input").setValue("Hello, World!"); 17 | expect(wrapper.props("modelValue")).toEqual("Hello, World!"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/test/components/Component/ComponentIcon.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import { test, expect, describe } from "vitest"; 4 | import { mount } from "@vue/test-utils"; 5 | import ComponentIcon from "@/components/Component/ComponentIcon.vue"; 6 | 7 | describe("ComponentIcon", () => { 8 | test(`ComponentIcon { props: { name: "loading" } }"`, () => { 9 | const wrapper = mount(ComponentIcon, { props: { name: "loading" } }); 10 | expect(wrapper.find("svg.component_icon g circle animateTransform").exists()).toEqual(true); 11 | }); 12 | test(`ComponentIcon { props: { name: "down", fill: "red" } }"`, () => { 13 | const wrapper = mount(ComponentIcon, { props: { name: "down", fill: "red" } }); 14 | expect(wrapper.find("svg.component_icon").attributes("fill")).toEqual("red"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*", 4 | "src/**/*.ts", 5 | "src/**/*.tsx", 6 | "src/**/*.vue", 7 | "src/**/*.d.ts", 8 | "vite.config.ts" 9 | ], 10 | "extends": "@vue/tsconfig/tsconfig.dom.json", 11 | "compilerOptions": { 12 | "jsx": "preserve", 13 | "lib": ["DOM", "ESNext"], 14 | "types": ["chrome"], 15 | "paths": { "@/*": ["./src/*"] }, 16 | "noEmit": true, 17 | "strict": true, 18 | "module": "ESNext", 19 | "target": "ESNext", 20 | "baseUrl": ".", 21 | "composite": true, 22 | "skipLibCheck": true, 23 | "isolatedModules": true, 24 | "esModuleInterop": true, 25 | "moduleResolution": "Node", 26 | "resolveJsonModule": true, 27 | "useDefineForClassFields": true, 28 | "allowSyntheticDefaultImports": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/render.ts: -------------------------------------------------------------------------------- 1 | import type { VNode } from "vue"; 2 | 3 | import { h } from "vue"; 4 | import { isArray } from "@/utils/is"; 5 | 6 | export interface IRenderOptions { 7 | type: string; 8 | props: any; 9 | children?: IRenderOptions | IRenderOptions[]; 10 | } 11 | 12 | const render = (option: IRenderOptions | IRenderOptions[], props?: any): VNode | VNode[] => { 13 | if (isArray(option)) { 14 | const result: VNode[] = []; 15 | option.forEach((item: IRenderOptions) => { 16 | const res = render(item); 17 | return isArray(res) ? result.push(...res) : result.push(res); 18 | }); 19 | return result; 20 | } 21 | 22 | return h( 23 | option.type, 24 | { ...option.props, ...props }, 25 | option.children ? render(option.children) : undefined 26 | ); 27 | }; 28 | 29 | export default render; 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | -------------------------------------------------------------------------------- /src/provider/index.ts: -------------------------------------------------------------------------------- 1 | import type Provider from "@/provider/interface"; 2 | 3 | import ProviderAli from "./lib/ali"; 4 | import ProviderBaidu from "./lib/baidu"; 5 | import ProviderQuark from "./lib/quark"; 6 | import ProviderPikpak from "./lib/pikpak"; 7 | 8 | export let provider: Provider | undefined; 9 | export const getProvider = (): Provider | undefined => { 10 | if (ProviderAli.test()) { 11 | provider = provider instanceof ProviderAli ? provider : new ProviderAli(); 12 | } else if (ProviderBaidu.test()) { 13 | provider = provider instanceof ProviderBaidu ? provider : new ProviderBaidu(); 14 | } else if (ProviderQuark.test()) { 15 | provider = provider instanceof ProviderQuark ? provider : new ProviderQuark(); 16 | } else if (ProviderPikpak.test()) { 17 | provider = provider instanceof ProviderPikpak ? provider : new ProviderPikpak(); 18 | } else { 19 | provider = undefined; 20 | return undefined; 21 | } 22 | return provider; 23 | }; 24 | 25 | export default getProvider; 26 | -------------------------------------------------------------------------------- /src/components/Component/ComponentIcon.vue: -------------------------------------------------------------------------------- 1 | 38 | 45 | -------------------------------------------------------------------------------- /src/components/Component/ComponentLoading.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 24 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 afeireal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/utils/complementZero.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | 3 | import complementZero from "@/utils/complementZero"; 4 | 5 | describe("complementZero", () => { 6 | test(`complementZero("test") to equal ""`, () => { 7 | expect(complementZero("test")).toEqual(""); 8 | }); 9 | test(`complementZero(0) to equal "00"`, () => { 10 | expect(complementZero(0)).toEqual("00"); 11 | }); 12 | test(`complementZero(1) to equal "01"`, () => { 13 | expect(complementZero(1)).toEqual("01"); 14 | }); 15 | test(`complementZero("1") to equal "01"`, () => { 16 | expect(complementZero("1")).toEqual("01"); 17 | }); 18 | test(`complementZero(10) to equal "10"`, () => { 19 | expect(complementZero(10)).toEqual("10"); 20 | }); 21 | test(`complementZero("10") to equal "10"`, () => { 22 | expect(complementZero("10")).toEqual("10"); 23 | }); 24 | test(`complementZero(99, 4) to equal "0099"`, () => { 25 | expect(complementZero(99, 4)).toEqual("0099"); 26 | }); 27 | test(`complementZero("99", 4) to equal "0099"`, () => { 28 | expect(complementZero("99", 4)).toEqual("0099"); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /image/readme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 23 | 24 | 25 |
26 |
27 |

百度网盘入口

28 | 29 |
30 |
31 |

阿里云盘入口

32 | 33 |
34 |
35 |

夸克网盘入口

36 | 37 |
38 |
39 |

PikPak云盘入口

40 | 41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/test/provider/getWebSocketDebuggerUrl.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process"; 2 | 3 | const scriptPath = "./src/test/provider/openChrome.sh"; 4 | // const scriptPath = "./openChrome.sh"; 5 | 6 | const getWebSocketDebuggerUrl = () => { 7 | return new Promise((resolve, reject) => { 8 | exec(scriptPath, (error, stdout, stderr) => { 9 | if (error) { 10 | console.error(`执行脚本时出错: ${error}`); 11 | console.log(`脚本退出码: ${error.code}`); 12 | reject(error); 13 | } else { 14 | // console.log("脚本执行成功", stderr); 15 | // console.log(`脚本退出码: ${stderr ? stderr.split("\n")[0] : 0}`); 16 | // 尝试解析 JSON 输出 17 | try { 18 | const jsonOutput = JSON.parse(stdout); 19 | // console.log("JSON 输出:", jsonOutput); 20 | resolve(jsonOutput.webSocketDebuggerUrl.replace("ws://localhost:", "ws://127.0.0.1:")); 21 | } catch (parseError) { 22 | console.error(`解析 JSON 输出时出错: ${parseError}`); 23 | reject(parseError); 24 | } 25 | } 26 | if (stderr) { 27 | console.error(`脚本标准错误输出: ${stderr}`); 28 | return; 29 | } 30 | }); 31 | }); 32 | }; 33 | 34 | export default getWebSocketDebuggerUrl; 35 | -------------------------------------------------------------------------------- /src/style/message.css: -------------------------------------------------------------------------------- 1 | .component-message-area { 2 | left: 0; 3 | right: 0; 4 | padding: 5px; 5 | display: flex; 6 | z-index: 20000; 7 | position: fixed; 8 | overflow: hidden; 9 | flex-wrap: nowrap; 10 | box-sizing: border-box; 11 | transition: var(--cdp-transition-all); 12 | pointer-events: none; 13 | flex-direction: column; 14 | } 15 | .component-message-area.is-top-left, 16 | .component-message-area.is-top-center, 17 | .component-message-area.is-top-right { 18 | top: 0; 19 | } 20 | .component-message-area.is-top-left { 21 | align-items: flex-start; 22 | } 23 | .component-message-area.is-top-center { 24 | align-items: center; 25 | } 26 | .component-message-area.is-top-right { 27 | align-items: flex-end; 28 | } 29 | .component-message-area.is-center { 30 | top: 0; 31 | bottom: 0; 32 | align-items: center; 33 | justify-content: center; 34 | } 35 | .component-message-area.is-bottom-left, 36 | .component-message-area.is-bottom-center, 37 | .component-message-area.is-bottom-right { 38 | bottom: 0; 39 | justify-content: flex-end; 40 | } 41 | .component-message-area.is-bottom-left { 42 | align-items: flex-start; 43 | } 44 | .component-message-area.is-bottom-center { 45 | align-items: center; 46 | } 47 | .component-message-area.is-bottom-right { 48 | align-items: flex-end; 49 | } 50 | .component-message-area.has-mask { 51 | top: 0; 52 | bottom: 0; 53 | z-index: 99999; 54 | pointer-events: all; 55 | backdrop-filter: blur(calc(var(--cdp-gutter) / 2)); 56 | } 57 | -------------------------------------------------------------------------------- /src/test/utils/message.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import type { Document } from "happy-dom"; 4 | 5 | import { test, expect, describe, beforeEach } from "vitest"; 6 | import { Window as HappyWindow } from "happy-dom"; 7 | import message from "@/utils/message"; 8 | 9 | describe("message", () => { 10 | let window; 11 | let document: Document; 12 | 13 | beforeEach(() => { 14 | // 创建一个 Happy DOM 的 Window 实例 15 | window = new HappyWindow(); 16 | document = window.document; 17 | 18 | // 设置 HTML 结构 19 | document.body.innerHTML = `
Hello, World!
`; 20 | 21 | // 将全局的 document 和 window 对象设置为 Happy DOM 的实例 22 | global.document = document as unknown as globalThis.Document; 23 | global.window = window as unknown as Window & typeof globalThis; 24 | }); 25 | 26 | test(`message.loading("Hello, World!")`, () => { 27 | message.loading("Hello, World!"); 28 | expect( 29 | document.querySelector( 30 | ".component-message-area.is-top-center .component-message .component-message-content-message" 31 | )?.innerHTML 32 | ).toEqual("Hello, World!"); 33 | }); 34 | 35 | test(`message.success("Hello, World!")`, () => { 36 | message.success("Hello, World!", { position: "bottom-right" }); 37 | expect( 38 | document.querySelector( 39 | ".component-message-area.is-bottom-right .component-message .component-message-content-message" 40 | )?.innerHTML 41 | ).toEqual("Hello, World!"); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from "vue"; 2 | import type Provider from "@/provider/interface"; 3 | 4 | import "@/style/index.css"; 5 | import { ref, createApp } from "vue"; 6 | import App from "@/App.vue"; 7 | import getProvider from "@/provider"; 8 | import sleep from "@/utils/sleep"; 9 | import querySelector from "@/utils/querySelector"; 10 | 11 | const providerRef: Ref = ref(); 12 | 13 | const getProviderRef = (): Ref => { 14 | const instance = getProvider(); 15 | 16 | if (providerRef.value !== instance) { 17 | providerRef.value = instance; 18 | Object.assign(window, { 19 | _toggleCloudDiskPlugin: () => { 20 | if (providerRef.value) { 21 | providerRef.value.setVisible(!providerRef.value.visible); 22 | } 23 | }, 24 | }); 25 | } 26 | 27 | return providerRef; 28 | }; 29 | 30 | const loop = () => { 31 | const providerRef = getProviderRef(); 32 | if (!providerRef?.value) { 33 | return; 34 | } 35 | 36 | const target = querySelector(providerRef.value.rootElementInsertTarget); 37 | 38 | const rootElement = querySelector("#" + providerRef.value.rootElementId); 39 | 40 | if (target && !rootElement) { 41 | const app = createApp(App); 42 | app.provide("providerRef", providerRef); 43 | app.mount( 44 | (() => { 45 | const root = document.createElement("div"); 46 | root.setAttribute("id", providerRef.value.rootElementId); 47 | target[providerRef.value.rootElementInsertMethod](root); 48 | return root; 49 | })() 50 | ); 51 | } 52 | }; 53 | 54 | const main = async () => { 55 | while (window?.parent === window) { 56 | loop(); 57 | await sleep(300); 58 | } 59 | }; 60 | 61 | main(); 62 | -------------------------------------------------------------------------------- /src/provider/lib/pikpak/EnterComponent.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 37 | 67 | -------------------------------------------------------------------------------- /src/provider/lib/baidu/EnterComponent.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 72 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import browser from "webextension-polyfill"; 2 | 3 | const urlSchemes = [ 4 | "https://pan.quark.cn/*", 5 | "https://pan.baidu.com/*", 6 | "https://www.alipan.com/*", 7 | "https://www.aliyundrive.com/*", 8 | ]; 9 | 10 | const urlRegExp = 11 | /^https:\/\/pan.quark.cn|^https:\/\/pan\.baidu\.com|^https:\/\/www\.ali(pan|yundrive)\.com/; 12 | 13 | const iconPath = chrome.runtime.getURL("logo.png"); 14 | const iconDisabledPath = chrome.runtime.getURL("logo_grey.png"); 15 | 16 | const updateIcon = (tabId: number) => { 17 | browser.tabs.get(tabId).then((tab) => { 18 | const isMatched = tab.url && urlRegExp.test(tab.url); 19 | if (isMatched) { 20 | browser.action.setIcon({ path: iconPath }); 21 | } else { 22 | browser.action.setIcon({ path: iconDisabledPath }); 23 | } 24 | }); 25 | }; 26 | 27 | browser.tabs.onActivated.addListener(function (event: any) { 28 | updateIcon(event.tabId); 29 | }); 30 | 31 | browser.tabs.onUpdated.addListener(function (tabId: number) { 32 | updateIcon(tabId); 33 | }); 34 | 35 | browser.webNavigation.onDOMContentLoaded.addListener( 36 | function (event) { 37 | if (event.frameId === 0) { 38 | chrome.scripting.executeScript({ 39 | world: "MAIN", 40 | files: ["src/main.js"], 41 | target: { tabId: event.tabId }, 42 | }); 43 | } 44 | }, 45 | { 46 | url: urlSchemes.map((item) => ({ urlMatches: item })), 47 | } 48 | ); 49 | 50 | browser.action.onClicked.addListener(function (event: any) { 51 | const isMatched = event.url && urlRegExp.test(event.url); 52 | if (isMatched) { 53 | chrome.scripting.executeScript({ 54 | func: () => { 55 | new Function(`window._toggleCloudDiskPlugin()`)(); 56 | }, 57 | world: "MAIN", 58 | target: { tabId: event.id }, 59 | }); 60 | } 61 | }); 62 | 63 | browser.runtime.onInstalled.addListener(function () { 64 | console.log("onInstalled"); 65 | }); 66 | -------------------------------------------------------------------------------- /src/test/utils/querySelector.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import type { Document } from "happy-dom"; 4 | 5 | import { test, expect, describe, beforeEach } from "vitest"; 6 | import { Window as HappyWindow } from "happy-dom"; 7 | import querySelector from "@/utils/querySelector"; 8 | 9 | describe("querySelector", () => { 10 | let window; 11 | let document: Document; 12 | 13 | beforeEach(() => { 14 | // 创建一个 Happy DOM 的 Window 实例 15 | window = new HappyWindow(); 16 | document = window.document; 17 | 18 | // 设置 HTML 结构 19 | document.body.innerHTML = `
Hello, World!
`; 20 | 21 | // 将全局的 document 和 window 对象设置为 Happy DOM 的实例 22 | global.document = document as unknown as globalThis.Document; 23 | global.window = window as unknown as Window & typeof globalThis; 24 | }); 25 | 26 | test(`querySelector("#app")?.innerText to equal "Hello, World!"`, () => { 27 | expect(querySelector("#app")?.innerText).toEqual("Hello, World!"); 28 | }); 29 | test(`await querySelector("#app", true)?.innerText to equal "Hello, World!"`, async () => { 30 | expect((await querySelector("#app", true))?.innerText).toEqual("Hello, World!"); 31 | }); 32 | test(`await querySelector("#app", 10, 1000) to equal { timeDiff: true, innerText: "Hello, World!" }`, async () => { 33 | const startTime = Date.now(); 34 | document.body.innerHTML = ""; 35 | let updateTimeDiff = startTime; 36 | setTimeout(() => { 37 | updateTimeDiff = Date.now() - startTime; 38 | document.body.innerHTML = `
Hello, World!
`; 39 | }, 2000); 40 | expect( 41 | await querySelector("#app", 10, 1000).then((res) => ({ 42 | timeDiff: Date.now() - startTime - updateTimeDiff < 10, 43 | innerText: res.innerText, 44 | })) 45 | ).toEqual({ timeDiff: true, innerText: "Hello, World!" }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/style/transition.css: -------------------------------------------------------------------------------- 1 | .fade-enter-from, 2 | .fade-leave-to { 3 | opacity: 0; 4 | } 5 | .fade-enter-to, 6 | .fade-leave-from { 7 | opacity: 1; 8 | } 9 | .fade-enter-active, 10 | .fade-leave-active { 11 | transition: opacity var(--cdp-transition-default); 12 | } 13 | 14 | .fade-bottom-enter-from, 15 | .fade-bottom-leave-to { 16 | opacity: 0; 17 | transform: translateY(10%); 18 | } 19 | .fade-bottom-enter-to, 20 | .fade-bottom-leave-from { 21 | opacity: 1; 22 | transform: none; 23 | } 24 | .fade-bottom-enter-active, 25 | .fade-bottom-leave-active { 26 | transition: 27 | opacity 0.3s, 28 | transform 0.3s ease; 29 | } 30 | /* 31 | .clip-top-enter-from, 32 | .clip-top-leave-to { 33 | clip-path: inset(0 0 100% 0); 34 | } 35 | .clip-top-enter-to, 36 | .clip-top-leave-from { 37 | clip-path: inset(0 0 0 0); 38 | } 39 | .clip-top-enter-active, 40 | .clip-top-leave-active { 41 | transition: clip-path var(--cdp-transition-default); 42 | } 43 | 44 | .clip-bottom-enter-from, 45 | .clip-bottom-leave-to { 46 | clip-path: inset(100% 0 0 0); 47 | } 48 | .clip-bottom-enter-to, 49 | .clip-bottom-leave-from { 50 | clip-path: inset(0 0 0 0); 51 | } 52 | .clip-bottom-enter-active, 53 | .clip-bottom-leave-active { 54 | transition: clip-path var(--cdp-transition-default); 55 | } 56 | 57 | .clip-left-enter-from, 58 | .clip-left-leave-to { 59 | clip-path: inset(0 0 100% 0); 60 | } 61 | .clip-left-enter-to, 62 | .clip-left-leave-from { 63 | clip-path: inset(0 0 0 0); 64 | } 65 | .clip-left-enter-active, 66 | .clip-left-leave-active { 67 | transition: clip-path var(--cdp-transition-default); 68 | } 69 | 70 | .clip-right-enter-from, 71 | .clip-right-leave-to { 72 | clip-path: inset(0 100% 0 0); 73 | } 74 | .clip-right-enter-to, 75 | .clip-right-leave-from { 76 | clip-path: inset(0 0 0 0); 77 | } 78 | .clip-right-enter-active, 79 | .clip-right-leave-active { 80 | transition: clip-path var(--cdp-transition-default); 81 | } 82 | 83 | .clip-center-enter-from, 84 | .clip-center-leave-to { 85 | clip-path: inset(100% 100% 100% 100%); 86 | } 87 | .clip-center-enter-to, 88 | .clip-center-leave-from { 89 | clip-path: inset(0 0 0 0); 90 | } 91 | .clip-center-enter-active, 92 | .clip-center-leave-active { 93 | transition: clip-path var(--cdp-transition-default); 94 | } */ 95 | -------------------------------------------------------------------------------- /src/utils/reactFiber.ts: -------------------------------------------------------------------------------- 1 | import querySelector from "@/utils/querySelector"; 2 | 3 | export interface IReactFiberNode { 4 | child?: IReactFiberNode; 5 | return?: IReactFiberNode; 6 | sibling?: IReactFiberNode; 7 | pendingProps: any; 8 | } 9 | 10 | export interface ICheckReactFiberNode { 11 | (node: IReactFiberNode): any | boolean | undefined | void; 12 | } 13 | 14 | export const findReactFiberNode = ( 15 | node: IReactFiberNode, 16 | check: ICheckReactFiberNode 17 | ): IReactFiberNode | undefined => { 18 | const list: IReactFiberNode[] = [node]; 19 | while (list.length) { 20 | const item = list.shift() as IReactFiberNode; 21 | if (check(item)) { 22 | return item; 23 | } else { 24 | if (item.child) { 25 | list.push(item.child); 26 | } 27 | if (item.sibling) { 28 | list.push(item.sibling); 29 | } 30 | // let next = item; 31 | // while (next.sibling) { 32 | // next = next.sibling; 33 | // list.push(next); 34 | // } 35 | } 36 | } 37 | }; 38 | 39 | export let rootReactContainer: IReactFiberNode; 40 | 41 | function _getRootReactContainer(selectors: string): IReactFiberNode | undefined; 42 | function _getRootReactContainer(selectors: string, isPromise?: boolean): Promise; 43 | function _getRootReactContainer( 44 | selectors: string, 45 | isPromise: boolean = true 46 | ): Promise | IReactFiberNode | undefined { 47 | if (rootReactContainer) { 48 | return isPromise ? Promise.resolve(rootReactContainer) : rootReactContainer; 49 | } 50 | const rootElement = querySelector(selectors); 51 | 52 | if (!rootElement) { 53 | return isPromise ? Promise.reject() : undefined; 54 | } 55 | const keys = Object.keys(rootElement) as Array; 56 | 57 | const reactContainerKey = keys.find((item: keyof HTMLElement) => 58 | item.startsWith("__reactContainer") 59 | ); 60 | 61 | if (!reactContainerKey) { 62 | return isPromise ? Promise.reject() : undefined; 63 | } 64 | 65 | rootReactContainer = rootElement[reactContainerKey] as unknown as IReactFiberNode; 66 | 67 | return isPromise ? Promise.resolve(rootReactContainer) : rootReactContainer; 68 | } 69 | 70 | export const getRootReactContainer = _getRootReactContainer; 71 | -------------------------------------------------------------------------------- /src/provider/lib/ali/EnterComponent.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 37 | 86 | -------------------------------------------------------------------------------- /src/provider/lib/quark/EnterComponent.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 36 | 37 | 43 | 44 | 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-disk-plugin", 3 | "type": "module", 4 | "cnName": "网盘文件批量重命名", 5 | "author": "afeireal", 6 | "license": "MIT", 7 | "version": "1.3.0", 8 | "keywords": [ 9 | "网盘", 10 | "重命名", 11 | "批量重命名", 12 | "cloud disk", 13 | "rename", 14 | "batch rename", 15 | "tamper monkey", 16 | "web extension" 17 | ], 18 | "repository": "https://github.com/afeireal/cloud-disk-plugin", 19 | "description": "Batch rename files of cloud disk.", 20 | "cnDescription": "网盘文件批量重命名插件,支持百度网盘、阿里云盘、夸克网盘、PikPak云盘。", 21 | "scripts": { 22 | "dev:tamper-monkey": "vite --mode tamper-monkey", 23 | "dev:web-extension": "vite --mode web-extension-dev", 24 | "build:tamper-monkey": "vue-tsc --noEmit && vite build --mode tamper-monkey", 25 | "build:web-extension": "vue-tsc --noEmit && vite build --mode web-extension-build && node buildWebExtension.js", 26 | "preview": "vite preview", 27 | "test": "vitest", 28 | "type-check": "vue-tsc --noEmit -p tsconfig.json --composite false", 29 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 30 | "format": "prettier --write src/" 31 | }, 32 | "dependencies": { 33 | "diff": "^7.0.0", 34 | "sortablejs": "^1.15.3", 35 | "vue": "^3.4.38", 36 | "webextension-polyfill": "^0.12.0" 37 | }, 38 | "devDependencies": { 39 | "@rushstack/eslint-patch": "^1.10.4", 40 | "@types/chrome": "^0.0.270", 41 | "@types/diff": "^6.0.0", 42 | "@types/node": "^20.16.2", 43 | "@types/sortablejs": "^1.15.8", 44 | "@types/webextension-polyfill": "^0.12.1", 45 | "@vitejs/plugin-vue": "^5.1.2", 46 | "@vue/eslint-config-prettier": "^9.0.0", 47 | "@vue/eslint-config-typescript": "^13.0.0", 48 | "@vue/test-utils": "^2.4.6", 49 | "@vue/tsconfig": "^0.5.1", 50 | "dotenv": "^16.4.5", 51 | "eslint": "^8.57.0", 52 | "eslint-plugin-vue": "^9.27.0", 53 | "happy-dom": "^15.3.1", 54 | "prettier": "^3.3.3", 55 | "puppeteer": "^23.2.0", 56 | "typescript": "^5.5.4", 57 | "vite": "^5.4.2", 58 | "vite-plugin-css-injected-by-js": "^3.5.1", 59 | "vite-plugin-monkey": "^4.0.6", 60 | "vite-plugin-web-extension": "^4.1.6", 61 | "vitest": "^2.0.5", 62 | "vue-tsc": "^2.1.0", 63 | "xmlbuilder2": "^3.1.1", 64 | "zip-a-folder": "^3.1.7" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/provider/openChrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 定义配置参数 4 | DEBUGGER_PORT=9222 5 | MAX_ATTEMPTS=5 6 | DELAY_SECONDS=2 7 | CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" 8 | 9 | # 函数:预先检查 10 | pre_check() { 11 | # 检查依赖工具是否安装 12 | if ! command -v curl >/dev/null 2>&1; then 13 | echo "curl not installed. Aborting." 14 | exit 1 15 | fi 16 | if ! command -v jq >/dev/null 2>&1; then 17 | echo "jq not installed. Aborting." 18 | exit 1 19 | fi 20 | # 检查Chrome是否已安装 21 | if [[ ! -x "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]]; then 22 | echo "Chrome is not installed at the expected location. Aborting." 23 | exit 1 24 | fi 25 | } 26 | 27 | # 函数:启动 Chrome 并获取 Chrome Debugger 数据 28 | launch_chromel() { 29 | # 检查 Chrome 是否已经运行在远程调试模式 30 | if pgrep -x "Google Chrome" >/dev/null; then 31 | # echo "Chrome is already running. Attempting to connect to the remote debugging port." 32 | get_chrome_debugger_data && return 0 33 | fi 34 | 35 | # 启动 Chrome 浏览器的远程调试模式 36 | # nohup "${CHROME_PATH}" --remote-debugging-port="${DEBUGGER_PORT}" >/dev/null 2>&1 & 37 | "${CHROME_PATH}" --remote-debugging-port="${DEBUGGER_PORT}" >/dev/null 2>&1 & 38 | 39 | # 获取 Chrome 进程的 PID 40 | # chrome_pid=$! 41 | 42 | # 使用 disown 命令放弃对 Chrome 进程的控制 43 | # disown $chrome_pid 44 | 45 | local attempts=0 46 | 47 | while ((attempts < MAX_ATTEMPTS)); do 48 | sleep $DELAY_SECONDS 49 | get_chrome_debugger_data && break 50 | ((attempts++)) 51 | done 52 | 53 | if ((attempts == MAX_ATTEMPTS)); then 54 | echo "Failed to start Chrome or get the WebSocketDebuggerUrl within the expected time." 55 | # kill $chrome_pid 2>/dev/nulls 56 | exit 1 57 | fi 58 | } 59 | 60 | # 函数:获取 Chrome Debugger 数据 61 | get_chrome_debugger_data() { 62 | local response=$(curl -s -w "%{http_code}" -o /dev/null http://localhost:$DEBUGGER_PORT/json/version 2>/dev/null) 63 | if [[ $response -eq 200 ]]; then 64 | local content=$(curl -s http://localhost:$DEBUGGER_PORT/json/version) 65 | local webSocketDebuggerUrl=$(echo $content | jq -r '.webSocketDebuggerUrl') 66 | if [[ -n $webSocketDebuggerUrl ]]; then 67 | echo $content 68 | return 0 69 | fi 70 | fi 71 | return 1 72 | } 73 | 74 | main() { 75 | pre_check 76 | launch_chromel 77 | } 78 | 79 | main 80 | 81 | # local response=$(curl -s http://localhost:9222/json/version) 82 | # local webSocketDebuggerUrl=$(echo $response | grep -o '"webSocketDebuggerUrl": "[^"]*' | cut -d '"' -f 4) 83 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export const is = (val: unknown, type: string): val is T => 2 | Object.prototype.toString.call(val) === `[object ${type}]`; 3 | 4 | export const isSet = (val: unknown): val is Set => is>(val, "Set"); 5 | 6 | export const isMap = (val: unknown): val is Map => is>(val, "Map"); 7 | 8 | export const isDate = (val: unknown): val is Date => is(val, "Date"); 9 | 10 | export const isArray = (val: unknown): val is Array => is>(val, "Array"); 11 | 12 | export const isString = (val: unknown): val is string => is(val, "String"); 13 | 14 | export const isNumber = (val: unknown): val is number => is(val, "Number"); 15 | 16 | export const isWindow = (val: unknown): val is Window => is(val, "Window"); 17 | 18 | export const isRegExp = (val: unknown): val is RegExp => is(val, "RegExp"); 19 | 20 | export const isObject = (val: any): val is Record => is>(val, "Object"); 21 | 22 | export const isBoolean = (val: unknown): val is boolean => is(val, "Boolean"); 23 | 24 | export const isPromise = (val: unknown): val is Promise => is>(val, "Promise"); 25 | 26 | export const isFunction = (val: unknown): val is Function => is(val, "Function"); 27 | 28 | export const isNull = (val: unknown): val is null => val === null; 29 | 30 | export const isUndefined = (val: unknown): val is undefined => val === undefined; 31 | 32 | export const isVoid = (val: unknown): val is null | undefined => isNull(val) || isUndefined(val); 33 | 34 | export const isEmpty = (val: unknown): val is "" | null | undefined => isVoid(val) || val === ""; 35 | 36 | export const isEmptyValue = (val: unknown): boolean => { 37 | if (isArray(val) || isString(val)) { 38 | return val.length === 0; 39 | } else if (isObject(val)) { 40 | return Object.keys(val).length === 0; 41 | } else if (isSet(val) || isMap(val)) { 42 | return val.size === 0; 43 | } 44 | return false; 45 | }; 46 | 47 | export const isImage = (val: string): boolean => 48 | isString(val) && /.+\.(jpeg|jpg|png|gif)$/i.test(val.toLocaleLowerCase()); 49 | 50 | export const isMobile = (): boolean => 51 | !navigator.userAgent.match( 52 | /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i 53 | ); 54 | 55 | export const isDark = () => window.matchMedia("(prefers-color-scheme: dark)").matches; 56 | 57 | export const isLight = () => window.matchMedia("(prefers-color-scheme: light)").matches; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 网盘文件批量重命名 2 | 3 | ## 功能介绍 4 | 5 | 批量重命名当前支持两种方式:剧集模式和正则模式 6 | 7 | 可对文件进行排序,应用到自动集数的重命名中 8 | 9 | ## 适用范围 10 | 11 | - ✅ 百度网盘 12 | - ✅ 阿里云盘 13 | - ✅ 夸克网盘 14 | - ✅ PikPak云盘 15 | 16 | 百度网盘入口 17 | 18 | 百度网盘入口 19 | 20 | 阿里云盘入口 21 | 22 | 阿里云盘入口 23 | 24 | 夸克网盘入口 25 | 26 | 夸克网盘入口 27 | 28 | PikPak云盘入口 29 | 30 | PikPak云盘入口 31 | 32 | ## 使用方法 33 | 34 | 1. 打开重命名界面 35 | 2. 选择适用的替换模式 36 | 3. 输入替换规则的参数 37 | 4. 点击 **应用** 开始替换 38 | 5. 等待替换完成 39 | 40 | - 如未获取到文件列表数据,可点击 **重置** 41 | 42 | - 点击当前版本可以检查更新 43 | 44 | ### 剧集模式 45 | 46 | 剧集模式界面 47 | 48 | 剧集模式界面 49 | 50 | - 输入剧名与季数 51 | - 季数可不输入 52 | - 建议勾选自动集数,将会按照排序自动补全集数 53 | 54 | 剧集模式案例 55 | 56 | ### 正则模式 57 | 58 | 正则模式界面 59 | 60 | 正则模式界面 61 | 62 | - 输入正则与替换文本 63 | - 正则替换模式使用 Javascript 的 `String.prototype.replace` 方法,建议有正则基础的用户使用 64 | - [关于正则表达式](https://www.runoob.com/regexp/regexp-syntax.html) 65 | - [学习正则表达式](https://regexlearn.com/zh-cn) 66 | 67 | 正则模式案例 68 | 69 | ### 错误提示 70 | 71 | 以下情况会出现错误提示 72 | 73 | - 新文件名重复 74 | - 新文件名为空 75 | 76 | 错误提示 77 | 78 | ### 替换范围 79 | 80 | - 可通过全选与不全选快速选择目标文件 81 | - 点击应用时,只会重命名已选中的文件 82 | 83 | 替换范围 84 | 85 | ### 排序已选 86 | 87 | - 可以手动调整排序 88 | - 勾选`排序已选`可只对已选中的文件进行排序 89 | 90 | 排序已选 91 | 排序已选 -------------------------------------------------------------------------------- /src/test/utils/is.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import { test, expect, describe } from "vitest"; 4 | import { 5 | is, 6 | isSet, 7 | isMap, 8 | isDate, 9 | isArray, 10 | isString, 11 | isNumber, 12 | isWindow, 13 | isRegExp, 14 | isObject, 15 | isBoolean, 16 | isPromise, 17 | isFunction, 18 | isNull, 19 | isUndefined, 20 | isVoid, 21 | isEmpty, 22 | isEmptyValue, 23 | isImage, 24 | isMobile, 25 | isDark, 26 | isLight, 27 | } from "@/utils/is"; 28 | 29 | describe("is", () => { 30 | test(`is(Symbol("is"), "Symbol") to equal true`, () => { 31 | expect(is(Symbol("is"), "Symbol")).toEqual(true); 32 | }); 33 | test(`isSet(new Set()) to equal true`, () => { 34 | expect(isSet(new Set())).toEqual(true); 35 | }); 36 | test(`isMap(new Map()) to equal true`, () => { 37 | expect(isMap(new Map())).toEqual(true); 38 | }); 39 | test(`isDate(new Date()) to equal true`, () => { 40 | expect(isDate(new Date())).toEqual(true); 41 | }); 42 | test(`isArray([]) to equal true`, () => { 43 | expect(isArray([])).toEqual(true); 44 | }); 45 | test(`isString("string") to equal true`, () => { 46 | expect(isString("string")).toEqual(true); 47 | }); 48 | test(`isNumber(0) to equal true`, () => { 49 | expect(isNumber(0)).toEqual(true); 50 | }); 51 | test(`isWindow(window) to equal false`, () => { 52 | expect(isWindow(window)).toEqual(false); 53 | }); 54 | test(`isRegExp(/regexp/) to equal true`, () => { 55 | expect(isRegExp(/regexp/)).toEqual(true); 56 | }); 57 | test(`isObject({}) to equal true`, () => { 58 | expect(isObject({})).toEqual(true); 59 | }); 60 | test(`isBoolean(false) to equal true`, () => { 61 | expect(isBoolean(false)).toEqual(true); 62 | }); 63 | test(`isPromise(Promise.resolve()) to equal true`, () => { 64 | expect(isPromise(Promise.resolve())).toEqual(true); 65 | }); 66 | test(`isFunction(() => {}) to equal true`, () => { 67 | expect(isFunction(() => {})).toEqual(true); 68 | }); 69 | test(`isNull(null) to equal true`, () => { 70 | expect(isNull(null)).toEqual(true); 71 | }); 72 | test(`isUndefined(undefined) to equal true`, () => { 73 | expect(isUndefined(undefined)).toEqual(true); 74 | }); 75 | test(`isVoid(null) to equal true`, () => { 76 | expect(isVoid(null)).toEqual(true); 77 | }); 78 | test(`isEmpty("") to equal true`, () => { 79 | expect(isEmpty("")).toEqual(true); 80 | }); 81 | test(`isEmptyValue([]) to equal true`, () => { 82 | expect(isEmptyValue([])).toEqual(true); 83 | }); 84 | test(`isImage("img.png") to equal true`, () => { 85 | expect(isImage("img.png")).toEqual(true); 86 | }); 87 | test(`isMobile() to equal true`, () => { 88 | expect(isMobile()).toEqual(true); 89 | }); 90 | test(`isDark() to equal false`, () => { 91 | expect(isDark()).toEqual(false); 92 | }); 93 | test(`isLight() to equal true`, () => { 94 | expect(isLight()).toEqual(true); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/RenamePanel.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 56 | 57 | 102 | -------------------------------------------------------------------------------- /src/utils/querySelector.ts: -------------------------------------------------------------------------------- 1 | import { isArray, isBoolean, isUndefined } from "@/utils/is"; 2 | 3 | export function querySelector(selectors: string): T | null; 4 | export function querySelector(selectors: string, isPromise: boolean): Promise; 5 | export function querySelector( 6 | selectors: string, 7 | isPromise: number, 8 | timeout?: number 9 | ): Promise; 10 | export function querySelector( 11 | selectors: string, 12 | isPromise?: boolean | number, 13 | timeout: number = 100 14 | ): Promise | T | null { 15 | let element = document.querySelector(selectors) as T; 16 | if (isUndefined(isPromise)) { 17 | return element; 18 | } else if (isBoolean(isPromise)) { 19 | if (isPromise) { 20 | return element ? Promise.resolve(element) : Promise.reject(selectors + " is not found"); 21 | } 22 | return element; 23 | } else if (element) { 24 | return Promise.resolve(element); 25 | } else if (isPromise > 0) { 26 | return new Promise((resolve, reject) => { 27 | const timer = setInterval(() => { 28 | element = document.querySelector(selectors) as T; 29 | if (element) { 30 | resolve(element); 31 | clearInterval(timer); 32 | } else if (--(isPromise as number) <= 0) { 33 | reject(selectors + " is not found"); 34 | clearInterval(timer); 35 | } 36 | }, timeout); 37 | }); 38 | } else { 39 | return Promise.reject(selectors + " is not found"); 40 | } 41 | } 42 | 43 | export function querySelectorAll>(selectors: string): T | null; 44 | export function querySelectorAll>( 45 | selectors: string, 46 | isPromise: boolean 47 | ): Promise; 48 | export function querySelectorAll>( 49 | selectors: string, 50 | isPromise: number, 51 | timeout?: number 52 | ): Promise; 53 | export function querySelectorAll>( 54 | selectors: string, 55 | isPromise?: boolean | number, 56 | timeout: number = 100 57 | ): Promise | T | null { 58 | let element = Array.from(document.querySelectorAll(selectors)) as T; 59 | if (isUndefined(isPromise)) { 60 | return element; 61 | } else if (isBoolean(isPromise)) { 62 | if (isArray(element)) { 63 | if (isPromise) { 64 | return element.length 65 | ? Promise.resolve(element) 66 | : Promise.reject(selectors + " is not found"); 67 | } 68 | return element.length ? element : null; 69 | } 70 | return null; 71 | } else if (isArray(element) && element.length) { 72 | return Promise.resolve(element); 73 | } else if (isPromise > 0) { 74 | return new Promise((resolve, reject) => { 75 | const timer = setInterval(() => { 76 | element = Array.from(document.querySelectorAll(selectors)) as T; 77 | console.dir("element", element); 78 | if (isArray(element) && element.length) { 79 | resolve(element); 80 | clearInterval(timer); 81 | } else if (--(isPromise as number) <= 0) { 82 | reject(selectors + " is not found"); 83 | clearInterval(timer); 84 | } 85 | }, timeout); 86 | }); 87 | } else { 88 | return Promise.reject(selectors + " is not found"); 89 | } 90 | } 91 | 92 | export default querySelector; 93 | -------------------------------------------------------------------------------- /src/test/provider/index.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | import type { Page, Browser } from "puppeteer"; 3 | 4 | import { cwd } from "node:process"; 5 | import { resolve } from "node:path"; 6 | import dotenv from "dotenv"; 7 | import puppeteer from "puppeteer"; 8 | import { test, expect, describe, beforeAll, afterAll } from "vitest"; 9 | import getWebSocketDebuggerUrl from "./getWebSocketDebuggerUrl"; 10 | 11 | function generateRandomString(length: number = 6) { 12 | const charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 13 | let result = ""; 14 | while (length--) { 15 | result += charset.charAt(Math.floor(Math.random() * charset.length)); 16 | } 17 | return result; 18 | } 19 | 20 | describe("provider", async () => { 21 | let browser: Browser; 22 | 23 | const env = dotenv.config({ path: resolve(cwd(), "./.env.test.local") }); 24 | 25 | const randomString = generateRandomString(6); 26 | 27 | beforeAll(async () => { 28 | // browser = await puppeteer.launch 29 | const browserWSEndpoint = await getWebSocketDebuggerUrl(); 30 | browser = await puppeteer.connect({ 31 | defaultViewport: { width: 1920, height: 1080 }, 32 | browserWSEndpoint, 33 | }); 34 | }); 35 | 36 | test.concurrent("baidu 甄嬛传", async () => { 37 | const page = await browser.newPage(); 38 | env?.parsed?.TEST_URL_BAIDU && (await page.goto(env.parsed.TEST_URL_BAIDU), { timeout: 45000 }); 39 | await previewTest(page, 76); 40 | await renameTest(page, randomString); 41 | await page.close(); 42 | }); 43 | 44 | test.concurrent("ali 甄嬛传", async () => { 45 | const page = await browser.newPage(); 46 | env?.parsed?.TEST_URL_ALI && (await page.goto(env.parsed.TEST_URL_ALI)); 47 | await page.waitForSelector("[class^=tr-wrapper--]"); 48 | await previewTest(page, 76); 49 | await renameTest(page, randomString); 50 | await page.close(); 51 | }); 52 | 53 | test.concurrent("quark 甄嬛传", async () => { 54 | const page = await browser.newPage(); 55 | env?.parsed?.TEST_URL_QUARK && (await page.goto(env.parsed.TEST_URL_QUARK)); 56 | await page.waitForSelector(".ant-table-body tbody tr"); 57 | await previewTest(page, 50); 58 | await renameTest(page, randomString); 59 | await page.close(); 60 | }); 61 | 62 | afterAll(async () => { 63 | await browser?.close(); 64 | }); 65 | }); 66 | 67 | const previewTest = async (page: Page, count: number) => { 68 | const enterComponentButton = await page.waitForSelector(".enter-component-button"); 69 | await enterComponentButton?.click(); 70 | await page.waitForSelector(".rename-preview-content-grid-body .rename-preview-content-grid-item"); 71 | const renamePreviewContentGridItem = await page.$$( 72 | ".rename-preview-content-grid-body .rename-preview-content-grid-item" 73 | ); 74 | expect(renamePreviewContentGridItem?.length).toEqual(count); 75 | }; 76 | 77 | const renameTest = async (page: Page, randomString: string) => { 78 | const titleElement = await page.$(".component-input-textarea"); 79 | const seasonElement = await page.$(".component-input-input"); 80 | const confirmButtonElement = await page.$(".rename-control-footer-button.confirm"); 81 | await titleElement?.type(randomString + "-甄嬛传"); 82 | await seasonElement?.type("1", { delay: 100 }); 83 | await confirmButtonElement?.click(); 84 | 85 | const resultMsgElement = await page.waitForSelector(".component-message-content", { 86 | timeout: 45000, 87 | }); 88 | 89 | const resultMsg = await resultMsgElement?.$eval( 90 | ".component-message-content-message", 91 | (node: any) => node?.innerText 92 | ); 93 | 94 | expect(resultMsg).toEqual("批量重命名成功"); 95 | }; 96 | -------------------------------------------------------------------------------- /src/style/index.css: -------------------------------------------------------------------------------- 1 | @import "./transition.css"; 2 | 3 | :root { 4 | --cdp-color-white: var(--cdp-color-gray-0); 5 | --cdp-color-black: var(--cdp-color-gray-1000); 6 | --cdp-color-gray: var(--cdp-color-gray-500); 7 | --cdp-color-gray-0: #ffffff; 8 | --cdp-color-gray-50: #f2f2f2; 9 | --cdp-color-gray-100: #e6e6e6; 10 | --cdp-color-gray-200: #cccccc; 11 | --cdp-color-gray-300: #b3b3b3; 12 | --cdp-color-gray-400: #989898; 13 | --cdp-color-gray-500: #808080; 14 | --cdp-color-gray-600: #656565; 15 | --cdp-color-gray-700: #4d4d4d; 16 | --cdp-color-gray-800: #333333; 17 | --cdp-color-gray-900: #1a1a1a; 18 | --cdp-color-gray-950: #0d0d0d; 19 | --cdp-color-gray-1000: #000000; 20 | 21 | --cdp-color-red: var(--cdp-color-red-500); 22 | --cdp-color-red-50: #fef2f2; 23 | --cdp-color-red-100: #fee2e2; 24 | --cdp-color-red-200: #fecaca; 25 | --cdp-color-red-300: #fca5a5; 26 | --cdp-color-red-400: #f87171; 27 | --cdp-color-red-500: #ef4444; 28 | --cdp-color-red-600: #dc2626; 29 | --cdp-color-red-700: #b91c1c; 30 | --cdp-color-red-800: #991b1b; 31 | --cdp-color-red-900: #7f1d1d; 32 | --cdp-color-red-950: #450a0a; 33 | 34 | --cdp-color-blue: var(--cdp-color-blue-500); 35 | --cdp-color-blue-50: #eff6ff; 36 | --cdp-color-blue-100: #dbeafe; 37 | --cdp-color-blue-200: #bfdbfe; 38 | --cdp-color-blue-300: #93c5fd; 39 | --cdp-color-blue-400: #60a5fa; 40 | --cdp-color-blue-500: #3b82f6; 41 | --cdp-color-blue-600: #2563eb; 42 | --cdp-color-blue-700: #1d4ed8; 43 | --cdp-color-blue-800: #1e40af; 44 | --cdp-color-blue-900: #1e3a8a; 45 | --cdp-color-blue-950: #172554; 46 | 47 | --cdp-color-green: var(--cdp-color-green-500); 48 | --cdp-color-green-50: #f0fdf4; 49 | --cdp-color-green-100: #dcfce7; 50 | --cdp-color-green-200: #bbf7d0; 51 | --cdp-color-green-300: #86efac; 52 | --cdp-color-green-400: #4ade80; 53 | --cdp-color-green-500: #22c55e; 54 | --cdp-color-green-600: #16a34a; 55 | --cdp-color-green-700: #15803d; 56 | --cdp-color-green-800: #166534; 57 | --cdp-color-green-900: #14532d; 58 | --cdp-color-green-950: #052e16; 59 | 60 | --cdp-color-yellow: var(--cdp-color-yellow-500); 61 | --cdp-color-yellow-50: #fefce8; 62 | --cdp-color-yellow-100: #fef9c3; 63 | --cdp-color-yellow-200: #fef08a; 64 | --cdp-color-yellow-300: #fde047; 65 | --cdp-color-yellow-400: #facc15; 66 | --cdp-color-yellow-500: #eab308; 67 | --cdp-color-yellow-600: #ca8a04; 68 | --cdp-color-yellow-700: #a16207; 69 | --cdp-color-yellow-800: #854d0e; 70 | --cdp-color-yellow-900: #713f12; 71 | --cdp-color-yellow-950: #422006; 72 | 73 | --cdp-font-size: var(--cdp-font-size-base); 74 | --cdp-font-size-xs: 10px; 75 | --cdp-font-size-sm: 12px; 76 | --cdp-font-size-base: 14px; 77 | --cdp-font-size-lg: 16px; 78 | --cdp-font-size-xl: 18px; 79 | 80 | --cdp-line-height: var(--cdp-line-height-base); 81 | --cdp-line-height-xs: 12px; 82 | --cdp-line-height-sm: 16px; 83 | --cdp-line-height-base: 20px; 84 | --cdp-line-height-lg: 24px; 85 | --cdp-line-height-xl: 28px; 86 | 87 | --cdp-gutter: 10px; 88 | 89 | --cdp-box-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); 90 | --cdp-box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 91 | --cdp-box-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); 92 | --cdp-box-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 93 | --cdp-box-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 94 | 95 | --cdp-box-shadow-around: 0px 0px 5px 0px rgb(0 0 0 / 0.1); 96 | --cdp-box-shadow-around-hover: 0px 0px 7px 2px rgb(0 0 0 / 0.1); 97 | 98 | --cdp-transition-all: all var(--cdp-transition-default); 99 | --cdp-transition-default: var(--cdp-transition-duration) var(--cdp-transition-timing-function); 100 | --cdp-transition-duration: 0.3s; 101 | --cdp-transition-timing-function: ease-in-out; 102 | } 103 | -------------------------------------------------------------------------------- /src/utils/useVersion.ts: -------------------------------------------------------------------------------- 1 | import type { Ref, ComputedRef } from "vue"; 2 | 3 | import { ref, computed, onMounted } from "vue"; 4 | import message from "@/utils/message"; 5 | 6 | const oneDay = 86400000; 7 | const regexp = /@version\s+(.+)\n/; 8 | 9 | const pluginMode: "tamper-monkey" | "web-extension" = import.meta.env.VITE_PLUGIN_MODE; 10 | const updateHref: string = import.meta.env.VITE_PLUGIN_UPDATE_URL; 11 | const localVersion: string = import.meta.env.VITE_VERSION; 12 | 13 | const useVersion = () => { 14 | if (pluginMode === "web-extension") { 15 | return { 16 | versionVisible: false, 17 | }; 18 | } 19 | // const remoteVersion = ref( 20 | // pluginMode === "web-extension" ? localVersion : localStorage.getItem("cdp_remoteVersion") || "" 21 | // ); 22 | const remoteVersion = ref(localStorage.getItem("cdp_remoteVersion")); 23 | const compareVersions = computed(() => { 24 | if (!remoteVersion.value || !localVersion) { 25 | return 0; 26 | } 27 | const localVersionList = localVersion.split(".").map((item) => parseInt(item) || 0); 28 | const remoteVersionList = remoteVersion.value.split(".").map((item) => parseInt(item) || 0); 29 | const len = Math.max(localVersionList.length, remoteVersionList.length); 30 | for (let index = 0; index < len; index++) { 31 | if (localVersionList[index] < remoteVersionList[index]) { 32 | return 1; 33 | } else if (localVersionList[index] > remoteVersionList[index]) { 34 | return -1; 35 | } 36 | } 37 | return 0; 38 | }); 39 | const hasNewVersion = computed(() => compareVersions.value === 1); 40 | const versionLoading = ref(false); 41 | 42 | let checkVersionTime: number = parseInt(localStorage.getItem("cdp_checkVersionTime") || "0"); 43 | 44 | let getRemoteVersionInstance: Promise; 45 | const getRemoteVersion = () => { 46 | // if (pluginMode === "web-extension") { 47 | // checkVersionTime = Date.now(); 48 | // localStorage.setItem("cdp_checkVersionTime", checkVersionTime + ""); 49 | // remoteVersion.value = localVersion; 50 | // localStorage.setItem("cdp_remoteVersion", remoteVersion.value); 51 | // return Promise.resolve(); 52 | // } 53 | if (versionLoading.value) { 54 | return getRemoteVersionInstance; 55 | } 56 | 57 | versionLoading.value = true; 58 | 59 | const now = Date.now(); 60 | 61 | getRemoteVersionInstance = fetch(`${import.meta.env.VITE_PLUGIN_META_URL}?t=${now}`) 62 | .then((res) => { 63 | if (res.ok) { 64 | return res.text(); 65 | } else { 66 | return Promise.reject(new Error("getRemoteVersion error")); 67 | } 68 | }) 69 | .then((res) => { 70 | checkVersionTime = now; 71 | localStorage.setItem("cdp_checkVersionTime", checkVersionTime + ""); 72 | remoteVersion.value = regexp.exec(res)?.[1] || ""; 73 | localStorage.setItem("cdp_remoteVersion", remoteVersion.value); 74 | }) 75 | .finally(() => { 76 | versionLoading.value = false; 77 | }); 78 | 79 | return getRemoteVersionInstance; 80 | }; 81 | 82 | let checkVersionInstance: Promise; 83 | const checkVersion = () => { 84 | if (versionLoading.value) { 85 | return checkVersionInstance; 86 | } 87 | checkVersionInstance = getRemoteVersion() 88 | .then(() => { 89 | message.success( 90 | compareVersions.value === 1 91 | ? `发现新版本:${remoteVersion.value}` 92 | : compareVersions.value === -1 93 | ? "当前版本已超过最新版本" 94 | : "当前版本已是最新版本" 95 | ); 96 | }) 97 | .catch(() => { 98 | message.error("查询更新失败"); 99 | }); 100 | return checkVersionInstance; 101 | }; 102 | 103 | onMounted(() => { 104 | if ( 105 | !checkVersionTime || 106 | Date.now() - new Date(checkVersionTime).setHours(0, 0, 0, 0) > oneDay 107 | ) { 108 | getRemoteVersion(); 109 | } 110 | }); 111 | 112 | return { 113 | updateHref, 114 | localVersion, 115 | remoteVersion, 116 | hasNewVersion, 117 | versionLoading, 118 | versionVisible: true, 119 | checkVersion, 120 | }; 121 | }; 122 | 123 | export interface IVersion { 124 | updateHref?: string; 125 | localVersion?: string; 126 | remoteVersion?: Ref; 127 | hasNewVersion?: ComputedRef; 128 | versionLoading?: Ref; 129 | versionVisible: boolean; 130 | checkVersion?: () => Promise; 131 | } 132 | 133 | export default useVersion; 134 | -------------------------------------------------------------------------------- /src/test/utils/reactFiber.test.ts: -------------------------------------------------------------------------------- 1 | // @vitest-environment happy-dom 2 | 3 | import type { Document } from "happy-dom"; 4 | import type { IReactFiberNode } from "@/utils/reactFiber"; 5 | 6 | import { test, expect, describe, beforeEach } from "vitest"; 7 | import { Window as HappyWindow } from "happy-dom"; 8 | import { findReactFiberNode, getRootReactContainer } from "@/utils/reactFiber"; 9 | 10 | class ReactFiberNode implements IReactFiberNode { 11 | child?: IReactFiberNode; 12 | return?: IReactFiberNode; 13 | sibling?: IReactFiberNode; 14 | pendingProps: any = {}; 15 | constructor(options: { 16 | child?: IReactFiberNode; 17 | return?: IReactFiberNode; 18 | sibling?: IReactFiberNode; 19 | pendingProps?: any; 20 | }) { 21 | this.child = options.child; 22 | this.return = options.return; 23 | this.sibling = options.sibling; 24 | this.pendingProps = options.pendingProps || this.pendingProps; 25 | } 26 | } 27 | 28 | class ReactFiberNodeList { 29 | head: ReactFiberNode; 30 | current: ReactFiberNode; 31 | constructor({ list, pendingProps }: { list?: { t?: 0 | 1 | 2; p?: any }[]; pendingProps?: any }) { 32 | this.head = new ReactFiberNode({ pendingProps }); 33 | this.current = this.head; 34 | if (list) { 35 | this.createList(list); 36 | } 37 | } 38 | addChild(pendingProps: any) { 39 | while (this.current.child) { 40 | this.current = this.current.child; 41 | } 42 | this.current.child = new ReactFiberNode({ return: this.current, pendingProps }); 43 | } 44 | addSibling(pendingProps: any) { 45 | while (this.current.sibling) { 46 | this.current = this.current.sibling; 47 | } 48 | this.current.sibling = new ReactFiberNode({ return: this.current, pendingProps }); 49 | } 50 | createList(list: { t?: 0 | 1 | 2; p?: any }[] = []) { 51 | list.forEach((item) => { 52 | if (item.t === 0) { 53 | if (this.current.return) { 54 | this.current = this.current.return; 55 | } 56 | } else if (item.t === 1) { 57 | this.addChild(item.p); 58 | } else if (item.t === 2) { 59 | this.addSibling(item.p); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | describe("findReactFiberNode", () => { 66 | test(`findReactFiberNode(node, (item: IReactFiberNode) => item.pendingProps.id === 5)?.pendingProps to equal "{ id: 5 }"`, () => { 67 | const node = new ReactFiberNodeList({ 68 | list: [ 69 | { t: 1, p: { id: 1 } }, 70 | { t: 1, p: { id: 2 } }, 71 | { t: 2, p: { id: 3 } }, 72 | { t: 1, p: { id: 4 } }, 73 | { t: 0 }, 74 | { t: 1, p: { id: 5 } }, 75 | ], 76 | pendingProps: { id: 0 }, 77 | }).head; 78 | 79 | expect( 80 | findReactFiberNode(node, (item: IReactFiberNode) => item.pendingProps.id === 5)?.pendingProps 81 | ).toEqual({ id: 5 }); 82 | }); 83 | 84 | test(`findReactFiberNode(node, (item: IReactFiberNode) => item.pendingProps.id === -1) to be undefined`, () => { 85 | const node = new ReactFiberNodeList({ 86 | list: [ 87 | { t: 1, p: { id: 1 } }, 88 | { t: 1, p: { id: 2 } }, 89 | { t: 2, p: { id: 3 } }, 90 | { t: 1, p: { id: 4 } }, 91 | { t: 0 }, 92 | { t: 1, p: { id: 5 } }, 93 | ], 94 | pendingProps: { id: 0 }, 95 | }).head; 96 | 97 | expect( 98 | findReactFiberNode(node, (item: IReactFiberNode) => item.pendingProps.id === -1) 99 | ).toBeUndefined(); 100 | }); 101 | }); 102 | 103 | describe("getRootReactContainer", () => { 104 | let window; 105 | let document: Document; 106 | const rootReactContainer = { pendingProps: { id: "root" } }; 107 | 108 | beforeEach(() => { 109 | // 创建一个 Happy DOM 的 Window 实例 110 | window = new HappyWindow(); 111 | document = window.document; 112 | 113 | const root = document.createElement("div"); 114 | Object.assign(root, { 115 | id: "root", 116 | __reactContainer: rootReactContainer, 117 | }); 118 | 119 | // 设置 HTML 结构 120 | document.body.append(root); 121 | 122 | // 将全局的 document 和 window 对象设置为 Happy DOM 的实例 123 | global.document = document as unknown as globalThis.Document; 124 | global.window = window as unknown as Window & typeof globalThis; 125 | }); 126 | 127 | test(`getRootReactContainer("#react", false) to be undefined`, () => { 128 | expect(getRootReactContainer("#react", false)).toBeUndefined(); 129 | }); 130 | 131 | test(`await getRootReactContainer("#root") to equal { pendingProps: { id: "root" } }`, async () => { 132 | expect(await getRootReactContainer("#root")).toEqual(rootReactContainer); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/provider/lib/ali/index.ts: -------------------------------------------------------------------------------- 1 | import type { IListItem, IOriginListItem } from "@/provider/interface"; 2 | 3 | import { 4 | Provider, 5 | LIST_ITEM_STATUS_READY, 6 | LIST_ITEM_STATUS_PENDING, 7 | LIST_ITEM_STATUS_SUCCESS, 8 | LIST_ITEM_STATUS_FAIL, 9 | } from "@/provider/interface"; 10 | import EnterComponent from "./EnterComponent.vue"; 11 | import querySelector from "@/utils/querySelector"; 12 | import fileNameParse from "@/utils/fileNameParse"; 13 | import { ROOT_ELEMENT_INSERT_METHOD_APPEND } from "@/provider/interface"; 14 | import { findReactFiberNode, getRootReactContainer } from "@/utils/reactFiber"; 15 | 16 | export default class ProviderAli extends Provider { 17 | static test = () => 18 | /^https:\/\/www\.ali(pan|yundrive)\.com\/drive\/file\/(all|backup|resource)/.test( 19 | location.href 20 | ); 21 | 22 | type = "ali"; 23 | rootElementId = "cloud-disk-plugin"; 24 | rootElementInsertTarget = "[class^=nav-tab-content--]"; 25 | rootElementInsertMethod = ROOT_ELEMENT_INSERT_METHOD_APPEND; 26 | 27 | EnterComponent = () => EnterComponent; 28 | 29 | private _rootReactContainerSelectors = "#root"; 30 | 31 | async getOriginList() { 32 | const rootReactContainer = await getRootReactContainer(this._rootReactContainerSelectors, true); 33 | 34 | const reactFiberNode = findReactFiberNode( 35 | rootReactContainer, 36 | (node) => node.pendingProps?.localStore?.listModel?.listModel 37 | ); 38 | 39 | if (!reactFiberNode) { 40 | return Promise.reject(); 41 | } 42 | 43 | const listModel = reactFiberNode.pendingProps.localStore.listModel.listModel; 44 | 45 | while (listModel.nextMarker) { 46 | await listModel.loadMoreData(); 47 | } 48 | 49 | const originList = reactFiberNode.pendingProps.localStore.list; 50 | 51 | if (!originList) { 52 | return Promise.reject(); 53 | } 54 | 55 | const result: IOriginListItem[] = []; 56 | let index = 0; 57 | originList.forEach((item: any) => { 58 | if (item.type === "file") { 59 | result.push({ 60 | id: item.fileId, 61 | index: index++, 62 | fullFileName: item.name, 63 | ...fileNameParse(item.name), 64 | }); 65 | } 66 | }); 67 | 68 | return result; 69 | } 70 | 71 | async renameRequest(data: IListItem[]) { 72 | const rootReactContainer = await getRootReactContainer(this._rootReactContainerSelectors, true); 73 | 74 | const reactFiberNode = findReactFiberNode( 75 | rootReactContainer, 76 | (node) => node.pendingProps?.localStore?.list 77 | ); 78 | 79 | if (!reactFiberNode) { 80 | return Promise.reject(); 81 | } 82 | 83 | const originListMap = new Map( 84 | reactFiberNode.pendingProps.localStore.list.map((item: any) => [item.fileId, item]) 85 | ); 86 | 87 | const taskList: { item: IListItem; originItem: any }[] = []; 88 | 89 | data.forEach((item) => { 90 | const originItem: any = originListMap.get(item.id); 91 | if (originItem) { 92 | item.status = LIST_ITEM_STATUS_READY; 93 | return taskList.push({ item, originItem }); 94 | } else { 95 | item.status = LIST_ITEM_STATUS_FAIL; 96 | } 97 | }); 98 | 99 | while (taskList.length) { 100 | const { item, originItem } = taskList.shift() as { 101 | item: IListItem; 102 | originItem: any; 103 | }; 104 | 105 | item.status = LIST_ITEM_STATUS_PENDING; 106 | this._updateStatus(); 107 | try { 108 | await originItem.rename(item.newFileName); 109 | if (originItem.name === item.newFileName) { 110 | item.status = LIST_ITEM_STATUS_SUCCESS; 111 | } else { 112 | item.status = LIST_ITEM_STATUS_FAIL; 113 | } 114 | } catch (error) { 115 | item.status = LIST_ITEM_STATUS_FAIL; 116 | } 117 | this._updateStatus(); 118 | } 119 | 120 | return Promise.resolve(); 121 | } 122 | 123 | async refresh() { 124 | const rootReactContainer = await getRootReactContainer(this._rootReactContainerSelectors, true); 125 | 126 | const reactFiberNode = findReactFiberNode( 127 | rootReactContainer, 128 | (node) => node.pendingProps?.localStore?.listModel?.reload 129 | ); 130 | 131 | if (!reactFiberNode) { 132 | location.reload(); 133 | return Promise.resolve(); 134 | } 135 | 136 | const tbodyScroller = querySelector( 137 | "[class^=node-list-table-view--]>[class^=tbody--]>div>[class^=scroller---]" 138 | ); 139 | 140 | if (tbodyScroller) { 141 | tbodyScroller.scrollTop = 0; 142 | } 143 | 144 | const reload = reactFiberNode.pendingProps.localStore.listModel.reload; 145 | 146 | return reload(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/components/Component/ComponentCheckbox.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 63 | 64 | 169 | -------------------------------------------------------------------------------- /src/provider/lib/quark/index.ts: -------------------------------------------------------------------------------- 1 | import type { IListItem, IOriginListItem } from "@/provider/interface"; 2 | 3 | import { 4 | Provider, 5 | LIST_ITEM_STATUS_READY, 6 | LIST_ITEM_STATUS_PENDING, 7 | LIST_ITEM_STATUS_SUCCESS, 8 | LIST_ITEM_STATUS_FAIL, 9 | } from "@/provider/interface"; 10 | import EnterComponent from "./EnterComponent.vue"; 11 | import fileNameParse from "@/utils/fileNameParse"; 12 | import { ROOT_ELEMENT_INSERT_METHOD_PREPEND } from "@/provider/interface"; 13 | import sleep from "@/utils/sleep"; 14 | import { findReactFiberNode, getRootReactContainer } from "@/utils/reactFiber"; 15 | 16 | export default class ProviderQuark extends Provider { 17 | static test = () => /^https:\/\/pan.quark.cn\/list#\/list\//.test(location.href); 18 | 19 | type = "quark"; 20 | rootElementId = "cloud-disk-plugin"; 21 | rootElementInsertTarget = 22 | "#ice-container .section-main > .section-header.list-header > .btn-operate > .btn-main"; 23 | 24 | rootElementInsertMethod = ROOT_ELEMENT_INSERT_METHOD_PREPEND; 25 | 26 | EnterComponent = () => EnterComponent; 27 | 28 | private _rootReactContainerSelectors = "#ice-container"; 29 | 30 | async getOriginList() { 31 | const rootReactContainer = await getRootReactContainer(this._rootReactContainerSelectors, true); 32 | 33 | const reactFiberNode = findReactFiberNode( 34 | rootReactContainer, 35 | (node) => node.pendingProps?.store?.getState?.()?.file?.listType 36 | ); 37 | 38 | if (!reactFiberNode) { 39 | return Promise.reject(); 40 | } 41 | 42 | let state = reactFiberNode.pendingProps.store.getState(); 43 | 44 | const hasMore = 45 | state.file[state.file.listType].list.length !== state.file[state.file.listType].total; 46 | if (hasMore) { 47 | await reactFiberNode.pendingProps.store.dispatch.file.loadAllFiles({ 48 | params: { 49 | needTotalNum: 1, 50 | page: 1, 51 | size: state.file[state.file.listType].total, 52 | sort: state.file[state.file.listType].sort, 53 | }, 54 | fid: state.file[state.file.listType].list[0].pdir_fid, 55 | listType: state.file.listType, 56 | }); 57 | 58 | do { 59 | await sleep(300); 60 | state = reactFiberNode.pendingProps.store.getState(); 61 | } while ( 62 | state.file[state.file.listType].list.length !== state.file[state.file.listType].total 63 | ); 64 | } 65 | const originList = state.file[state.file.listType].list; 66 | if (!originList) { 67 | return Promise.reject(); 68 | } 69 | 70 | const result: IOriginListItem[] = []; 71 | let index = 0; 72 | originList.forEach((item: any) => { 73 | if (item.file) { 74 | result.push({ 75 | id: item.fid, 76 | index: index++, 77 | fullFileName: item.file_name, 78 | ...fileNameParse(item.file_name), 79 | }); 80 | } 81 | }); 82 | 83 | return result; 84 | } 85 | 86 | async renameRequest(data: IListItem[]) { 87 | const rootReactContainer = await getRootReactContainer(this._rootReactContainerSelectors, true); 88 | 89 | const reactFiberNode = findReactFiberNode( 90 | rootReactContainer, 91 | (node) => node.pendingProps?.rename 92 | ); 93 | 94 | if (!reactFiberNode) { 95 | return Promise.reject(); 96 | } 97 | 98 | const taskList: IListItem[] = []; 99 | 100 | data.forEach((item) => { 101 | item.status = LIST_ITEM_STATUS_READY; 102 | taskList.push(item); 103 | }); 104 | 105 | while (taskList.length) { 106 | const item = taskList.shift() as IListItem; 107 | 108 | item.status = LIST_ITEM_STATUS_PENDING; 109 | this._updateStatus(); 110 | try { 111 | const res = await reactFiberNode.pendingProps.rename({ 112 | fid: item.id, 113 | fileName: item.newFileName, 114 | }); 115 | if (res.status === 200 && res.code === 0) { 116 | item.status = LIST_ITEM_STATUS_SUCCESS; 117 | } else { 118 | item.status = LIST_ITEM_STATUS_FAIL; 119 | } 120 | } catch (error) { 121 | item.status = LIST_ITEM_STATUS_FAIL; 122 | } 123 | this._updateStatus(); 124 | } 125 | 126 | return this.refresh(); 127 | } 128 | 129 | async refresh() { 130 | const rootReactContainer = await getRootReactContainer(this._rootReactContainerSelectors, true); 131 | 132 | const reactFiberNode = findReactFiberNode( 133 | rootReactContainer, 134 | (node) => node.pendingProps?.onRefresh 135 | ); 136 | 137 | if (!reactFiberNode) { 138 | location.reload(); 139 | return Promise.resolve(); 140 | } 141 | 142 | const reload = reactFiberNode.pendingProps.onRefresh; 143 | 144 | return reload(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/components/Component/ComponentRadio.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 71 | 72 | 182 | -------------------------------------------------------------------------------- /src/provider/lib/pikpak/index.ts: -------------------------------------------------------------------------------- 1 | import type { IListItem, IOriginListItem } from "@/provider/interface"; 2 | 3 | import { 4 | Provider, 5 | LIST_ITEM_STATUS_READY, 6 | LIST_ITEM_STATUS_SUCCESS, 7 | LIST_ITEM_STATUS_FAIL, 8 | ROOT_ELEMENT_INSERT_METHOD_APPEND, 9 | } from "@/provider/interface"; 10 | import EnterComponent from "./EnterComponent.vue"; 11 | import fileNameParse from "@/utils/fileNameParse"; 12 | import querySelector from "@/utils/querySelector"; 13 | 14 | interface renameFetchHeaders { 15 | Authorization?: string; 16 | "X-Device-Id"?: string; 17 | "X-Captcha-Token"?: string; 18 | } 19 | 20 | export default class ProviderQuark extends Provider { 21 | static test = () => /^https:\/\/mypikpak.com\/drive\/all/.test(location.href); 22 | 23 | type = "pikpak"; 24 | rootElementId = "cloud-disk-plugin"; 25 | rootElementInsertTarget = "#app .sidebar .overflow-middle-bar .nav"; 26 | 27 | rootElementInsertMethod = ROOT_ELEMENT_INSERT_METHOD_APPEND; 28 | 29 | EnterComponent = () => EnterComponent; 30 | 31 | async getOriginList() { 32 | const vue = this._getVue(); 33 | 34 | const originList = vue?.config?.globalProperties?.$pinia?.state?.value?.file?.dbFiles; 35 | 36 | if (!originList) { 37 | return Promise.reject(); 38 | } 39 | 40 | const result: IOriginListItem[] = []; 41 | let index = 0; 42 | originList.forEach((item: any) => { 43 | if (item.kind === "drive#file") { 44 | result.push({ 45 | id: item.id, 46 | index: index++, 47 | fullFileName: item.name, 48 | ...fileNameParse(item.name), 49 | }); 50 | } 51 | }); 52 | return result; 53 | } 54 | 55 | async renameRequest(data: IListItem[]) { 56 | const tasks = data.map((item) => { 57 | item.status = LIST_ITEM_STATUS_READY; 58 | return item; 59 | }); 60 | const captchaKey = Object.keys(localStorage).find((item) => item.startsWith("captcha_")); 61 | const credentialsKey = Object.keys(localStorage).find((item) => 62 | item.startsWith("credentials_") 63 | ); 64 | if (!captchaKey || !credentialsKey) { 65 | return Promise.reject(); 66 | } 67 | try { 68 | const headers: renameFetchHeaders = {}; 69 | const captcha = JSON.parse(localStorage[captchaKey]); 70 | headers["X-Captcha-Token"] = captcha.captcha_token; 71 | const credentials = JSON.parse(localStorage[credentialsKey]); 72 | headers.Authorization = `${credentials.token_type} ${credentials.access_token}`; 73 | if (localStorage.deviceid) { 74 | const deviceid = localStorage.deviceid.match(/^.+\.(.{32})/)?.[1]; 75 | if (deviceid) { 76 | headers["X-Device-Id"] = deviceid; 77 | } 78 | } 79 | 80 | while (tasks.length) { 81 | const task = tasks.shift() as IListItem; 82 | await this.renameFetch(task, headers); 83 | } 84 | this.refresh(); 85 | return Promise.resolve(); 86 | } catch (error) { 87 | return Promise.reject(error); 88 | } 89 | } 90 | 91 | renameFetch(data: IListItem, headers: renameFetchHeaders) { 92 | this._updateStatus(); 93 | return fetch(`https://api-drive.mypikpak.com/drive/v1/files/${data.id}`, { 94 | body: JSON.stringify({ name: data.newFileName }), 95 | method: "PATCH", 96 | headers: { 97 | "Content-Type": "application/json", 98 | ...headers, 99 | }, 100 | }) 101 | .then((res) => { 102 | if (res.ok) { 103 | return res.json(); 104 | } else { 105 | return Promise.reject(res); 106 | } 107 | }) 108 | .then((res) => { 109 | if (res.error_code || res.error_description) { 110 | return Promise.reject(res); 111 | } 112 | data.status = LIST_ITEM_STATUS_SUCCESS; 113 | }) 114 | .catch(() => { 115 | data.status = LIST_ITEM_STATUS_FAIL; 116 | }) 117 | .finally(() => { 118 | this._updateStatus(); 119 | }); 120 | } 121 | 122 | async refresh() { 123 | const vue = this._getVue(); 124 | const router = vue.config.globalProperties.$router; 125 | if (!router.hasRoute("cdp_refresh")) { 126 | const routeAll = router.options.routes.find((item: any) => item.name === "all"); 127 | router.addRoute({ 128 | path: "/cdp_refresh", 129 | name: "cdp_refresh", 130 | meta: { ...routeAll.meta, noAuth: true }, 131 | component: () => 132 | Promise.resolve({ 133 | render() { 134 | return ""; 135 | }, 136 | beforeRouteEnter(to: any, from: any, next: any) { 137 | next((vm: any) => { 138 | vm.$router.replace(from.fullPath); 139 | }); 140 | }, 141 | }), 142 | }); 143 | } 144 | router.push("/cdp_refresh"); 145 | } 146 | 147 | private _vueInstance?: any; 148 | private _getVue(): any | undefined { 149 | if (this._vueInstance) { 150 | return this._vueInstance; 151 | } 152 | 153 | const element: any = querySelector("#app"); 154 | 155 | if (!element?.__vue_app__) { 156 | return; 157 | } 158 | 159 | this._vueInstance = element.__vue_app__; 160 | 161 | return this._vueInstance; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/provider/lib/baidu/index.ts: -------------------------------------------------------------------------------- 1 | import type { IListItem, IOriginListItem } from "@/provider/interface"; 2 | 3 | import { 4 | Provider, 5 | LIST_ITEM_STATUS_READY, 6 | LIST_ITEM_STATUS_PENDING, 7 | LIST_ITEM_STATUS_SUCCESS, 8 | LIST_ITEM_STATUS_FAIL, 9 | ROOT_ELEMENT_INSERT_METHOD_APPEND, 10 | } from "@/provider/interface"; 11 | import EnterComponent from "./EnterComponent.vue"; 12 | import sleep from "@/utils/sleep"; 13 | import querySelector from "@/utils/querySelector"; 14 | import fileNameParse from "@/utils/fileNameParse"; 15 | 16 | export default class ProviderBaidu extends Provider { 17 | static test = () => 18 | /^https:\/\/pan\.baidu\.com\/disk\/main(.+)?#\/index\?category=all/.test(location.href); 19 | 20 | type = "baidu"; 21 | rootElementId = "cloud-disk-plugin"; 22 | rootElementInsertTarget = ".wp-s-aside-nav__main-top"; 23 | rootElementInsertMethod = ROOT_ELEMENT_INSERT_METHOD_APPEND; 24 | maxTask = 50; 25 | 26 | EnterComponent = () => EnterComponent; 27 | 28 | async getOriginList() { 29 | const vue = this._getVue(); 30 | 31 | while (vue.listConf.hasMore) { 32 | vue?.getNextData(); 33 | while (vue?.listLoading) { 34 | await sleep(100); 35 | } 36 | } 37 | 38 | const originList = vue?.fileList; 39 | 40 | if (!originList) { 41 | return Promise.reject(); 42 | } 43 | 44 | const result: IOriginListItem[] = []; 45 | let index = 0; 46 | originList.forEach((item: any) => { 47 | if (item.isdir === 0) { 48 | result.push({ 49 | id: item.fs_id, 50 | path: item.path, 51 | index: index++, 52 | fullFileName: item.formatName, 53 | ...fileNameParse(item.formatName), 54 | }); 55 | } 56 | }); 57 | 58 | return result; 59 | } 60 | 61 | async renameRequest(data: IListItem[]) { 62 | const token = this._vueInstance?.yunData?.bdstoken; 63 | if (!token) { 64 | return Promise.reject(); 65 | } 66 | // 任务切片 67 | const tasks: IListItem[][] = []; 68 | data.forEach((item, index) => { 69 | const key = Math.floor(index / this.maxTask); 70 | if (!tasks[key]) { 71 | tasks[key] = []; 72 | } 73 | item.status = LIST_ITEM_STATUS_READY; 74 | tasks[key].push(item); 75 | }); 76 | 77 | try { 78 | while (tasks.length) { 79 | const task = tasks.shift() as IListItem[]; 80 | await this.renameTask(task, token); 81 | } 82 | this.refresh(); 83 | return Promise.resolve(); 84 | } catch (error) { 85 | return Promise.reject(error); 86 | } 87 | } 88 | 89 | async renameTask(data: IListItem[], token: string) { 90 | const body = new FormData(); 91 | const filelist = data.map((item) => { 92 | item.status = LIST_ITEM_STATUS_PENDING; 93 | return { 94 | id: item.id, 95 | path: item.path, 96 | newname: item.newFileName, 97 | }; 98 | }); 99 | this._updateStatus(); 100 | body.append("filelist", JSON.stringify(filelist)); 101 | this._vueInstance.editLoading = true; 102 | return fetch( 103 | `https://pan.baidu.com/api/filemanager?async=2&onnest=fail&opera=rename&bdstoken=${token}&clienttype=0&app_id=250528&web=1`, 104 | { 105 | body, 106 | method: "POST", 107 | } 108 | ) 109 | .then((res) => { 110 | if (res.ok) { 111 | return res.json(); 112 | } else { 113 | data.forEach((item) => { 114 | item.status = LIST_ITEM_STATUS_FAIL; 115 | }); 116 | return Promise.reject(res); 117 | } 118 | }) 119 | .then(async (res: any) => { 120 | if (res.errno !== 0) { 121 | data.forEach((item) => { 122 | item.status = LIST_ITEM_STATUS_FAIL; 123 | }); 124 | return Promise.reject(res); 125 | } 126 | this._vueInstance.renameFileList = filelist; 127 | const result = res.taskid ? await this.waitPollTaskResult(res) : res; 128 | 129 | data.forEach((item) => { 130 | item.status = LIST_ITEM_STATUS_SUCCESS; 131 | }); 132 | return result; 133 | }); 134 | } 135 | 136 | async refresh() { 137 | const vue = this._getVue(); 138 | 139 | if (!vue?.reloadList) { 140 | location.reload(); 141 | return Promise.resolve(); 142 | } 143 | 144 | vue.reloadList(); 145 | 146 | return new Promise((resolve) => { 147 | let count = 20; 148 | const timer = setInterval(() => { 149 | if (vue.$store.state.fileList.loadingList === false || --count < 0) { 150 | resolve(); 151 | clearInterval(timer); 152 | } 153 | }, 500); 154 | }); 155 | } 156 | 157 | async waitPollTaskResult(res: any): Promise { 158 | this._vueInstance?.pollTask(res.taskid); 159 | // 延迟时间,等待百度网盘更新,对比更新结果 160 | while (this._vueInstance?.editLoading) { 161 | await sleep(100); 162 | } 163 | 164 | // let count = 10; 165 | // const timeout = Math.max(data.length * 50, 1000); 166 | 167 | // while (count-- > 0) { 168 | // await this.refresh(); 169 | // const originList: IOriginListItem[] = await this.getOriginList(); 170 | // const originListMap = new Map( 171 | // originList.map((item) => [item.id, item.fullFileName]) 172 | // ); 173 | // let isMatched = true; 174 | // data.forEach((item) => { 175 | // if ( 176 | // originListMap.has(item.id) && 177 | // originListMap.get(item.id) === item.newFileName 178 | // ) { 179 | // item.status = LIST_ITEM_STATUS_SUCCESS; 180 | // } else { 181 | // isMatched = false; 182 | // } 183 | // }); 184 | // if (!isMatched) { 185 | // await sleep(timeout); 186 | // } else { 187 | // return res; 188 | // } 189 | // } 190 | } 191 | 192 | private _vueInstance?: any; 193 | private _getVue(): any | undefined { 194 | if (this._vueInstance) { 195 | return this._vueInstance; 196 | } 197 | 198 | const element: any = querySelector(".nd-main-list, .nd-new-main-list"); 199 | 200 | if (!element?.__vue__) { 201 | return; 202 | } 203 | 204 | this._vueInstance = element.__vue__; 205 | 206 | return this._vueInstance; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/components/Component/ComponentInput.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 146 | 147 | 221 | -------------------------------------------------------------------------------- /src/utils/message.ts: -------------------------------------------------------------------------------- 1 | import type { VNode, AppContext } from "vue"; 2 | 3 | import "@/style/message.css"; 4 | import { render, createVNode } from "vue"; 5 | import ComponentMessage from "@/components/Component/ComponentMessage.vue"; 6 | 7 | type TMessageOptionsType = "info" | "error" | "success" | "warning" | "loading"; 8 | 9 | const MESSAGE_TYPE_INFO: TMessageOptionsType = "info"; 10 | const MESSAGE_TYPE_ERROR: TMessageOptionsType = "error"; 11 | const MESSAGE_TYPE_SUCCESS: TMessageOptionsType = "success"; 12 | const MESSAGE_TYPE_WARNING: TMessageOptionsType = "warning"; 13 | const MESSAGE_TYPE_LOADING: TMessageOptionsType = "loading"; 14 | 15 | type TMessageOptionsMessage = string | VNode | (() => VNode); 16 | 17 | type TMessageOptionsPosition = 18 | | "top-left" 19 | | "top-center" 20 | | "top-right" 21 | | "center" 22 | | "bottom-left" 23 | | "bottom-center" 24 | | "bottom-right"; 25 | 26 | interface IMessageComponentOptions { 27 | type?: TMessageOptionsType; 28 | icon?: string; 29 | width?: string; 30 | message?: TMessageOptionsMessage; 31 | duration?: number; 32 | showClose?: boolean; 33 | customClass?: string; 34 | dangerouslyUseHTMLString?: boolean; 35 | onClose?: () => void; 36 | onDestroy?: () => void; 37 | } 38 | 39 | interface IMessageFunctionSpecialOptions { 40 | ctx?: AppContext; 41 | zIndex?: number; 42 | hasMask?: boolean; 43 | position?: TMessageOptionsPosition; 44 | } 45 | 46 | type TMessageFunctionOptions = IMessageComponentOptions & IMessageFunctionSpecialOptions; 47 | 48 | type TMessageTypeFunctionOptions = Omit; 49 | 50 | type TDefaultMessageFunctionOptions = TMessageFunctionOptions & 51 | Required>; 52 | 53 | interface IMessageInstance { 54 | id: string; 55 | type?: TMessageOptionsType; 56 | message?: TMessageOptionsMessage; 57 | close: () => void; 58 | } 59 | 60 | interface IMessageTypeFunction { 61 | (message: TMessageOptionsMessage, options?: TMessageFunctionOptions): IMessageInstance; 62 | } 63 | 64 | interface IMessage { 65 | (options: TMessageFunctionOptions): IMessageInstance; 66 | info: IMessageTypeFunction; 67 | error: IMessageTypeFunction; 68 | success: IMessageTypeFunction; 69 | warning: IMessageTypeFunction; 70 | loading: IMessageTypeFunction; 71 | closeAll: () => void; 72 | setDefault: (options: TMessageFunctionOptions) => void; 73 | _context?: AppContext | null; 74 | } 75 | 76 | const instances: IMessageInstance[] = []; 77 | 78 | interface iPositionMaskContainer { 79 | el: HTMLElement; 80 | count: number; 81 | } 82 | 83 | interface iPositionContainer { 84 | noMask?: iPositionMaskContainer; 85 | hasMask?: iPositionMaskContainer; 86 | } 87 | 88 | const containers: { [key in TMessageOptionsPosition]?: iPositionContainer } = {}; 89 | 90 | const defaultOptions: TDefaultMessageFunctionOptions = { 91 | zIndex: 2000, 92 | position: "top-center", 93 | }; 94 | 95 | let globalOptions: TMessageFunctionOptions = {}; 96 | 97 | let seed = 0; 98 | 99 | const message: IMessage = (options: TMessageFunctionOptions) => { 100 | const id = "component-message-" + seed++; 101 | const { zIndex, hasMask, position, ...props }: TDefaultMessageFunctionOptions = { 102 | ...defaultOptions, 103 | ...globalOptions, 104 | ...options, 105 | }; 106 | 107 | const maskKey = hasMask ? "hasMask" : "noMask"; 108 | 109 | if (!containers[position]) { 110 | containers[position] = {}; 111 | } 112 | 113 | const positionContainer = containers[position] as iPositionContainer; 114 | 115 | const hsaPositionMaskContainer = !!positionContainer[maskKey]; 116 | 117 | if (!hsaPositionMaskContainer) { 118 | const customClass = ["component-message-area", "is-" + position]; 119 | if (hasMask) { 120 | customClass.push("has-mask"); 121 | } 122 | const el = document.createElement("div"); 123 | 124 | el.className = customClass.join(" "); 125 | 126 | document.body.appendChild(el); 127 | 128 | positionContainer[maskKey] = { el, count: 1 }; 129 | } 130 | 131 | const positionMaskContainer = positionContainer[maskKey] as iPositionMaskContainer; 132 | 133 | if (hsaPositionMaskContainer) { 134 | positionMaskContainer.count++; 135 | } 136 | 137 | if (zIndex) { 138 | positionMaskContainer.el.style.zIndex = String(options.zIndex); 139 | } 140 | 141 | const onDestroy = props.onDestroy; 142 | props.onDestroy = () => { 143 | positionMaskContainer.count--; 144 | if (positionMaskContainer.count === 0) { 145 | delete positionContainer[maskKey]; 146 | positionMaskContainer.el.remove(); 147 | } 148 | if (onDestroy) { 149 | onDestroy(); 150 | } 151 | render(null, div); 152 | }; 153 | 154 | const vm = createVNode(ComponentMessage, props); 155 | const div = document.createElement("div"); 156 | 157 | vm.appContext = options.ctx || message._context || null; 158 | 159 | render(vm, div); 160 | 161 | // for position of bottom-* 162 | if (position.startsWith("bottom") && positionMaskContainer.el.firstChild) { 163 | positionMaskContainer.el.insertBefore( 164 | div.firstElementChild!, 165 | positionMaskContainer.el.firstChild 166 | ); 167 | } else { 168 | positionMaskContainer.el.appendChild(div.firstElementChild!); 169 | } 170 | 171 | const instance = { 172 | id, 173 | type: props.type, 174 | message: props.message, 175 | close() { 176 | vm?.component?.exposed?.close(); 177 | }, 178 | }; 179 | 180 | instances.push(instance); 181 | return instance; 182 | }; 183 | 184 | const messageFunction = message; 185 | 186 | message.info = (message: TMessageOptionsMessage, options?: TMessageTypeFunctionOptions) => 187 | messageFunction({ ...options, type: MESSAGE_TYPE_INFO, message }); 188 | message.error = (message: TMessageOptionsMessage, options?: TMessageTypeFunctionOptions) => 189 | messageFunction({ ...options, type: MESSAGE_TYPE_ERROR, message }); 190 | message.success = (message: TMessageOptionsMessage, options?: TMessageTypeFunctionOptions) => 191 | messageFunction({ ...options, type: MESSAGE_TYPE_SUCCESS, message }); 192 | message.warning = (message: TMessageOptionsMessage, options?: TMessageTypeFunctionOptions) => 193 | messageFunction({ ...options, type: MESSAGE_TYPE_WARNING, message }); 194 | message.loading = (message: TMessageOptionsMessage, options?: TMessageTypeFunctionOptions) => 195 | messageFunction({ ...options, type: MESSAGE_TYPE_LOADING, message }); 196 | 197 | message.closeAll = () => { 198 | for (let i = instances.length - 1; i >= 0; i--) { 199 | instances[i].close(); 200 | } 201 | }; 202 | 203 | // global options 204 | message.setDefault = (options: TMessageFunctionOptions) => { 205 | globalOptions = { ...options }; 206 | }; 207 | 208 | export default message; 209 | -------------------------------------------------------------------------------- /src/components/RenameControl.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 150 | 151 | 230 | -------------------------------------------------------------------------------- /src/components/Component/ComponentMessage.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 201 | 288 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type { UserConfig } from "vite"; 4 | 5 | import { URL, fileURLToPath } from "node:url"; 6 | import { defineConfig } from "vite"; 7 | import vue from "@vitejs/plugin-vue"; 8 | import monkey, { cdn } from "vite-plugin-monkey"; 9 | import webExtension from "vite-plugin-web-extension"; 10 | import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; 11 | import packageJson from "./package.json" assert { type: "json" }; 12 | 13 | const urlSchemes = [ 14 | "https://pan.quark.cn/*", 15 | "https://pan.baidu.com/*", 16 | "https://www.alipan.com/*", 17 | "https://www.aliyundrive.com/*", 18 | "https://mypikpak.com/*" 19 | ]; 20 | 21 | export default defineConfig(({ mode, command, isPreview, isSsrBuild }) => { 22 | console.log("mode: " + mode); // 'tamper-monkey' 23 | console.log("command: " + command); // 'serve' 24 | console.log("isPreview: " + isPreview); // false 25 | console.log("isSsrBuild: " + isSsrBuild); // false 26 | 27 | const config: UserConfig = { 28 | test: { 29 | testTimeout: 60000, 30 | }, 31 | server: { 32 | port: 10086, 33 | }, 34 | preview: { 35 | port: 10010, 36 | }, 37 | plugins: [], 38 | resolve: { 39 | alias: { 40 | "@": fileURLToPath(new URL("./src", import.meta.url)), 41 | }, 42 | }, 43 | }; 44 | 45 | if (mode === "tamper-monkey") { 46 | const commandType = command === "serve" ? "dev" : "build"; 47 | 48 | config.build = { outDir: `dist/${commandType}/tamper-monkey` }; 49 | config.publicDir = "public/tamper-monkey"; 50 | config.plugins?.push( 51 | vue(), 52 | monkey({ 53 | entry: "src/main.ts", 54 | build: { 55 | fileName: `${packageJson.name}-tamper-monkey@${packageJson.version}.js`, 56 | autoGrant: false, 57 | externalGlobals: { 58 | vue: cdn.jsdelivr("Vue", "dist/vue.global.prod.js"), 59 | }, 60 | }, 61 | userscript: { 62 | name: packageJson.cnName, 63 | icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAACUpJREFUeJzlm3lzU2UUxnEZxhH1f78AX6AK7oIi4gAuuO+iMygqyqKOS10HUUSxilKoIDsFtYCgoixSWWQVrFWg+5KUdN/bdD+e35sbTdPc9jZNm3RyZs6khOTe9znvOc9Z3psRIwZZEt7Pulx1mupc1UWqG1T3qZ5Vrbf0rPXeBuszc63vXD7Y64u46KLPVx2rOl81Q1UGqBnWtbjm+dHGZyu6uEtVX1ItiABoOy2w7nFptPH+J7qYi1VfVq0YRODBWmHd8+JoAj9P9WFV1xACD1aXtYbzhhr8aNX0KAIPVtYyeqjAz1RtjgHQwcqaZg4m8ItU18UA0L6UNV4UafCjVPfGADinylpHRQr8hao7YwBUf5U1XxgJAyTHAJhwNXmg4GfGAIiBanjEqF8co9oaAwAGqmAY01/wl6nmxcDiI6Vguaw/Blg51Iu8ckGW3JmcL69tPSdLfq2QjceqZeff9fLDX3Xy9aEqmf9jqczc4JLbv8yXqxdmh3OPlU7BT1DtGirgYz7IkqfWFsu6I9VyvLBJCqtapaKhXRpaOqWto0ta27uktrlDSmraJLusRU4WN8u2P2tl1ia3XL8opz/3AtOEvsCPVM0aKvATk3LNbv/papY6b4d0dolUNXbIX26v7D3TIJtP1Mg3qvz9d4nXGKZDP9Tc1ik55S2y/mi13JdSoEZ07BFgG9mbAeYMFfgHviqUradqpazeByqvolVW/14lsze75ZGvi+TuZQUy+Yt8o/z9qL43Y71LFu0qMwZrUc+oU884nN9kQuMa52Exxw78JQlD1NI+tqpI9uiu4uZNrZ2SdrJWntvokkmf5cmtqk8r0Hd/KJWl6RWyZF+FvLHtnPnOTYtzZbwqBlpxsEo8de3SqmGCQWasK5arPnRkBDBeEsoA8yIF8NqPcmSqktX0NcXyfKrbLH7ykjxDXnfo+7/8U2+A4+6AvCu5QG77PM8Q3b6sBskqbRG3xnxlY7tx+yLlhX/OeeX3vEb5XEMGIuTzb37vkbzyVsMVB3Mb5f6UQrligaM1zgsGf4Fq0UCB46qLd5fL3rMNhqxOe7ySo8TF4v8oapZf9f392Y2G1NCkveVm1wmHLRoOLgUNGDvp0v8qV4NwjWfU7Sd8mms8BWN5lRfWHq4213OwVrBeEGiAKQMBfoOy8Yc/l5l4JKbbO30giFPcvMP6N68A5JXF4hWPrCySXafrzefsoXcXrgFRYoSJSXmySrmjpqlDyvXec78tcZompwQaIDVc8OM/yZWU/ZVSXNVmgJdqXH6vaQp3flEJ7VmNbXI7ufxYQZPUezslUxl9+uoimaIE92NmnTRqOPRXMCJuf+/yAhNCR/XaGIbagRBxsPbUwNRXH16sZ8uy3yrFU+tz3d26k/N0B1jAdcoDGIdY5RXXfHBFoTHGLOWFcZ/kyKd7ys2uhSu4/dL0SuMFeCDGr9HQgnfG9p0awTwSA4wLd/ff3u6RgspWs/PbM+oM2QEMFielHchplBMa+7x+daBSntBdxxi4KIY5oiHjD49wJV/TJ/edqt6UodmAyyXrptys/OAAwzgMkBgOeNz3kLIyOw87T19dbFJYigKlaCFH+7HxWq0xiuu/s8NjjESswu4DFYz//k+l5prUFWQXSPjOpY7CIBEDpPUXPNZdrO4L4QH01S3nzAKwPLnZblNhcUC/klYiq5QTSIORkE3Ha4xHUVWSOsk6ZBYHWNIwQKYT0JSbVGPLlfBwaWrzdt19/ib94fZnPC224APlkJIXhNjY0n/yCyXUFXfoji/YWWp4gLB4eKUjA2RigBon7o5rU3Gxa8QtCoF9vKvcVGjsvtMdxU3xHhqdSMhPmkkg3kW/lJnr0jQ9tMKRAWowQK+dH2UnN/ADz9XK69s/auQtJUB2fZK6Ht5B2+ptcw6I2O2KDH5JPeYLATYJrjmlRdh9KY4M0DWitw/cr3G054yvbCXWUzXWaDzwiBs/zpFpyb4mhVwMEbb3UsUNplB9wkH0FKzVVd0mL39XYkryvoxga4AbFCA7TYUGsZCzqe9hekiPtEe5C+FkuJtNaRupHe2PsOM0UuR9higUWmQm+okFmh0Iz7AMQKNBdUecLlPiw9UZXFC5FWrupwiJzn53F0JvqlX5QcYLtSCCjDECPcJHygu9GSGkAfhCunZltJr7leVJKQweaGYYRsSSAJadf0+bIkLxlqRc0xqzbjaJcKAkt+sPQpLg7G/cZjTFBRK3eUy807zg5rEm7DT8xG5TdVJqU21SHUKG8JI/TdqRYI80SOMCWDoudp+LEesDrFoHXVgfNUCihi9G8PcHjNueXFNsmwZ7FEIwP5aFVWlZ/fl1uAgECF8xTsMrMAzjNLtCqEcpTGsJo9PAEFNMbLHicJGOTt/aJ2rGokwmk9kYIC1kM8R4GoH9mbps1EKD9DKchCELcU/PwhTJxgCJIdthGJSqD3Yl/X2xjyZj+HgAstsywAc7fTxgY4BxIQcizONxG/r1e5b7Gh3G1sNJ/OUxQ9TK0B7gG4iEGolR59PtEfdzNCXerHUBvXY4o6toCLNIRumcHPl6lJAckGo7FMVy8ABh4J+xUfNT+rbHei4UX/z7+5TTmr67QmeBbkPRbmNxDioXWqkPL6DlJRvQCDGI9MZYNRgobBxgqWY3H/eRN6U70+cA8N3H4qEORkghtMF0V0xx3t7hMRmBwihJmRUrk2+5ASVzWxSVucRv2Q0m3h/Xoo3KlSLIbZ0zcPgSNCPsfjBidzSGC1FIABArUiHScOAN0/SVm+EVTGFnRVFnrHOZPoBdp1X/UgH7h7X0L7T1V/5/YhT6aCzU4SgtJocPFEYQC60nLrZeC6P52mqSHUg1pMqoqXoqmYpxPFMpRnS+U2QxBzU0RkGNUOjD0YAzgm7H4xw44gk0Q+RTrAovkCUIARoOvOSEGiZayjicHoCpFc0P66OMp/4POjXu/XjcMkKPByQ4cCQzcHTNvP+oWpbCKJYSAuO4Mx6vAf66doRkrrHdT4v7fkAiwAghH5Hhgv7DTNz/hU1u02vHgvpPoTkgYdev6Ll+Z4/IWAaI74ekLCPE72NyAUaI3wclA4wQv4/KWgaI74elLSPE7+PyAUaI3x9MBBkiPn8yE2SE+P3RVIAR4vdnc0GGiM8fTgZLQrz+dDZYEuL1x9N2khDjP5//F09WbW9U5XehAAAAAElFTkSuQmCC", 64 | grant: "none", 65 | match: urlSchemes, 66 | license: packageJson.license, 67 | version: packageJson.version, 68 | homepage: packageJson.repository, 69 | namespace: packageJson.name, 70 | description: packageJson.cnDescription, 71 | }, 72 | }) 73 | ); 74 | } else if (mode.startsWith("web-extension")) { 75 | const commandType = mode.slice(14); 76 | 77 | config.build = { 78 | outDir: `dist/${commandType}/web-extension${commandType === "build" ? "/src" : ""}`, 79 | }; 80 | config.publicDir = "public/web-extension"; 81 | config.plugins?.push( 82 | webExtension({ 83 | manifest: () => { 84 | return { 85 | name: packageJson.cnName, 86 | icons: { 87 | "16": "logo.png", 88 | "32": "logo.png", 89 | "48": "logo.png", 90 | "96": "logo.png", 91 | "128": "logo.png", 92 | }, 93 | version: packageJson.version, 94 | background: { 95 | "{{firefox}}.scripts": "src/background.ts", 96 | "{{chrome}}.service_worker": "src/background.ts", 97 | }, 98 | // update_url: `${packageJson.repository}/releases/download/v${packageJson.version}/web-extension/updates.xml`, 99 | description: packageJson.cnDescription, 100 | permissions: ["scripting", "webNavigation"], 101 | homepage_url: packageJson.repository, 102 | default_locale: "zh_CN", 103 | host_permissions: urlSchemes, 104 | "{{chrome}}.action": { 105 | default_icon: { 106 | "16": "logo_grey.png", 107 | "32": "logo_grey.png", 108 | "48": "logo_grey.png", 109 | "96": "logo_grey.png", 110 | "128": "logo_grey.png", 111 | }, 112 | default_title: packageJson.name, 113 | // default_popup: "src/popup/index.html", 114 | }, 115 | "{{firefox}}.browser_action": { 116 | default_icon: { 117 | "16": "logo_grey.png", 118 | "32": "logo_grey.png", 119 | "48": "logo_grey.png", 120 | "96": "logo_grey.png", 121 | "128": "logo_grey.png", 122 | }, 123 | default_title: packageJson.name, 124 | // default_popup: "src/popup/index.html", 125 | }, 126 | web_accessible_resources: [ 127 | { 128 | matches: urlSchemes, 129 | resources: ["src/main.js"], 130 | use_dynamic_url: true, 131 | }, 132 | ], 133 | "{{chrome}}.manifest_version": 3, 134 | "{{firefox}}.manifest_version": 2, 135 | }; 136 | }, 137 | // webExtConfig: {}, 138 | watchFilePaths: ["package.json", "vite.config.ts"], 139 | additionalInputs: ["src/main.ts"], 140 | scriptViteConfig: { plugins: [vue(), cssInjectedByJsPlugin()] }, 141 | disableAutoLaunch: true, 142 | skipManifestValidation: true, 143 | }) 144 | ); 145 | } else if (mode === "test") { 146 | config.plugins?.push(vue()); 147 | } 148 | 149 | return config; 150 | }); 151 | -------------------------------------------------------------------------------- /src/components/RenamePreview.vue: -------------------------------------------------------------------------------- 1 | 137 | 138 | 233 | 234 | 393 | -------------------------------------------------------------------------------- /src/utils/icons.ts: -------------------------------------------------------------------------------- 1 | import type { IRenderOptions } from "@/utils/render"; 2 | 3 | type TIcon = IRenderOptions & { children: IRenderOptions | IRenderOptions[] }; 4 | 5 | const down: TIcon = { 6 | type: "svg", 7 | props: { 8 | fill: "currentColor", 9 | xmlns: "http://www.w3.org/2000/svg", 10 | width: "1em", 11 | height: "1em", 12 | version: "1.1", 13 | viewBox: "0 0 1024 1024", 14 | }, 15 | children: { 16 | type: "path", 17 | props: { 18 | d: "M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3 0.1-12.7-6.4-12.7z", 19 | }, 20 | }, 21 | }; 22 | 23 | const edit: TIcon = { 24 | type: "svg", 25 | props: { 26 | fill: "currentColor", 27 | xmlns: "http://www.w3.org/2000/svg", 28 | width: "1em", 29 | height: "1em", 30 | version: "1.1", 31 | viewBox: "64 64 896 896", 32 | }, 33 | children: { 34 | type: "path", 35 | props: { 36 | d: "M257.7 752c2 0 4-.2 6-.5L431.9 722c2-.4 3.9-1.3 5.3-2.8l423.9-423.9a9.96 9.96 0 000-14.1L694.9 114.9c-1.9-1.9-4.4-2.9-7.1-2.9s-5.2 1-7.1 2.9L256.8 538.8c-1.5 1.5-2.4 3.3-2.8 5.3l-29.5 168.2a33.5 33.5 0 009.4 29.8c6.6 6.4 14.9 9.9 23.8 9.9zm67.4-174.4L687.8 215l73.3 73.3-362.7 362.6-88.9 15.7 15.6-89zM880 836H144c-17.7 0-32 14.3-32 32v36c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-36c0-17.7-14.3-32-32-32z", 37 | }, 38 | }, 39 | }; 40 | const editCircle: TIcon = { 41 | type: "svg", 42 | props: { 43 | fill: "currentColor", 44 | xmlns: "http://www.w3.org/2000/svg", 45 | width: "1em", 46 | height: "1em", 47 | version: "1.1", 48 | viewBox: "64 64 896 896", 49 | }, 50 | children: [ 51 | { 52 | type: "path", 53 | props: { 54 | d: "M712.533333 288c-25.6-25.6-66.133333-25.6-89.6 0L362.666667 546.133333c-6.4 6.4-12.8 14.933333-17.066667 23.466667l-53.333333 117.333333c-14.933333 32 19.2 66.133333 51.2 51.2l117.333333-53.333333c8.533333-4.266667 17.066667-10.666667 23.466667-17.066667l258.133333-258.133333c25.6-25.6 25.6-66.133333 0-89.6l-29.866667-32zM448 631.466667c-2.133333 2.133333-6.4 4.266667-10.666667 6.4l-85.333333 38.4 38.4-85.333334c2.133333-4.266667 4.266667-6.4 6.4-10.666666l194.133333-194.133334 49.066667 49.066667-192 196.266667z m258.133333-258.133334l-27.733333 27.733334-49.066667-49.066667 27.733334-27.733333c4.266667-4.266667 12.8-4.266667 17.066666 0l29.866667 29.866666c6.4 4.266667 6.4 12.8 2.133333 19.2z", 55 | }, 56 | }, 57 | { 58 | type: "path", 59 | props: { 60 | d: "M512 85.333333C277.333333 85.333333 85.333333 277.333333 85.333333 512s192 426.666667 426.666667 426.666667 426.666667-192 426.666667-426.666667S746.666667 85.333333 512 85.333333z m0 802.133334c-206.933333 0-375.466667-168.533333-375.466667-375.466667S305.066667 136.533333 512 136.533333 887.466667 305.066667 887.466667 512 718.933333 887.466667 512 887.466667z", 61 | }, 62 | }, 63 | ], 64 | }; 65 | 66 | const info: TIcon = { 67 | type: "svg", 68 | props: { 69 | fill: "var(--cdp-color-blue)", 70 | xmlns: "http://www.w3.org/2000/svg", 71 | width: "1em", 72 | height: "1em", 73 | version: "1.1", 74 | viewBox: "0 0 1024 1024", 75 | }, 76 | children: { 77 | type: "path", 78 | props: { 79 | d: "M512 224m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM544 392h-64c-4.4 0-8 3.6-8 8v464c0 4.4 3.6 8 8 8h64c4.4 0 8-3.6 8-8V400c0-4.4-3.6-8-8-8z", 80 | }, 81 | }, 82 | }; 83 | const infoCircle: TIcon = { 84 | type: "svg", 85 | props: { 86 | fill: "var(--cdp-color-blue)", 87 | xmlns: "http://www.w3.org/2000/svg", 88 | width: "1em", 89 | height: "1em", 90 | version: "1.1", 91 | viewBox: "0 0 1024 1024", 92 | }, 93 | children: { 94 | type: "path", 95 | props: { 96 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zM512 336m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM536 448h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z", 97 | }, 98 | }, 99 | }; 100 | const infoCircleFilled: TIcon = { 101 | type: "svg", 102 | props: { 103 | fill: "var(--cdp-color-blue)", 104 | xmlns: "http://www.w3.org/2000/svg", 105 | width: "1em", 106 | height: "1em", 107 | version: "1.1", 108 | viewBox: "0 0 1024 1024", 109 | }, 110 | children: { 111 | type: "path", 112 | props: { 113 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272z m-32-344c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z", 114 | }, 115 | }, 116 | }; 117 | 118 | const check: TIcon = { 119 | type: "svg", 120 | props: { 121 | fill: "var(--cdp-color-green)", 122 | xmlns: "http://www.w3.org/2000/svg", 123 | width: "1em", 124 | height: "1em", 125 | version: "1.1", 126 | viewBox: "0 0 1024 1024", 127 | }, 128 | children: { 129 | type: "path", 130 | props: { 131 | d: "M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474c-6.1-7.7-15.3-12.2-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1 0.4-12.8-6.3-12.8z", 132 | }, 133 | }, 134 | }; 135 | const checkCircle: TIcon = { 136 | type: "svg", 137 | props: { 138 | fill: "var(--cdp-color-green)", 139 | xmlns: "http://www.w3.org/2000/svg", 140 | width: "1em", 141 | height: "1em", 142 | version: "1.1", 143 | viewBox: "0 0 1024 1024", 144 | }, 145 | children: { 146 | type: "path", 147 | props: { 148 | d: "M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8c12.7 17.7 39 17.7 51.7 0l210.6-292c3.9-5.3 0.1-12.7-6.4-12.7zM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z", 149 | }, 150 | }, 151 | }; 152 | const checkCircleFilled: TIcon = { 153 | type: "svg", 154 | props: { 155 | fill: "var(--cdp-color-green)", 156 | xmlns: "http://www.w3.org/2000/svg", 157 | width: "1em", 158 | height: "1em", 159 | version: "1.1", 160 | viewBox: "0 0 1024 1024", 161 | }, 162 | children: { 163 | type: "path", 164 | props: { 165 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m193.5 301.7l-210.6 292c-12.7 17.7-39 17.7-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z", 166 | }, 167 | }, 168 | }; 169 | 170 | const close: TIcon = { 171 | type: "svg", 172 | props: { 173 | fill: "var(--cdp-color-red)", 174 | xmlns: "http://www.w3.org/2000/svg", 175 | width: "1em", 176 | height: "1em", 177 | version: "1.1", 178 | viewBox: "0 0 1024 1024", 179 | }, 180 | children: { 181 | type: "path", 182 | props: { 183 | d: "M563.8 512l262.5-312.9c4.4-5.2 0.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-0.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z", 184 | }, 185 | }, 186 | }; 187 | const closeCircle: TIcon = { 188 | type: "svg", 189 | props: { 190 | fill: "var(--cdp-color-red)", 191 | xmlns: "http://www.w3.org/2000/svg", 192 | width: "1em", 193 | height: "1em", 194 | version: "1.1", 195 | viewBox: "0 0 1024 1024", 196 | }, 197 | children: { 198 | type: "path", 199 | props: { 200 | d: "M685.4 354.8c0-4.4-3.6-8-8-8l-66 0.3L512 465.6l-99.3-118.4-66.1-0.3c-4.4 0-8 3.5-8 8 0 1.9 0.7 3.7 1.9 5.2l130.1 155L340.5 670c-1.2 1.5-1.9 3.3-1.9 5.2 0 4.4 3.6 8 8 8l66.1-0.3L512 564.4l99.3 118.4 66 0.3c4.4 0 8-3.5 8-8 0-1.9-0.7-3.7-1.9-5.2L553.5 515l130.1-155c1.2-1.4 1.8-3.3 1.8-5.2zM512 65C264.6 65 64 265.6 64 513s200.6 448 448 448 448-200.6 448-448S759.4 65 512 65z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z", 201 | }, 202 | }, 203 | }; 204 | const closeCircleFilled: TIcon = { 205 | type: "svg", 206 | props: { 207 | fill: "var(--cdp-color-red)", 208 | xmlns: "http://www.w3.org/2000/svg", 209 | width: "1em", 210 | height: "1em", 211 | version: "1.1", 212 | viewBox: "0 0 1024 1024", 213 | }, 214 | children: { 215 | type: "path", 216 | props: { 217 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m165.4 618.2l-66-0.3L512 563.4l-99.3 118.4-66.1 0.3c-4.4 0-8-3.5-8-8 0-1.9 0.7-3.7 1.9-5.2l130.1-155L340.5 359c-1.2-1.5-1.9-3.3-1.9-5.2 0-4.4 3.6-8 8-8l66.1 0.3L512 464.6l99.3-118.4 66-0.3c4.4 0 8 3.5 8 8 0 1.9-0.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z", 218 | }, 219 | }, 220 | }; 221 | 222 | const error: TIcon = { 223 | type: "svg", 224 | props: { 225 | fill: "var(--cdp-color-red)", 226 | xmlns: "http://www.w3.org/2000/svg", 227 | width: "1em", 228 | height: "1em", 229 | version: "1.1", 230 | viewBox: "0 0 1024 1024", 231 | }, 232 | children: { 233 | type: "path", 234 | props: { 235 | d: "M512 720m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM480 416v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zM955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48z m-783.5-27.9L512 239.9l339.8 588.2H172.2z", 236 | }, 237 | }, 238 | }; 239 | const errorFilled: TIcon = { 240 | type: "svg", 241 | props: { 242 | fill: "var(--cdp-color-red)", 243 | xmlns: "http://www.w3.org/2000/svg", 244 | width: "1em", 245 | height: "1em", 246 | version: "1.1", 247 | viewBox: "0 0 1024 1024", 248 | }, 249 | children: { 250 | type: "path", 251 | props: { 252 | d: "M955.7 856l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zM480 416c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v184c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V416z m32 352c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z", 253 | }, 254 | }, 255 | }; 256 | 257 | const warning: TIcon = { 258 | type: "svg", 259 | props: { 260 | fill: "var(--cdp-color-yellow)", 261 | xmlns: "http://www.w3.org/2000/svg", 262 | width: "1em", 263 | height: "1em", 264 | version: "1.1", 265 | viewBox: "0 0 1024 1024", 266 | }, 267 | children: { 268 | type: "path", 269 | props: { 270 | d: "M512 804m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0ZM480 636h64c4.4 0 8-3.6 8-8V164c0-4.4-3.6-8-8-8h-64c-4.4 0-8 3.6-8 8v464c0 4.4 3.6 8 8 8z", 271 | }, 272 | }, 273 | }; 274 | const warningCircle: TIcon = { 275 | type: "svg", 276 | props: { 277 | fill: "var(--cdp-color-yellow)", 278 | xmlns: "http://www.w3.org/2000/svg", 279 | width: "1em", 280 | height: "1em", 281 | version: "1.1", 282 | viewBox: "0 0 1024 1024", 283 | }, 284 | children: { 285 | type: "path", 286 | props: { 287 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zM512 688m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM488 576h48c4.4 0 8-3.6 8-8V296c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8z", 288 | }, 289 | }, 290 | }; 291 | const warningCircleFilled: TIcon = { 292 | type: "svg", 293 | props: { 294 | fill: "var(--cdp-color-yellow)", 295 | xmlns: "http://www.w3.org/2000/svg", 296 | width: "1em", 297 | height: "1em", 298 | version: "1.1", 299 | viewBox: "0 0 1024 1024", 300 | }, 301 | children: { 302 | type: "path", 303 | props: { 304 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296z m32 440c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z", 305 | }, 306 | }, 307 | }; 308 | 309 | const timeCircle: TIcon = { 310 | type: "svg", 311 | props: { 312 | fill: "var(--cdp-color-blue)", 313 | xmlns: "http://www.w3.org/2000/svg", 314 | width: "1em", 315 | height: "1em", 316 | version: "1.1", 317 | viewBox: "0 0 1024 1024", 318 | }, 319 | children: { 320 | type: "path", 321 | props: { 322 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zM686.7 638.6L544.1 535.5V288c0-4.4-3.6-8-8-8H488c-4.4 0-8 3.6-8 8v275.4c0 2.6 1.2 5 3.3 6.5l165.4 120.6c3.6 2.6 8.6 1.8 11.2-1.7l28.6-39c2.6-3.7 1.8-8.7-1.8-11.2z", 323 | }, 324 | }, 325 | }; 326 | const timeCircleFilled: TIcon = { 327 | type: "svg", 328 | props: { 329 | fill: "var(--cdp-color-blue)", 330 | xmlns: "http://www.w3.org/2000/svg", 331 | width: "1em", 332 | height: "1em", 333 | version: "1.1", 334 | viewBox: "0 0 1024 1024", 335 | }, 336 | children: { 337 | type: "path", 338 | props: { 339 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m176.5 585.7l-28.6 39c-2.6 3.6-7.6 4.3-11.2 1.7L483.3 569.8c-2.1-1.5-3.3-3.9-3.3-6.5V288c0-4.4 3.6-8 8-8h48.1c4.4 0 8 3.6 8 8v247.5l142.6 103.1c3.6 2.5 4.4 7.5 1.8 11.1z", 340 | }, 341 | }, 342 | }; 343 | 344 | const frown: TIcon = { 345 | type: "svg", 346 | props: { 347 | fill: "var(--cdp-color-red)", 348 | xmlns: "http://www.w3.org/2000/svg", 349 | width: "1em", 350 | height: "1em", 351 | version: "1.1", 352 | viewBox: "0 0 1024 1024", 353 | }, 354 | children: { 355 | type: "path", 356 | props: { 357 | d: "M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2zM512 533c-85.5 0-155.6 67.3-160 151.6-0.2 4.6 3.4 8.4 8 8.4h48.1c4.2 0 7.8-3.2 8.1-7.4C420 636.1 461.5 597 512 597s92.1 39.1 95.8 88.6c0.3 4.2 3.9 7.4 8.1 7.4H664c4.6 0 8.2-3.8 8-8.4-4.4-84.3-74.5-151.6-160-151.6z", 358 | }, 359 | }, 360 | }; 361 | const frownFilled: TIcon = { 362 | type: "svg", 363 | props: { 364 | fill: "var(--cdp-color-red)", 365 | xmlns: "http://www.w3.org/2000/svg", 366 | width: "1em", 367 | height: "1em", 368 | version: "1.1", 369 | viewBox: "0 0 1024 1024", 370 | }, 371 | children: { 372 | type: "path", 373 | props: { 374 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zM288 421c0-26.5 21.5-48 48-48s48 21.5 48 48-21.5 48-48 48-48-21.5-48-48z m376 272h-48.1c-4.2 0-7.8-3.2-8.1-7.4C604 636.1 562.5 597 512 597s-92.1 39.1-95.8 88.6c-0.3 4.2-3.9 7.4-8.1 7.4H360c-4.6 0-8.2-3.8-8-8.4 4.4-84.3 74.5-151.6 160-151.6s155.6 67.3 160 151.6c0.2 4.6-3.4 8.4-8 8.4z m24-224c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z", 375 | }, 376 | }, 377 | }; 378 | 379 | const meh: TIcon = { 380 | type: "svg", 381 | props: { 382 | fill: "var(--cdp-color-yellow)", 383 | xmlns: "http://www.w3.org/2000/svg", 384 | width: "1em", 385 | height: "1em", 386 | version: "1.1", 387 | viewBox: "0 0 1024 1024", 388 | }, 389 | children: { 390 | type: "path", 391 | props: { 392 | d: "M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2zM664 565H360c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8h304c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8z", 393 | }, 394 | }, 395 | }; 396 | const mehFilled: TIcon = { 397 | type: "svg", 398 | props: { 399 | fill: "var(--cdp-color-yellow)", 400 | xmlns: "http://www.w3.org/2000/svg", 401 | width: "1em", 402 | height: "1em", 403 | version: "1.1", 404 | viewBox: "0 0 1024 1024", 405 | }, 406 | children: { 407 | type: "path", 408 | props: { 409 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zM288 421c0-26.5 21.5-48 48-48s48 21.5 48 48-21.5 48-48 48-48-21.5-48-48z m384 200c0 4.4-3.6 8-8 8H360c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h304c4.4 0 8 3.6 8 8v48z m16-152c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z", 410 | }, 411 | }, 412 | }; 413 | 414 | const smile: TIcon = { 415 | type: "svg", 416 | props: { 417 | fill: "var(--cdp-color-green)", 418 | xmlns: "http://www.w3.org/2000/svg", 419 | width: "1em", 420 | height: "1em", 421 | version: "1.1", 422 | viewBox: "0 0 1024 1024", 423 | }, 424 | children: { 425 | type: "path", 426 | props: { 427 | d: "M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0ZM512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2zM664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-0.3-4.2-3.9-7.4-8.1-7.4H360c-4.6 0-8.2 3.8-8 8.4 4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6c0.2-4.6-3.4-8.4-8-8.4z", 428 | }, 429 | }, 430 | }; 431 | const smileFilled: TIcon = { 432 | type: "svg", 433 | props: { 434 | fill: "var(--cdp-color-green)", 435 | xmlns: "http://www.w3.org/2000/svg", 436 | width: "1em", 437 | height: "1em", 438 | version: "1.1", 439 | viewBox: "0 0 1024 1024", 440 | }, 441 | children: { 442 | type: "path", 443 | props: { 444 | d: "M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zM288 421c0-26.5 21.5-48 48-48s48 21.5 48 48-21.5 48-48 48-48-21.5-48-48z m224 272c-85.5 0-155.6-67.3-160-151.6-0.2-4.6 3.4-8.4 8-8.4h48.1c4.2 0 7.8 3.2 8.1 7.4C420 589.9 461.5 629 512 629s92.1-39.1 95.8-88.6c0.3-4.2 3.9-7.4 8.1-7.4H664c4.6 0 8.2 3.8 8 8.4-4.4 84.3-74.5 151.6-160 151.6z m176-224c-26.5 0-48-21.5-48-48s21.5-48 48-48 48 21.5 48 48-21.5 48-48 48z", 445 | }, 446 | }, 447 | }; 448 | 449 | const copyDocument: TIcon = { 450 | type: "svg", 451 | props: { 452 | fill: "currentColor", 453 | xmlns: "http://www.w3.org/2000/svg", 454 | width: "1em", 455 | height: "1em", 456 | version: "1.1", 457 | viewBox: "0 0 1024 1024", 458 | }, 459 | children: [ 460 | { 461 | type: "path", 462 | props: { 463 | d: "M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64z", 464 | }, 465 | }, 466 | { 467 | type: "path", 468 | props: { 469 | d: "M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64", 470 | }, 471 | }, 472 | ], 473 | }; 474 | 475 | const rank: TIcon = { 476 | type: "svg", 477 | props: { 478 | fill: "currentColor", 479 | xmlns: "http://www.w3.org/2000/svg", 480 | width: "1em", 481 | height: "1em", 482 | version: "1.1", 483 | viewBox: "0 0 1024 1024", 484 | }, 485 | children: { 486 | type: "path", 487 | props: { 488 | d: "m186.496 544 41.408 41.344a32 32 0 1 1-45.248 45.312l-96-96a32 32 0 0 1 0-45.312l96-96a32 32 0 1 1 45.248 45.312L186.496 480h290.816V186.432l-41.472 41.472a32 32 0 1 1-45.248-45.184l96-96.128a32 32 0 0 1 45.312 0l96 96.064a32 32 0 0 1-45.248 45.184l-41.344-41.28V480H832l-41.344-41.344a32 32 0 0 1 45.248-45.312l96 96a32 32 0 0 1 0 45.312l-96 96a32 32 0 0 1-45.248-45.312L832 544H541.312v293.44l41.344-41.28a32 32 0 1 1 45.248 45.248l-96 96a32 32 0 0 1-45.312 0l-96-96a32 32 0 1 1 45.312-45.248l41.408 41.408V544H186.496z", 489 | }, 490 | }, 491 | }; 492 | 493 | const sort: TIcon = { 494 | type: "svg", 495 | props: { 496 | fill: "currentColor", 497 | xmlns: "http://www.w3.org/2000/svg", 498 | width: "1em", 499 | height: "1em", 500 | version: "1.1", 501 | viewBox: "0 0 1024 1024", 502 | }, 503 | children: { 504 | type: "path", 505 | props: { 506 | d: "M384 96a32 32 0 0 1 64 0v786.752a32 32 0 0 1-54.592 22.656L95.936 608a32 32 0 0 1 0-45.312h.128a32 32 0 0 1 45.184 0L384 805.632zm192 45.248a32 32 0 0 1 54.592-22.592L928.064 416a32 32 0 0 1 0 45.312h-.128a32 32 0 0 1-45.184 0L640 218.496V928a32 32 0 1 1-64 0V141.248z", 507 | }, 508 | }, 509 | }; 510 | 511 | const dCaret: TIcon = { 512 | type: "svg", 513 | props: { 514 | fill: "currentColor", 515 | xmlns: "http://www.w3.org/2000/svg", 516 | width: "1em", 517 | height: "1em", 518 | version: "1.1", 519 | viewBox: "0 0 1024 1024", 520 | }, 521 | children: { 522 | type: "path", 523 | props: { 524 | d: "m512 128 288 320H224zM224 576h576L512 896z", 525 | }, 526 | }, 527 | }; 528 | 529 | const loading: TIcon = { 530 | type: "svg", 531 | props: { 532 | xmlns: "http://www.w3.org/2000/svg", 533 | width: "1em", 534 | height: "1em", 535 | viewBox: "0 0 50 50", 536 | }, 537 | children: [ 538 | { 539 | type: "g", 540 | props: { 541 | fill: "none", 542 | strokeLinecap: "round", 543 | }, 544 | children: [ 545 | { 546 | type: "circle", 547 | props: { 548 | r: "20", 549 | cx: "25", 550 | cy: "25", 551 | stroke: "var(--cdp-color-gray-50)", 552 | "stroke-width": "3.5", 553 | }, 554 | }, 555 | { 556 | type: "circle", 557 | props: { 558 | r: "20", 559 | cx: "25", 560 | cy: "25", 561 | stroke: "var(--cdp-color-blue)", 562 | "stroke-width": "3.5", 563 | strokeDasharray: "90, 150", 564 | strokeDashoffset: "0", 565 | }, 566 | children: [ 567 | { 568 | type: "animate", 569 | props: { 570 | dur: "1.5s", 571 | values: "1,200;90,150;90,150", 572 | repeatCount: "indefinite", 573 | attributeName: "stroke-dasharray", 574 | }, 575 | }, 576 | { 577 | type: "animate", 578 | props: { 579 | dur: "1.5s", 580 | values: "0;-40;-120", 581 | repeatCount: "indefinite", 582 | attributeName: "stroke-dashoffset", 583 | }, 584 | }, 585 | { 586 | type: "animateTransform", 587 | props: { 588 | to: "360 25 25", 589 | dur: "2s", 590 | type: "rotate", 591 | from: "0 25 25", 592 | repeatCount: "indefinite", 593 | attributeName: "transform", 594 | }, 595 | }, 596 | ], 597 | }, 598 | ], 599 | }, 600 | ], 601 | }; 602 | 603 | // const success: TIcon = { 604 | // type: "svg", 605 | // props: { 606 | // fill: "var(--cdp-color-green)", 607 | // xmlns: "http://www.w3.org/2000/svg", 608 | // width: "1em", 609 | // height: "1em", 610 | // viewBox: "0 0 1024 1024", 611 | // }, 612 | // children: { 613 | // type: "path", 614 | // props: { 615 | // d: "M512,72C269,72,72,269,72,512s197,440,440,440s440-197,440-440S755,72,512,72L512,72z M758.9,374 c-48.5,48.6-81.2,76.9-172.3,186.8c-52.6,63.4-102.3,131.5-102.7,132L462.1,720c-4.6,6.1-13.5,6.8-19.1,1.6L267.9,558.9 c-17.8-16.5-18.8-44.4-2.3-62.2s44.4-18.8,62.2-2.3l104.9,97.5c5.5,5.1,14.1,4.5,18.9-1.3c16.2-20.1,38.4-44.5,62.4-68.6 c90.2-90.9,145.6-139.7,175.2-161.3c36-26.2,77.3-48.6,87.3-36.2C792,343.9,782.5,350.3,758.9,374L758.9,374z", 616 | // }, 617 | // }, 618 | // }; 619 | 620 | const icons: { [key: string]: TIcon } = { 621 | down, 622 | 623 | edit, 624 | editCircle, 625 | 626 | info, 627 | infoCircle, 628 | infoCircleFilled, 629 | 630 | check, 631 | checkCircle, 632 | checkCircleFilled, 633 | 634 | close, 635 | closeCircle, 636 | closeCircleFilled, 637 | 638 | error, 639 | errorFilled, 640 | 641 | warning, 642 | warningCircle, 643 | warningCircleFilled, 644 | 645 | timeCircle, 646 | timeCircleFilled, 647 | 648 | frown, 649 | frownFilled, 650 | meh, 651 | mehFilled, 652 | smile, 653 | smileFilled, 654 | 655 | copyDocument, 656 | rank, 657 | sort, 658 | dCaret, 659 | 660 | loading, 661 | }; 662 | 663 | export default icons; 664 | -------------------------------------------------------------------------------- /src/provider/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "vue"; 2 | import type { Change } from "diff"; 3 | 4 | import { diffChars } from "diff"; 5 | import Sortable from "sortablejs"; 6 | import message from "@/utils/message"; 7 | import { isArray, isNumber } from "@/utils/is"; 8 | import querySelector from "@/utils/querySelector"; 9 | import complementZero from "@/utils/complementZero"; 10 | 11 | export abstract class Provider { 12 | // 匹配测试 13 | static test() { 14 | return false; 15 | } 16 | // 类型 17 | public abstract type: string; 18 | // 根元素 ID 19 | public abstract rootElementId: string; 20 | // 根元素插入目标 21 | public abstract rootElementInsertTarget: string; 22 | // 根元素插入方式 23 | public abstract rootElementInsertMethod: TRootElementInsertMethod; 24 | // 入口组件 25 | public abstract EnterComponent: () => Component; 26 | 27 | // 显示加载 28 | public isLoading: boolean = false; 29 | // 更新加载中 30 | public isUpdateLoading: boolean = false; 31 | // 控制加载中 32 | public isControlLoading: boolean = false; 33 | // 预览加载中 34 | public isPreviewLoading: boolean = false; 35 | 36 | // 主面板显示控制 37 | public visible: boolean = false; 38 | public setVisible(val: boolean = false) { 39 | if ( 40 | !val && 41 | (this.isLoading || this.isUpdateLoading || this.isControlLoading || this.isPreviewLoading) 42 | ) { 43 | return false; 44 | } 45 | this.visible = val; 46 | if (val) { 47 | this._updateOriginList().then(() => { 48 | this._initDragSort(); 49 | }); 50 | } else { 51 | this._destroyDragSort(); 52 | } 53 | } 54 | 55 | // 替换参数 56 | public replaceParams: ReplaceParams = new ReplaceParams({ 57 | onUpdateHandler: () => this._onReplaceParamsUpdate.call(this), 58 | onChangeHandler: (key: TReplaceParamsKeys, data: ReplaceParams) => 59 | this._onReplaceParamsChange.call(this, key, data), 60 | }); 61 | // 替换参数禁用 62 | public replaceParamsDisabled: boolean = false; 63 | // 替换参数更新回调函数 64 | private _onReplaceParamsUpdate() { 65 | this._updateCurrentList(); 66 | } 67 | private _onReplaceParamsChange(key: TReplaceParamsKeys, data: ReplaceParams) { 68 | if (key === "sortChecked") { 69 | this._onSortCheckedChange(data[key]); 70 | } 71 | } 72 | // 重置替换参数 73 | private _resetReplaceParams() { 74 | this.replaceParams.reset(); 75 | } 76 | 77 | // 原始文件列表数据 78 | protected originList: IOriginListItem[] = []; 79 | // 获取原始文件列表数据 80 | protected abstract getOriginList(): Promise; 81 | // 更新原始文件列表数据 82 | private _updateOriginList(): Promise { 83 | this.isLoading = true; 84 | this._clearIndexMap(); 85 | this._clearUncheckedList(); 86 | return this.getOriginList() 87 | .then((res) => { 88 | this._clearIndexMap(); 89 | this.isLoading = false; 90 | this.originList = res; 91 | this._initListItemIndex(); 92 | return this._updateCurrentList(); 93 | }) 94 | .catch(() => { 95 | this._clearIndexMap(); 96 | this.isLoading = false; 97 | this.originList = []; 98 | this._updateCurrentList(); 99 | return []; 100 | }); 101 | } 102 | 103 | // 当前文件列表数据 104 | private _currentList: IListItem[] = []; 105 | public get currentList(): IListItem[] { 106 | return this._currentList; 107 | } 108 | // 更新当前文件列表数据 109 | private _updateCurrentList(): IListItem[] { 110 | const renameMode = this.replaceParams.renameMode; 111 | 112 | let result: IListItem[] = this.originList.map((item) => { 113 | return { 114 | id: item.id, 115 | ext: item.ext, 116 | path: item.path, 117 | index: this._getListItemIndex(item.id), 118 | status: LIST_ITEM_STATUS_NONE, 119 | isEmpty: false, 120 | isError: false, 121 | isRepeat: false, 122 | fileName: item.fileName, 123 | isChange: false, 124 | isMatched: true, 125 | isChecked: !this._uncheckedList.has(item.id), 126 | isLoading: false, 127 | oldFileName: item.fullFileName, 128 | newFileName: "", 129 | displayIndex: this._getListItemDisplayIndex(item.id), 130 | }; 131 | }); 132 | 133 | result = result.sort((a, b) => a.index - b.index); 134 | 135 | const newFileNameSet: Set = new Set(); 136 | 137 | if (renameMode === RENAME_MODE_SERIES) { 138 | if (this.replaceParams.title || this.replaceParams.season) { 139 | const season = this.replaceParams.season 140 | ? ".S" + complementZero(this.replaceParams.season) 141 | : ""; 142 | result.forEach((item) => { 143 | const fileName = this.replaceParams.title || item.fileName; 144 | let newFileName = fileName + season; 145 | if (this.replaceParams.autoEpisode && isNumber(item.displayIndex)) { 146 | const episode = (season ? "" : ".") + "E" + complementZero(item.displayIndex); 147 | newFileName += episode; 148 | } 149 | newFileName += "." + item.ext; 150 | item.newFileName = newFileName.trim(); 151 | this._listItemGeneralMethod(item, newFileNameSet); 152 | }); 153 | } 154 | } 155 | 156 | if (renameMode === RENAME_MODE_PATTERN) { 157 | let regexp: RegExp | undefined; 158 | 159 | if (this.replaceParams.pattern) { 160 | try { 161 | regexp = new RegExp(this.replaceParams.pattern, "g"); 162 | } catch (error) { 163 | console.error("regexp error", error); 164 | } 165 | if (regexp) { 166 | result.forEach((item) => { 167 | if (this.replaceParams.autoEpisode) { 168 | item.isMatched = !!regexp?.test(item.fileName); 169 | if (item.isMatched) { 170 | let newFileName = item.fileName.replace( 171 | regexp as RegExp, 172 | this.replaceParams.replace 173 | ); 174 | if (isNumber(item.displayIndex)) { 175 | newFileName += (newFileName ? ".E" : "E") + complementZero(item.displayIndex); 176 | } 177 | if (newFileName) { 178 | newFileName += "." + item.ext; 179 | } 180 | item.newFileName = newFileName.trim(); 181 | this._listItemGeneralMethod(item, newFileNameSet); 182 | } 183 | } else { 184 | item.isMatched = !!regexp?.test(item.oldFileName); 185 | if (item.isMatched) { 186 | item.newFileName = item.oldFileName.replace( 187 | regexp as RegExp, 188 | this.replaceParams.replace 189 | ); 190 | this._listItemGeneralMethod(item, newFileNameSet); 191 | } 192 | } 193 | }); 194 | } 195 | } 196 | } 197 | 198 | this._currentList = result; 199 | this._updateStatus(); 200 | this._emitCurrentListUpdateHandler(); 201 | 202 | return result; 203 | } 204 | // 文件列表项通用处理 205 | private _listItemGeneralMethod(item: IListItem, newFileNameSet: Set) { 206 | item.isChange = item.oldFileName !== item.newFileName; 207 | item.isEmpty = item.isChecked && !item.newFileName; 208 | item.isRepeat = item.isChecked && !!item.newFileName && newFileNameSet.has(item.newFileName); 209 | item.isError = item.isEmpty || item.isRepeat; 210 | item.isChecked && newFileNameSet.add(item.newFileName); 211 | if (item.isChange) { 212 | item.diffList = diffChars(item.oldFileName, item.newFileName); 213 | } else { 214 | item.diffList = undefined; 215 | } 216 | } 217 | 218 | // 空名计数 219 | private _emptyCount: number = 0; 220 | // 错误计数 221 | private _errorCount: number = 0; 222 | // 重复计数 223 | private _repeatCount: number = 0; 224 | // 变更计数 225 | private _changeCount: number = 0; 226 | // 匹配计数 227 | private _matchedCount: number = 0; 228 | // 状态计数 229 | private _failStatusCount: number = 0; 230 | private _readyStatusCount: number = 0; 231 | private _pendingStatusCount: number = 0; 232 | private _successStatusCount: number = 0; 233 | // 是否有错误 234 | public hasError: boolean = false; 235 | // 是否有变更 236 | public hasChange: boolean = false; 237 | // 是否全选 238 | public hasCheckedAll: boolean = false; 239 | // 是否全不选 240 | public hasUncheckedAll: boolean = false; 241 | // 是否可继续 242 | public shouldContinue: boolean = false; 243 | // 状态列表 244 | public statusList: IStatusList[] = []; 245 | protected _updateStatus(): void { 246 | let emptyCount = 0; 247 | let errorCount = 0; 248 | let repeatCount = 0; 249 | let changeCount = 0; 250 | let matchedCount = 0; 251 | let failStatusCount = 0; 252 | let readyStatusCount = 0; 253 | let pendingStatusCount = 0; 254 | let successStatusCount = 0; 255 | 256 | this._currentList.forEach((item) => { 257 | if (item.isChecked) { 258 | item.isEmpty && emptyCount++; 259 | item.isError && errorCount++; 260 | item.isRepeat && repeatCount++; 261 | item.isChange && changeCount++; 262 | item.isMatched && matchedCount++; 263 | } 264 | if (item.status === LIST_ITEM_STATUS_PENDING) { 265 | pendingStatusCount++; 266 | } else if (item.status === LIST_ITEM_STATUS_SUCCESS) { 267 | successStatusCount++; 268 | } else if (item.status === LIST_ITEM_STATUS_READY) { 269 | readyStatusCount++; 270 | } else if (item.status === LIST_ITEM_STATUS_FAIL) { 271 | failStatusCount++; 272 | } 273 | }); 274 | this._emptyCount = emptyCount; 275 | this._errorCount = errorCount; 276 | this._repeatCount = repeatCount; 277 | this._changeCount = changeCount; 278 | this._matchedCount = matchedCount; 279 | this._failStatusCount = failStatusCount; 280 | this._readyStatusCount = readyStatusCount; 281 | this._pendingStatusCount = pendingStatusCount; 282 | this._successStatusCount = successStatusCount; 283 | 284 | this.hasError = this._errorCount > 0; 285 | this.hasChange = this._changeCount > 0; 286 | 287 | this.shouldContinue = !this.hasError && this.hasChange; 288 | 289 | this.hasCheckedAll = this._uncheckedList.size === 0; 290 | this.hasUncheckedAll = this._uncheckedList.size === this._currentList.length; 291 | 292 | this._updateStatusList(); 293 | } 294 | private _updateStatusList() { 295 | const result: IStatusList[] = []; 296 | if (!this._currentList.length) { 297 | const title = "无文件"; 298 | result.push({ icon: "closeCircleFilled", title, className: "red" }, { message: title }); 299 | } else { 300 | if (this.isUpdateLoading) { 301 | if (this._successStatusCount) { 302 | result.push({ 303 | message: `已成功(${this._successStatusCount})`, 304 | className: "green", 305 | }); 306 | } 307 | if (this._pendingStatusCount) { 308 | result.push({ 309 | message: `加载中(${this._pendingStatusCount})`, 310 | className: "blue", 311 | }); 312 | } 313 | if (this._readyStatusCount) { 314 | result.push({ 315 | message: `准备中(${this._readyStatusCount})`, 316 | className: "blue", 317 | }); 318 | } 319 | if (this._failStatusCount) { 320 | result.push({ 321 | message: `已失败(${this._failStatusCount})`, 322 | className: "red", 323 | }); 324 | } 325 | } else if (this.shouldContinue) { 326 | const title = "准备就绪"; 327 | result.push( 328 | { icon: "checkCircleFilled", title, className: "green" }, 329 | { message: title, className: "green" } 330 | ); 331 | } 332 | if (!this.hasChange) { 333 | const title = "暂无改动"; 334 | result.push( 335 | { icon: "infoCircleFilled", title, className: "yellow" }, 336 | { message: title, className: "yellow" } 337 | ); 338 | } 339 | if (this._emptyCount) { 340 | const title = `文件名为空(${this._emptyCount})`; 341 | result.push( 342 | { icon: "closeCircleFilled", title, className: "red" }, 343 | { message: title, className: "red" } 344 | ); 345 | } 346 | if (this._repeatCount) { 347 | const title = `文件名重复(${this._repeatCount})`; 348 | result.push( 349 | { icon: "closeCircleFilled", title, className: "red" }, 350 | { message: title, className: "red" } 351 | ); 352 | } 353 | const checked = this._currentList.length - this._uncheckedList.size; 354 | if (checked > 0) { 355 | result.push({ message: `已选中(${checked})`, className: "blue" }); 356 | } 357 | if (this._uncheckedList.size > 0) { 358 | result.push({ 359 | message: `未选中(${this._uncheckedList.size})`, 360 | className: "yellow", 361 | }); 362 | } 363 | // if (this._uncheckedList.size && checked) { 364 | // if (this._uncheckedList.size > checked) { 365 | // result.push({ message: `已选中(${checked})`, className: "blue" }); 366 | // } else { 367 | // result.push({ 368 | // message: `未选中(${this._uncheckedList.size})`, 369 | // className: "yellow", 370 | // }); 371 | // } 372 | // } 373 | const unmatchedCount = checked - this._matchedCount; 374 | if (unmatchedCount && this._matchedCount) { 375 | if (unmatchedCount > this._matchedCount) { 376 | result.push({ 377 | message: `已匹配(${this._matchedCount})`, 378 | className: "blue", 379 | }); 380 | } else { 381 | result.push({ 382 | message: `未匹配(${unmatchedCount})`, 383 | className: "yellow", 384 | }); 385 | } 386 | } 387 | } 388 | this.statusList = result; 389 | } 390 | 391 | // 文件列表更新回调函数集合 392 | private _currentListUpdateHandlerSet: Set<(currentList: IListItem[]) => void> = new Set(); 393 | // 绑定文件列表更新回调函数 394 | public onCurrentListUpdate(handler: (currentList: IListItem[]) => void): void { 395 | if (!this._currentListUpdateHandlerSet.has(handler)) { 396 | this._currentListUpdateHandlerSet.add(handler); 397 | } 398 | } 399 | // 解绑文件列表更新回调函数 400 | public offCurrentListUpdate(handler: (currentList: IListItem[]) => void): void { 401 | if (this._currentListUpdateHandlerSet.has(handler)) { 402 | this._currentListUpdateHandlerSet.delete(handler); 403 | } 404 | } 405 | // 触发文件列表更新回调函数 406 | private _emitCurrentListUpdateHandler(): void { 407 | this._currentListUpdateHandlerSet.forEach((handler) => { 408 | handler(this._currentList); 409 | }); 410 | } 411 | 412 | // 未选中的文件列表 413 | private _uncheckedList: Set = new Set(); 414 | // 更新是否选中文件列表 415 | public updateItemIsChecked(item: IListItem, val: boolean): void { 416 | item.isChecked = val; 417 | if (val) { 418 | this._uncheckedList.delete(item.id); 419 | } else { 420 | this._uncheckedList.add(item.id); 421 | } 422 | this._updateItemSortByIsChecked(); 423 | this._updateCurrentList(); 424 | } 425 | // 更新是否全选 426 | public updateCheckedAll(val: boolean): void { 427 | this.hasCheckedAll = val; 428 | this.hasUncheckedAll = !val; 429 | if (val) { 430 | this._uncheckedList = new Set(); 431 | } else { 432 | this._currentList.forEach((item) => { 433 | this._uncheckedList.add(item.id); 434 | }); 435 | } 436 | this._updateItemSortByCheckedAll(val); 437 | this._updateCurrentList(); 438 | } 439 | private _clearUncheckedList() { 440 | this._uncheckedList.clear(); 441 | } 442 | 443 | // 初始化拖动排序 444 | private _sortableInstance: Sortable | null = null; 445 | private _initDragSort() { 446 | querySelector(".rename-preview-content-table-body", 10).then((res) => { 447 | this._sortableInstance = Sortable.create(res, { 448 | handle: ".rename-preview-content-table-item-index-handler", 449 | filter: ".rename-preview-content-table-item.block-drop", 450 | draggable: ".rename-preview-content-table-item", 451 | ghostClass: "rename-preview-content-table-item-placeholder", 452 | fallbackClass: "rename-preview-content-table-item-dragged", 453 | forceFallback: true, 454 | onSort: (event) => { 455 | if (isNumber(event.newIndex) && isNumber(event.oldIndex)) { 456 | this._sortListItem(event.newIndex, event.oldIndex); 457 | } 458 | }, 459 | }); 460 | }); 461 | } 462 | private _destroyDragSort() { 463 | if (this._sortableInstance) { 464 | this._sortableInstance.destroy(); 465 | this._sortableInstance = null; 466 | } 467 | } 468 | // 排序 469 | private _indexMap: Map = new Map(); 470 | private _clearIndexMap(): void { 471 | this._indexMap.clear(); 472 | } 473 | private _initListItemIndex() { 474 | this.originList.forEach((item, index) => { 475 | this._setListItemIndex(item.id, index); 476 | }); 477 | } 478 | private _setListItemIndex(id: string, index: number): number { 479 | const temp = this._indexMap.get(id); 480 | if (!temp) { 481 | this._indexMap.set(id, { index, displayIndex: index + 1 }); 482 | } else if (temp.index !== index) { 483 | temp.index = index; 484 | } 485 | return index; 486 | } 487 | private _setListItemDisplayIndex(id: string, displayIndex?: number): number | undefined { 488 | const temp = this._indexMap.get(id); 489 | if (!temp) { 490 | if (displayIndex) { 491 | this._indexMap.set(id, { index: displayIndex - 1, displayIndex }); 492 | } else { 493 | this._indexMap.set(id, { index: this._findListItemIndex(id) }); 494 | } 495 | } else if (temp.displayIndex !== displayIndex) { 496 | temp.displayIndex = displayIndex; 497 | } 498 | return displayIndex; 499 | } 500 | private _getListItemIndex(id: string): number { 501 | const temp = this._indexMap.get(id); 502 | if (temp) { 503 | return temp.index; 504 | } 505 | return this._findListItemIndex(id); 506 | } 507 | private _getListItemDisplayIndex(id: string): number | undefined { 508 | const temp = this._indexMap.get(id); 509 | if (temp) { 510 | return temp.displayIndex; 511 | } 512 | return this._findListItemIndex(id) + 1; 513 | } 514 | private _findListItemIndex(id: string): number { 515 | let index = this.originList.findIndex((item) => item.id === id); 516 | if (index !== -1) { 517 | index = this.originList.length; 518 | } 519 | this._setListItemIndex(id, index); 520 | return index; 521 | } 522 | private _sortListItem(newIndex: number, oldIndex: number): void { 523 | if (newIndex === oldIndex) { 524 | return; 525 | } 526 | const currentList = [...this._currentList]; 527 | currentList.splice(newIndex, 0, currentList.splice(oldIndex, 1)[0]); 528 | const start = newIndex > oldIndex ? oldIndex : newIndex; 529 | const end = newIndex > oldIndex ? newIndex : oldIndex; 530 | for (let index = start; index <= end; index++) { 531 | this._setListItemIndex(currentList[index].id, index); 532 | if (!this.replaceParams.sortChecked) { 533 | this._setListItemDisplayIndex(currentList[index].id, index + 1); 534 | } 535 | } 536 | if (this.replaceParams.sortChecked) { 537 | let index = 1; 538 | currentList.forEach((item) => { 539 | this._setListItemDisplayIndex(item.id, item.isChecked ? index++ : undefined); 540 | }); 541 | } 542 | this._updateCurrentList(); 543 | } 544 | private _updateItemSortByIsChecked() { 545 | if (this.replaceParams.sortChecked) { 546 | let index = 1; 547 | this._currentList.forEach((item) => { 548 | this._setListItemDisplayIndex(item.id, item.isChecked ? index++ : undefined); 549 | }); 550 | } 551 | } 552 | private _updateItemSortByCheckedAll(val: boolean) { 553 | if (this.replaceParams.sortChecked) { 554 | if (val) { 555 | this._currentList.forEach((item, index) => { 556 | this._setListItemDisplayIndex(item.id, index + 1); 557 | }); 558 | } else { 559 | this._currentList.forEach((item) => { 560 | this._setListItemDisplayIndex(item.id); 561 | }); 562 | } 563 | } 564 | } 565 | private _onSortCheckedChange(val: boolean) { 566 | if (val) { 567 | let index = 1; 568 | this._currentList.forEach((item) => { 569 | this._setListItemDisplayIndex(item.id, item.isChecked ? index++ : undefined); 570 | }); 571 | } else { 572 | this._currentList.forEach((item, index) => { 573 | this._setListItemDisplayIndex(item.id, index + 1); 574 | }); 575 | } 576 | } 577 | 578 | // 刷新数据 579 | protected abstract refresh(): Promise; 580 | // 发起重命名请求 581 | protected abstract renameRequest(data?: IListItem[]): Promise; 582 | // 批量重命名 583 | public batchRename(): void { 584 | if (!this.shouldContinue) { 585 | return; 586 | } 587 | this.isUpdateLoading = true; 588 | this.replaceParamsDisabled = true; 589 | this._updateStatusList(); 590 | const data = this.currentList.filter( 591 | (item) => item.isChecked && item.isChange && !item.isError 592 | ); 593 | this.renameRequest(data) 594 | .then(() => { 595 | message.success("批量重命名成功"); 596 | this.visible = false; 597 | this._resetReplaceParams(); 598 | }) 599 | .catch(() => { 600 | message.error("重命名失败"); 601 | this.refresh().then(() => { 602 | this._updateOriginList(); 603 | }); 604 | }) 605 | .finally(() => { 606 | this.isUpdateLoading = false; 607 | this.replaceParamsDisabled = false; 608 | this._updateStatusList(); 609 | }); 610 | } 611 | // 重置 612 | public reset(): void { 613 | this._resetReplaceParams(); 614 | this._updateOriginList(); 615 | } 616 | // 重置排序 617 | public resetSort(): void { 618 | this._clearIndexMap(); 619 | this._initListItemIndex(); 620 | this._updateCurrentList(); 621 | } 622 | } 623 | 624 | export class ReplaceParams { 625 | // 剧名 626 | private _title: string = ""; 627 | public get title() { 628 | return this._title; 629 | } 630 | public set title(val) { 631 | this._title = val; 632 | this._onUpdate("title"); 633 | } 634 | // 季数 635 | private _season: string = ""; 636 | public get season() { 637 | return this._season; 638 | } 639 | public set season(val) { 640 | this._season = val; 641 | this._onUpdate("season"); 642 | } 643 | // 正则 644 | private _pattern: string = ""; 645 | public get pattern() { 646 | return this._pattern; 647 | } 648 | public set pattern(val) { 649 | this._pattern = val; 650 | this._onUpdate("pattern"); 651 | } 652 | // 替换文本 653 | private _replace: string = ""; 654 | public get replace() { 655 | return this._replace; 656 | } 657 | public set replace(val) { 658 | this._replace = val; 659 | this._onUpdate("replace"); 660 | } 661 | // 自动集数 662 | public _autoEpisode: boolean = true; 663 | public get autoEpisode() { 664 | return this._autoEpisode; 665 | } 666 | public set autoEpisode(val) { 667 | this._autoEpisode = val; 668 | this._onUpdate("autoEpisode"); 669 | } 670 | // 排序已选 671 | public _sortChecked: boolean = false; 672 | public get sortChecked() { 673 | return this._sortChecked; 674 | } 675 | public set sortChecked(val) { 676 | this._sortChecked = val; 677 | this._onUpdate("sortChecked"); 678 | } 679 | // 重命名模式 680 | private _renameMode: TRenameMode = RENAME_MODE_SERIES; 681 | public get renameMode() { 682 | return this._renameMode; 683 | } 684 | public set renameMode(val) { 685 | this._renameMode = val; 686 | this._onUpdate("renameMode"); 687 | } 688 | 689 | private _stopUpdate = false; 690 | private _onUpdate = (key: TReplaceParamsKeys | TReplaceParamsKeys[]) => { 691 | if (!this._stopUpdate) { 692 | if (this._onChangeHandler) { 693 | if (isArray(key)) { 694 | for (let index = 0; index < key.length; index++) { 695 | this._onChangeHandler(key[index], this); 696 | } 697 | } else { 698 | this._onChangeHandler(key, this); 699 | } 700 | } 701 | this._onUpdateHandler && this._onUpdateHandler(this); 702 | } 703 | }; 704 | private _onUpdateHandler?: (replaceParams: ReplaceParams) => void; 705 | private _onChangeHandler?: (key: TReplaceParamsKeys, replaceParams: ReplaceParams) => void; 706 | 707 | public reset(val?: any) { 708 | this._stopUpdate = true; 709 | this.title = val?.title || ""; 710 | this.season = val?.season || ""; 711 | this.pattern = val?.pattern || ""; 712 | this.replace = val?.replace || ""; 713 | this.autoEpisode = val ? !!val.autoEpisode : true; 714 | this.sortChecked = val ? !!val.sortChecked : false; 715 | this.renameMode = val?.renameMode || RENAME_MODE_SERIES; 716 | this._stopUpdate = false; 717 | const keys: TReplaceParamsKeys[] = [ 718 | "title", 719 | "season", 720 | "pattern", 721 | "replace", 722 | "autoEpisode", 723 | "sortChecked", 724 | "renameMode", 725 | ]; 726 | this._onUpdate(keys); 727 | } 728 | 729 | constructor( 730 | options: { 731 | onUpdateHandler?: (replaceParams: ReplaceParams) => void; 732 | onChangeHandler?: (key: TReplaceParamsKeys, replaceParams: ReplaceParams) => void; 733 | } = {} 734 | ) { 735 | if (options.onUpdateHandler) { 736 | this._onUpdateHandler = options.onUpdateHandler; 737 | } 738 | if (options.onChangeHandler) { 739 | this._onChangeHandler = options.onChangeHandler; 740 | } 741 | } 742 | } 743 | 744 | export type TReplaceParamsKeys = 745 | | "title" 746 | | "season" 747 | | "pattern" 748 | | "replace" 749 | | "autoEpisode" 750 | | "sortChecked" 751 | | "renameMode"; 752 | 753 | export type TRenameMode = "series" | "pattern"; 754 | export const RENAME_MODE_SERIES: TRenameMode = "series"; 755 | export const RENAME_MODE_PATTERN: TRenameMode = "pattern"; 756 | 757 | export type TRootElementInsertMethod = "append" | "prepend"; 758 | export const ROOT_ELEMENT_INSERT_METHOD_APPEND: TRootElementInsertMethod = "append"; 759 | export const ROOT_ELEMENT_INSERT_METHOD_PREPEND: TRootElementInsertMethod = "prepend"; 760 | 761 | export interface IOriginListItem { 762 | id: string; 763 | ext: string; 764 | path?: string; 765 | index: number; 766 | fileName: string; 767 | fullFileName: string; 768 | } 769 | 770 | export type TListItemStatus = "none" | "ready" | "pending" | "success" | "fail"; 771 | export const LIST_ITEM_STATUS_NONE: TListItemStatus = "none"; 772 | export const LIST_ITEM_STATUS_READY: TListItemStatus = "ready"; 773 | export const LIST_ITEM_STATUS_PENDING: TListItemStatus = "pending"; 774 | export const LIST_ITEM_STATUS_SUCCESS: TListItemStatus = "success"; 775 | export const LIST_ITEM_STATUS_FAIL: TListItemStatus = "fail"; 776 | 777 | export interface IListItem { 778 | id: string; 779 | ext: string; 780 | path?: string; 781 | index: number; 782 | status: TListItemStatus; 783 | isEmpty: boolean; 784 | isError: boolean; 785 | isRepeat: boolean; 786 | fileName: string; 787 | isChange: boolean; 788 | diffList?: Change[]; 789 | isMatched: boolean; 790 | isChecked: boolean; 791 | isLoading: boolean; 792 | oldFileName: string; 793 | newFileName: string; 794 | displayIndex?: number; 795 | } 796 | 797 | export interface IStatusList { 798 | icon?: string; 799 | color?: string; 800 | title?: string; 801 | message?: string; 802 | className?: string; 803 | } 804 | 805 | export default Provider; 806 | --------------------------------------------------------------------------------