├── .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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | 重命名
8 |
9 |
10 |
11 |
12 |
36 |
37 |
67 |
--------------------------------------------------------------------------------
/src/provider/lib/baidu/EnterComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 重命名
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 | 重命名
8 |
9 |
10 |
11 |
12 |
36 |
37 |
86 |
--------------------------------------------------------------------------------
/src/provider/lib/quark/EnterComponent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 重命名
8 |
9 |
10 |
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 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
2 |
12 |
13 |
14 |
15 |
16 | {{ label }}
17 |
18 |
19 |
20 |
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 |
2 |
12 |
13 |
19 |
20 |
21 | {{ label }}
22 |
23 |
24 |
25 |
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 |
2 |
10 | {{ label }}
11 |
24 |
37 |
38 |
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 |
2 |
3 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
45 |
50 |
51 |
52 |
57 |
58 |
59 |
60 |
98 |
99 |
100 |
101 |
102 |
103 |
150 |
151 |
230 |
--------------------------------------------------------------------------------
/src/components/Component/ComponentMessage.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
16 |
17 |
22 |
23 | {{ message }}
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 |
36 |
43 |
44 |
45 |
46 |
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 |
2 |
3 |
4 |
11 |
17 | {{ item.message }}
18 |
19 |
20 |
21 |
22 |
73 |
74 |
90 |
91 |
96 |
97 |
98 |
102 |
103 | {{ item.displayIndex }}
104 |
105 |
106 |
107 |
108 |
112 |
113 | {{ item.oldFileName }}
114 |
115 |
116 | ⮕
117 |
118 |
122 |
123 | {{ item.newFileName }}
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
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 |
--------------------------------------------------------------------------------