├── .deepsource.toml ├── .eslintrc-auto-import.json ├── .eslintrc.js ├── .github └── workflows │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .stylelintignore ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.2.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── build ├── buildAfterPack.js └── icons │ ├── 256x256.png │ ├── 512x512.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── commitlint.config.js ├── dev-app-update.yml ├── package.json ├── packages ├── common │ ├── charCode.ts │ ├── color.ts │ ├── constant.ts │ ├── cookies.ts │ ├── dom.ts │ ├── index.ts │ ├── logger.ts │ ├── net.ts │ ├── object.ts │ ├── string.ts │ ├── timer.ts │ └── uri.ts ├── inject │ ├── index.ts │ ├── tsconfig.json │ ├── utils.ts │ └── vite.config.ts ├── interfaces │ ├── .gitignore │ ├── cookies.d.ts │ ├── index.d.ts │ ├── net.d.ts │ ├── package.json │ ├── plugin.ts │ ├── storage.d.ts │ ├── tabs.d.ts │ ├── view.ts │ ├── window.d.ts │ └── yarn.lock ├── main │ ├── application.ts │ ├── core │ │ ├── db │ │ │ ├── index.ts │ │ │ └── types.ts │ │ └── plugin │ │ │ ├── handler.ts │ │ │ ├── index.ts │ │ │ ├── plugin.ts │ │ │ └── tab.ts │ ├── index.ts │ ├── ipcMain.ts │ ├── menus │ │ ├── main.ts │ │ └── view.ts │ ├── models │ │ ├── protocol.ts │ │ └── sessions.ts │ ├── services │ │ ├── adblocker.ts │ │ ├── autoUpdater.ts │ │ └── storage.ts │ ├── tsconfig.json │ ├── utils │ │ ├── getUrl.ts │ │ └── index.ts │ ├── view.ts │ ├── viewManager.ts │ ├── vite.config.ts │ └── windows │ │ ├── common.ts │ │ ├── main.ts │ │ └── selectPart.ts ├── preload │ ├── index.ts │ ├── tsconfig.json │ ├── utils │ │ ├── loading.ts │ │ └── versions.ts │ └── vite.config.ts └── renderer │ ├── index.html │ ├── src │ ├── App.vue │ ├── apis │ │ └── plugin.ts │ ├── assets │ │ ├── css │ │ │ ├── css-variables.less │ │ │ └── global.less │ │ └── images │ │ │ └── icon.png │ ├── auto-imports.d.ts │ ├── components │ │ ├── TopBar.vue │ │ └── ui │ │ │ ├── _utils │ │ │ └── global-config.ts │ │ │ ├── button │ │ │ ├── Button.vue │ │ │ └── index.ts │ │ │ ├── icon │ │ │ ├── icons │ │ │ │ ├── TargetTwo.tsx │ │ │ │ └── Windmill.tsx │ │ │ ├── index.tsx │ │ │ ├── map.tsx │ │ │ └── runtime │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ └── settings │ │ │ ├── SettingsContainer.vue │ │ │ ├── SettingsTile.vue │ │ │ └── index.ts │ ├── env.d.ts │ ├── global.d.ts │ ├── main.ts │ ├── router │ │ ├── index.ts │ │ └── types.d.ts │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ ├── app │ │ │ ├── index.ts │ │ │ └── types.ts │ │ │ └── tabs │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── type.ts │ ├── utils │ │ ├── index.ts │ │ ├── ipc.ts │ │ ├── request.ts │ │ └── view.ts │ └── views │ │ ├── Main.vue │ │ ├── SelectPart.vue │ │ └── pages │ │ ├── home │ │ ├── Home.vue │ │ └── WebNav.vue │ │ ├── plugin │ │ └── Plugin.vue │ │ └── settings │ │ ├── About.vue │ │ └── Settings.vue │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── postcss.config.js ├── prettier.config.js ├── resources └── pages │ └── network-error.html ├── scripts ├── build.mjs └── watch.mjs ├── stylelint.config.js ├── tsconfig.json ├── types.d.ts └── yarn.lock /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "shell" 5 | enabled = true -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "acceptHMRUpdate": "readonly", 4 | "axios": "readonly", 5 | "computed": "readonly", 6 | "createApp": "readonly", 7 | "createPinia": "readonly", 8 | "customRef": "readonly", 9 | "defineAsyncComponent": "readonly", 10 | "defineComponent": "readonly", 11 | "defineStore": "readonly", 12 | "effectScope": "readonly", 13 | "EffectScope": "readonly", 14 | "getActivePinia": "readonly", 15 | "getCurrentInstance": "readonly", 16 | "getCurrentScope": "readonly", 17 | "h": "readonly", 18 | "inject": "readonly", 19 | "isReadonly": "readonly", 20 | "isRef": "readonly", 21 | "mapActions": "readonly", 22 | "mapGetters": "readonly", 23 | "mapState": "readonly", 24 | "mapStores": "readonly", 25 | "mapWritableState": "readonly", 26 | "markRaw": "readonly", 27 | "nextTick": "readonly", 28 | "onActivated": "readonly", 29 | "onBeforeMount": "readonly", 30 | "onBeforeUnmount": "readonly", 31 | "onBeforeUpdate": "readonly", 32 | "onDeactivated": "readonly", 33 | "onErrorCaptured": "readonly", 34 | "onMounted": "readonly", 35 | "onRenderTracked": "readonly", 36 | "onRenderTriggered": "readonly", 37 | "onScopeDispose": "readonly", 38 | "onServerPrefetch": "readonly", 39 | "onUnmounted": "readonly", 40 | "onUpdated": "readonly", 41 | "provide": "readonly", 42 | "reactive": "readonly", 43 | "readonly": "readonly", 44 | "ref": "readonly", 45 | "resolveComponent": "readonly", 46 | "setActivePinia": "readonly", 47 | "setMapStoreSuffix": "readonly", 48 | "shallowReactive": "readonly", 49 | "shallowReadonly": "readonly", 50 | "shallowRef": "readonly", 51 | "storeToRefs": "readonly", 52 | "toRaw": "readonly", 53 | "toRef": "readonly", 54 | "toRefs": "readonly", 55 | "triggerRef": "readonly", 56 | "unref": "readonly", 57 | "useAttrs": "readonly", 58 | "useCssModule": "readonly", 59 | "useCssVars": "readonly", 60 | "useRoute": "readonly", 61 | "useRouter": "readonly", 62 | "useSlots": "readonly", 63 | "watch": "readonly", 64 | "watchEffect": "readonly" 65 | } 66 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | es6: true, 7 | 'vue/setup-compiler-macros': true, 8 | }, 9 | parser: 'vue-eslint-parser', 10 | parserOptions: { 11 | parser: '@typescript-eslint/parser', 12 | ecmaVersion: 2020, 13 | sourceType: 'module', 14 | jsxPragma: 'React', 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | }, 19 | plugins: ['vue', '@typescript-eslint', 'prettier'], 20 | extends: [ 21 | './.eslintrc-auto-import.json', 22 | 'plugin:vue/vue3-recommended', 23 | 'eslint:recommended', 24 | 'plugin:@typescript-eslint/recommended', 25 | 'plugin:prettier/recommended', 26 | ], 27 | rules: { 28 | '@typescript-eslint/no-namespace': 'off', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | '@typescript-eslint/ban-ts-ignore': 'off', 31 | '@typescript-eslint/ban-ts-comment': 'off', 32 | '@typescript-eslint/no-unused-vars': [ 33 | 'error', 34 | { 35 | argsIgnorePattern: '^_', 36 | varsIgnorePattern: '^_', 37 | }, 38 | ], 39 | 'no-unused-vars': [ 40 | 'error', 41 | { 42 | argsIgnorePattern: '^_', 43 | varsIgnorePattern: '^_', 44 | }, 45 | ], 46 | 'vue/script-setup-uses-vars': 'error', 47 | 'vue/html-self-closing': [ 48 | 'error', 49 | { 50 | html: { 51 | void: 'always', 52 | normal: 'never', 53 | component: 'always', 54 | }, 55 | svg: 'always', 56 | math: 'always', 57 | }, 58 | ], 59 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '37 9 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/samuelmeuli/action-electron-builder 2 | 3 | name: Build/release 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | release: 12 | runs-on: ${{ matrix.os }} 13 | 14 | # Platforms to build on/for 15 | strategy: 16 | matrix: 17 | # os: [macos-latest, ubuntu-latest, windows-latest] 18 | os: [ubuntu-latest, windows-latest] 19 | 20 | steps: 21 | - name: Check out Git repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Install Node.js, NPM and Yarn 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - name: Install Snapcraft 30 | uses: samuelmeuli/action-snapcraft@v1 31 | # Only install Snapcraft on Ubuntu 32 | if: startsWith(matrix.os, 'ubuntu') 33 | with: 34 | # Log in to Snap Store 35 | snapcraft_token: ${{ secrets.snapcraft_token }} 36 | 37 | - name: Yarn install 38 | run: | 39 | yarn 40 | 41 | - name: Build & release app 42 | run: | 43 | yarn release 44 | env: 45 | GH_TOKEN: ${{ secrets.TOKEN }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | node_modules/ 4 | **/.DS_Store 5 | 6 | dist/ 7 | release/ 8 | package-lock.json 9 | 10 | .yarn/* 11 | .yarn/cache 12 | !.yarn/releases 13 | !.yarn/patches 14 | !.yarn/plugins 15 | !.yarn/sdks 16 | !.yarn/versions 17 | 18 | packages/renderer/src/components.d.ts 19 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # https://github.com/SalhiYassine/next-ts-template/blob/main/.husky/pre-commit 5 | 6 | # If everything passes... Now we can commit 7 | echo '🤔🤔🤔🤔 Trying to build now. 🤔🤔🤔🤔' 8 | 9 | yarn run prebuild || 10 | ( 11 | echo '❌👷🔨❌ Better call Bob... Because your build failed ❌👷🔨❌ 12 | Next build failed: View the errors above to see why. 13 | ' 14 | false; 15 | ) 16 | 17 | # If everything passes... Now we can commit 18 | echo '✅✅✅✅ You win this time... I am committing this now. ✅✅✅✅' 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/renderer/src/components.d.ts 2 | packages/renderer/src/auto-imports.d.ts 3 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | resources/plugins/ 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["johnsoncodehk.volar", "voorjaar.windicss-intellisense", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"], 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | ".yarn": true, 4 | "yarn.lock": true, 5 | ".eslintrc-auto-import.json": true 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "typescript.enablePromptUseWorkspaceTsdk": true 9 | // "css.validate": false, 10 | // "less.validate": false 11 | } 12 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | checksumBehavior: update 2 | 3 | nodeLinker: node-modules 4 | 5 | npmRegistryServer: "https://registry.npm.taobao.org" 6 | 7 | plugins: 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: "@yarnpkg/plugin-interactive-tools" 10 | 11 | yarnPath: .yarn/releases/yarn-3.2.0.cjs 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 liumingye 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webmini 2 | 3 | ## 迷你 web 应用 4 | 5 | ![GitHub](https://img.shields.io/github/license/liumingye/webmini) ![GitHub package.json version](https://img.shields.io/github/package-json/v/liumingye/webmini) ![GitHub last commit](https://img.shields.io/github/last-commit/liumingye/webmini) [![Build/release](https://github.com/liumingye/webmini/actions/workflows/release.yml/badge.svg?branch=main)](https://github.com/liumingye/webmini/actions/workflows/release.yml) 6 | 7 | ## 💽 安装稳定版 8 | 9 | [GitHub](https://github.com/liumingye/webmini/releases) 或 [Hazel(速度比较快)](https://webmini.vercel.app) 提供了已经编译好的稳定版安装包,当然你也可以自己克隆代码编译打包。 10 | 11 | ## ✨ 特性 12 | 13 | - 小窗口 14 | - 总在最前 15 | - 同时支持 Windows/Mac 16 | - 可拓展 17 | 18 | ## 🖥 应用界面 19 | 20 | ![](https://ae01.alicdn.com/kf/He81afd1338794a5582bc4e8e7e6f3c17w.png) 21 | 22 | ![](https://ae01.alicdn.com/kf/Hd16eae4af9154bdfa7f861c6cbc31c78c.png) 23 | 24 | ![](https://ae01.alicdn.com/kf/H18a6522c15254a688ed418c684c74997s.png) 25 | 26 | ![](https://ae01.alicdn.com/kf/H9e8bdce3125f41ef9c051b81fe6f290f0.png) 27 | 28 | ![](https://ae01.alicdn.com/kf/H27880bddc8be4eef986523d4ff60cbaez.png) 29 | 30 | ![](https://ae01.alicdn.com/kf/H5710f7fbaf38452da4b05b60f27638dfg.png) 31 | 32 | ## ⌨️ 本地开发 33 | 34 | ### 克隆代码 35 | 36 | ```bash 37 | git clone git@github.com:liumingye/webmini.git 38 | ``` 39 | 40 | ### 安装依赖 41 | 42 | ``` 43 | corepack enable 44 | yarn install 45 | ``` 46 | 47 | > Error: Electron failed to install correctly, please delete node_modules/electron and try installing again 48 | 49 | `Electron` 下载安装失败的问题,解决方式请参考 https://github.com/electron/electron/issues/8466#issuecomment-571425574 50 | 51 | 或者使用 52 | 53 | ``` 54 | yarn dlx electron-fix start 55 | ``` 56 | 57 | ### 开发模式 58 | 59 | ``` 60 | yarn dev 61 | ``` 62 | 63 | ### 编译打包 64 | 65 | ``` 66 | yarn build 67 | yarn build:mac 68 | yarn build:win 69 | yarn build:linux 70 | ``` 71 | -------------------------------------------------------------------------------- /build/buildAfterPack.js: -------------------------------------------------------------------------------- 1 | /* 基于 APP 打包后的钩子功能补充语言包配置,以满足类似“旧名字”在Mac文件系统中显示为“新名字”的需求 */ 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const fs = require('fs') 4 | 5 | exports.default = async (context) => { 6 | const { electronPlatformName, appOutDir } = context 7 | if (electronPlatformName !== 'darwin') { 8 | return 9 | } 10 | const { 11 | productFilename, 12 | info: { 13 | _metadata: { electronLanguagesInfoPlistStrings }, 14 | }, 15 | } = context.packager.appInfo 16 | 17 | const resPath = `${appOutDir}/${productFilename}.app/Contents/Resources/` 18 | console.log( 19 | '\n> 基于 package.json 配置项 “electronLanguagesInfoPlistStrings” 创建语言包 Sta \n', 20 | '\n> electronLanguagesInfoPlistStrings:\n', 21 | electronLanguagesInfoPlistStrings, 22 | '\n\n', 23 | '> ResourcesPath:', 24 | resPath, 25 | ) 26 | 27 | // 创建APP语言包文件 28 | const createLangFilesPromise = await Promise.all( 29 | Object.keys(electronLanguagesInfoPlistStrings).map((langKey) => { 30 | const infoPlistStrPath = `${langKey}.lproj/InfoPlist.strings` 31 | let infos = '' 32 | const langItem = electronLanguagesInfoPlistStrings[langKey] 33 | Object.keys(langItem).forEach((infoKey) => { 34 | infos += `"${infoKey}" = "${langItem[infoKey]}";\n` 35 | }) 36 | return new Promise((resolve) => { 37 | fs.writeFile(`${resPath}${infoPlistStrPath}`, infos, (err) => { 38 | resolve() 39 | if (err) throw err 40 | console.log(`> “{ResourcesPath}/${infoPlistStrPath}” 创建完毕。`) 41 | }) 42 | }) 43 | }), 44 | ) 45 | console.log('\n> 基于 package.json 配置项 “electronLanguagesInfoPlistStrings” 创建语言包 End \n') 46 | return createLangFilesPromise 47 | } 48 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumingye/webmini/9ec67af3fcab8034fdd787becd57426e22c0f087/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumingye/webmini/9ec67af3fcab8034fdd787becd57426e22c0f087/build/icons/512x512.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumingye/webmini/9ec67af3fcab8034fdd787becd57426e22c0f087/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumingye/webmini/9ec67af3fcab8034fdd787becd57426e22c0f087/build/icons/icon.ico -------------------------------------------------------------------------------- /build/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumingye/webmini/9ec67af3fcab8034fdd787becd57426e22c0f087/build/icons/icon.png -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignores: [(commit) => commit.includes('init')], 3 | extends: ['@commitlint/config-conventional'], 4 | rules: { 5 | 'body-leading-blank': [2, 'always'], 6 | 'footer-leading-blank': [1, 'always'], 7 | 'header-max-length': [2, 'always', 108], 8 | 'subject-empty': [2, 'never'], 9 | 'type-empty': [2, 'never'], 10 | 'subject-case': [0], 11 | 'type-enum': [ 12 | 2, 13 | 'always', 14 | [ 15 | 'feat', 16 | 'fix', 17 | 'perf', 18 | 'style', 19 | 'docs', 20 | 'test', 21 | 'refactor', 22 | 'build', 23 | 'ci', 24 | 'chore', 25 | 'revert', 26 | 'wip', 27 | 'workflow', 28 | 'types', 29 | 'release', 30 | ], 31 | ], 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: github 2 | owner: liumingye 3 | repo: webmini 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmini", 3 | "version": "1.0.8-beta.2", 4 | "author": "liumingye ", 5 | "main": "dist/main/index.cjs", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/liumingye/webmini.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/liumingye/webmini/issues" 13 | }, 14 | "homepage": "https://github.com/liumingye/webmini", 15 | "packageManager": "yarn@3.2.0", 16 | "scripts": { 17 | "dev": "node scripts/watch.mjs", 18 | "format": "prettier --write ./packages", 19 | "stylelint": "stylelint --fix \"packages/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", 20 | "prebuild": "node scripts/build.mjs", 21 | "build": "yarn prebuild && electron-builder", 22 | "build:mac": "yarn build -m", 23 | "build:win": "yarn build -w", 24 | "build:linux": "yarn build -l", 25 | "lint:prettier": "prettier --check ./packages", 26 | "lint:eslint": "eslint --cache --cache-location node_modules/.cache/eslint/ --max-warnings 0 ./packages --ext .ts,.tsx,.vue --fix", 27 | "release": "yarn build --publish always", 28 | "electron-fix": "yarn dlx electron-fix start" 29 | }, 30 | "engines": { 31 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 32 | }, 33 | "dependencies": { 34 | "@cliqz/adblocker-electron": "^1.23.7", 35 | "@electron/remote": "^2.0.8", 36 | "axios": "^0.26.1", 37 | "electron-is": "^3.0.0", 38 | "lodash": "^4.17.21", 39 | "node-gyp-build": "^4.4.0", 40 | "pouchdb": "^7.2.2", 41 | "tar": "^6.1.11", 42 | "vue-request": "^1.2.4", 43 | "winston": "^3.7.2", 44 | "winston-daily-rotate-file": "^4.6.1" 45 | }, 46 | "devDependencies": { 47 | "@arco-design/web-vue": "^2.23.0", 48 | "@commitlint/cli": "^16.2.3", 49 | "@commitlint/config-conventional": "^16.2.1", 50 | "@types/lodash": "^4.14.181", 51 | "@types/nprogress": "^0.2.0", 52 | "@types/overlayscrollbars": "^1.12.1", 53 | "@types/pouchdb": "^6.4.0", 54 | "@types/tar": "^6.1.1", 55 | "@typescript-eslint/eslint-plugin": "^5.18.0", 56 | "@typescript-eslint/parser": "^5.18.0", 57 | "@vitejs/plugin-vue": "^2.3.1", 58 | "@vitejs/plugin-vue-jsx": "^1.3.9", 59 | "autoprefixer": "^10.4.4", 60 | "electron": "^17.4.0", 61 | "electron-builder": "^23.0.3", 62 | "electron-devtools-installer": "^3.2.0", 63 | "electron-fetch": "^1.8.0", 64 | "electron-updater": "^4.6.5", 65 | "eslint": "^8.13.0", 66 | "eslint-config-prettier": "^8.5.0", 67 | "eslint-plugin-prettier": "^4.0.0", 68 | "eslint-plugin-vue": "^8.6.0", 69 | "husky": "^7.0.4", 70 | "less": "^4.1.2", 71 | "nprogress": "^0.2.0", 72 | "overlayscrollbars": "^1.13.1", 73 | "pinia": "^2.0.13", 74 | "postcss": "^8.4.12", 75 | "postcss-html": "^1.3.0", 76 | "postcss-less": "^6.0.0", 77 | "prettier": "^2.6.2", 78 | "stylelint": "^14.6.1", 79 | "stylelint-config-rational-order": "^0.1.2", 80 | "stylelint-config-recommended-vue": "^1.4.0", 81 | "stylelint-config-standard": "^25.0.0", 82 | "stylelint-order": "^5.0.0", 83 | "typescript": "^4.6.3", 84 | "unplugin-auto-import": "^0.6.9", 85 | "unplugin-vue-components": "^0.18.5", 86 | "vite": "^2.9.1", 87 | "vite-plugin-optimize-persist": "^0.1.2", 88 | "vite-plugin-package-config": "^0.1.1", 89 | "vite-plugin-windicss": "^1.8.3", 90 | "vue": "^3.2.31", 91 | "vue-router": "^4.0.14", 92 | "windicss": "^3.5.1" 93 | }, 94 | "electronLanguagesInfoPlistStrings": { 95 | "en": { 96 | "CFBundleDisplayName": "WebMini", 97 | "CFBundleName": "WebMini" 98 | }, 99 | "zh_CN": { 100 | "CFBundleDisplayName": "WebMini", 101 | "CFBundleName": "WebMini" 102 | } 103 | }, 104 | "build": { 105 | "appId": "org.electron.webmini", 106 | "productName": "webmini", 107 | "asar": true, 108 | "artifactName": "${name}-${version}-${os}-${arch}.${ext}", 109 | "directories": { 110 | "output": "release/${version}", 111 | "buildResources": "build/icons" 112 | }, 113 | "files": [ 114 | "dist", 115 | "resources/**/*" 116 | ], 117 | "afterPack": "./build/buildAfterPack.js", 118 | "linux": { 119 | "category": "Network", 120 | "target": [ 121 | "appimage", 122 | "snap" 123 | ] 124 | }, 125 | "mac": { 126 | "category": "public.app-category.navigation", 127 | "extendInfo": { 128 | "LSHasLocalizedDisplayName": true 129 | }, 130 | "target": [ 131 | { 132 | "target": "zip", 133 | "arch": [ 134 | "x64", 135 | "arm64" 136 | ] 137 | }, 138 | { 139 | "target": "dmg", 140 | "arch": [ 141 | "x64", 142 | "arm64" 143 | ] 144 | } 145 | ], 146 | "darkModeSupport": true 147 | }, 148 | "win": { 149 | "target": [ 150 | { 151 | "target": "nsis", 152 | "arch": [ 153 | "x64", 154 | "ia32" 155 | ] 156 | }, 157 | { 158 | "target": "zip", 159 | "arch": [ 160 | "x64", 161 | "ia32" 162 | ] 163 | } 164 | ] 165 | }, 166 | "snap": { 167 | "publish": [ 168 | { 169 | "provider": "github" 170 | } 171 | ] 172 | }, 173 | "nsis": { 174 | "oneClick": false, 175 | "perMachine": false, 176 | "allowToChangeInstallationDirectory": true, 177 | "deleteAppDataOnUninstall": true 178 | }, 179 | "electronDownload": { 180 | "mirror": "https://npmmirror.com/mirrors/electron/" 181 | } 182 | }, 183 | "env": { 184 | "//": "Used in build scripts", 185 | "PORT": 3344 186 | }, 187 | "keywords": [ 188 | "bilibili", 189 | "video", 190 | "vite", 191 | "electron", 192 | "vue3", 193 | "rollup" 194 | ], 195 | "vite": { 196 | "optimizeDeps": { 197 | "include": [ 198 | "@arco-design/web-vue", 199 | "@arco-design/web-vue/es/icon", 200 | "@electron/remote", 201 | "axios", 202 | "electron", 203 | "electron-is", 204 | "less", 205 | "lodash", 206 | "nprogress", 207 | "overlayscrollbars", 208 | "pinia", 209 | "vue", 210 | "vue-request", 211 | "vue-router", 212 | "winston", 213 | "winston-daily-rotate-file" 214 | ] 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /packages/common/constant.ts: -------------------------------------------------------------------------------- 1 | import is from 'electron-is' 2 | const electron: typeof Electron = is.renderer() 3 | ? // renderer 4 | require('@electron/remote') 5 | : // main 6 | require('electron') 7 | 8 | const version = `${electron.app.name}/${electron.app.getVersion()}` 9 | 10 | export const userAgent = { 11 | desktop: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 ${version} Safari/605.1.15`, 12 | mobile: `Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 ${version} Safari/604.1`, 13 | } 14 | 15 | export const ERROR_PROTOCOL = 'webmini-error' 16 | 17 | export const NETWORK_ERROR_HOST = 'network-error' 18 | -------------------------------------------------------------------------------- /packages/common/cookies.ts: -------------------------------------------------------------------------------- 1 | import type Electron from 'electron' 2 | import is from 'electron-is' 3 | import type { CookiesApi } from '../interfaces' 4 | 5 | const electron: typeof Electron = is.renderer() 6 | ? // renderer 7 | require('@electron/remote') 8 | : // main 9 | require('electron') 10 | 11 | /** 12 | * A `Cookies` object for this session. 13 | */ 14 | class Cookies implements CookiesApi { 15 | private cookies 16 | 17 | constructor(cookies = electron.session.defaultSession.cookies) { 18 | this.cookies = cookies 19 | } 20 | 21 | public get = (filter: Electron.CookiesGetFilter) => { 22 | return this.cookies.get(filter) 23 | } 24 | } 25 | 26 | export default Cookies 27 | -------------------------------------------------------------------------------- /packages/common/dom.ts: -------------------------------------------------------------------------------- 1 | /** docoment ready */ 2 | export function domContentLoaded( 3 | condition: DocumentReadyState[] = ['complete', 'interactive'], 4 | ): Promise { 5 | return new Promise((resolve) => { 6 | if (condition.includes(document.readyState)) { 7 | resolve(undefined) 8 | } else { 9 | document.addEventListener('readystatechange', (e) => { 10 | if (condition.includes(document.readyState)) { 11 | resolve(e) 12 | } 13 | }) 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /packages/common/index.ts: -------------------------------------------------------------------------------- 1 | // `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it. 2 | export const withPrototype = (obj: Record) => { 3 | const protos = Object.getPrototypeOf(obj) 4 | for (const [key, value] of Object.entries(protos)) { 5 | if (Object.prototype.hasOwnProperty.call(obj, key)) continue 6 | if (typeof value === 'function') { 7 | // Some native APIs, like `NodeJS.EventEmitter['on']`, don't work in the Renderer process. Wrapping them into a function. 8 | obj[key] = function (...args: any) { 9 | return value.call(obj, ...args) 10 | } 11 | } else { 12 | obj[key] = value 13 | } 14 | } 15 | return obj 16 | } 17 | -------------------------------------------------------------------------------- /packages/common/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston' 2 | import is from 'electron-is' 3 | import { resolve } from 'path' 4 | import 'winston-daily-rotate-file' 5 | import node_console from 'console' 6 | 7 | const electron = is.renderer() ? require('@electron/remote') : require('electron') 8 | const level = is.dev() ? 'debug' : 'info' 9 | const logDir = electron.app.getPath('logs') 10 | const logName = `${electron.app.name}-%DATE%.log` 11 | 12 | export default createLogger({ 13 | level, 14 | format: format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), 15 | transports: [ 16 | new transports.DailyRotateFile({ 17 | level: 'debug', 18 | filename: resolve(logDir, logName), 19 | datePattern: 'YYYY-MM-DD', 20 | zippedArchive: true, 21 | maxSize: '10m', 22 | maxFiles: '7d', 23 | format: format.combine( 24 | format.printf(({ level, message, label, timestamp }) => { 25 | return `${timestamp} ${label ? `[${label}] ` : ''}${level}: ${message}` 26 | }), 27 | ), 28 | }), 29 | new transports.Console({ 30 | format: format.combine(format.colorize({ all: true })), 31 | log: ({ level, message, label, timestamp }, next) => { 32 | const print = `${timestamp} ${label ? `[${label}] ` : ''}${level}: ${message}` 33 | is.renderer() && console.log(print) 34 | node_console.log(print) 35 | next() 36 | }, 37 | }), 38 | ], 39 | }) 40 | -------------------------------------------------------------------------------- /packages/common/net.ts: -------------------------------------------------------------------------------- 1 | import is from 'electron-is' 2 | import type { NetApi, FetchOptions, NetReturn } from '../interfaces' 3 | import { merge } from 'lodash' 4 | 5 | const DEFAULT_FETCH_CONFIG: FetchOptions = { 6 | method: 'GET', 7 | body: null, 8 | headers: null, 9 | } 10 | 11 | class Net implements NetApi { 12 | public electron: typeof Electron = is.renderer() 13 | ? require('@electron/remote') 14 | : require('electron') 15 | 16 | public fetch = (url: string, options = {}) => { 17 | const config = merge(DEFAULT_FETCH_CONFIG, options) 18 | return new Promise>((resolve, reject) => { 19 | const request = this.electron.net.request({ 20 | url, 21 | method: config.method, 22 | useSessionCookies: true, 23 | }) 24 | if (config.headers instanceof Object) { 25 | for (const [header, value] of Object.entries(config.headers)) { 26 | request.setHeader(header, value) 27 | } 28 | } 29 | request.on('login', () => { 30 | reject(new Error('Unauthorized')) 31 | }) 32 | request.once('error', (error) => { 33 | request.removeAllListeners() 34 | reject(error) 35 | }) 36 | request.on('response', (response) => { 37 | response.removeAllListeners() 38 | const chunks: Uint8Array[] = [] 39 | response.on('data', (chunk) => { 40 | chunks.push(chunk) 41 | }) 42 | response.on('end', () => { 43 | response.removeAllListeners() 44 | const data = Buffer.concat(chunks).toString() 45 | resolve({ 46 | ok: response.statusCode === 200, 47 | headers: response.headers, 48 | status: response.statusCode, 49 | statusText: response.statusMessage, 50 | text: async () => data, 51 | json: async () => JSON.parse(data), 52 | }) 53 | }) 54 | response.on('error', (error: any) => { 55 | response.removeAllListeners() 56 | reject(error) 57 | }) 58 | }) 59 | if (config.body) { 60 | request.write(config.body.toString()) 61 | } 62 | request.end() 63 | }) 64 | } 65 | } 66 | 67 | export default Net 68 | -------------------------------------------------------------------------------- /packages/common/object.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isValidKey 3 | * @param {string | number | symbol} key 4 | * @param {object} object 5 | * @returns {key is keyof typeof object} 是否存在 6 | */ 7 | export const isValidKey = ( 8 | key: string | number | symbol, 9 | object: object, 10 | ): key is keyof typeof object => { 11 | return key in object 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 批量替换字符串 3 | * @param text 字符串 4 | * @param map 字符串替换的映射 5 | * @param replacer 替换的字符串 6 | * @returns 替换后的字符串 7 | */ 8 | export const replaceAll = (text: string, map: string[], replacer: string): string => { 9 | return map.reduce((acc, cur) => acc.replace(cur, replacer), text) 10 | } 11 | -------------------------------------------------------------------------------- /packages/common/timer.ts: -------------------------------------------------------------------------------- 1 | export class Timer { 2 | private timer: NodeJS.Timeout | NodeJS.Immediate | null = null 3 | 4 | private mode = 'timeout' 5 | 6 | private debug = false 7 | 8 | private logger 9 | 10 | constructor( 11 | private fn: (...args: TArgs) => void, 12 | private delay = 0, 13 | option: { mode?: string; debug?: boolean } = {}, 14 | ) { 15 | if (option.mode) this.mode = option.mode 16 | if (option.debug) this.debug = option.debug 17 | if (typeof process !== 'object' && typeof window.app.logger === 'object') { 18 | this.logger = window.app.logger 19 | } else { 20 | this.logger = console 21 | } 22 | } 23 | 24 | public start() { 25 | switch (this.mode) { 26 | case 'timeout': { 27 | this.log('timeout start') 28 | this.timer = setTimeout(() => { 29 | this.log('timeout run') 30 | this.fn() 31 | this.clear() 32 | }, this.delay) 33 | break 34 | } 35 | case 'interval': { 36 | this.log('interval start') 37 | this.timer = setInterval(() => { 38 | this.log('interval run') 39 | this.fn() 40 | }, this.delay) 41 | break 42 | } 43 | case 'immediate': { 44 | this.log('immediate start') 45 | this.timer = setImmediate(() => { 46 | this.log('immediate run') 47 | this.fn() 48 | }) 49 | break 50 | } 51 | default: 52 | break 53 | } 54 | } 55 | 56 | public clear() { 57 | if (this.timer) { 58 | switch (this.mode) { 59 | case 'timeout': { 60 | this.log('timeout clear') 61 | clearTimeout(this.timer) 62 | break 63 | } 64 | case 'interval': { 65 | this.log('interval clear') 66 | clearInterval(this.timer) 67 | break 68 | } 69 | case 'immediate': { 70 | this.log('immediate clear') 71 | clearImmediate(this.timer) 72 | break 73 | } 74 | default: 75 | break 76 | } 77 | this.timer = null 78 | } 79 | } 80 | 81 | public restart() { 82 | this.clear() 83 | this.start() 84 | } 85 | 86 | private log(...args: any) { 87 | if (this.debug) { 88 | this.logger.info(args) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/common/uri.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * isURI 3 | * @param {string} uriOrPath 4 | * @returns {boolean} 是否为uri 5 | */ 6 | export const isURI = (uriOrPath: string): boolean => { 7 | try { 8 | return !!new URL(uriOrPath) 9 | } catch (e) { 10 | return false 11 | } 12 | } 13 | 14 | /** 15 | * prefixHttp 16 | * @param {string} url 17 | * @returns string 18 | */ 19 | export const prefixHttp = (url: string): string => { 20 | url = url.trim() 21 | return url.startsWith('http') ? url : `http://${url}` 22 | } 23 | -------------------------------------------------------------------------------- /packages/inject/index.ts: -------------------------------------------------------------------------------- 1 | import { addStyle, whenDom } from '@/utils' 2 | import { ipcRenderer } from 'electron' 3 | import { withPrototype } from '../common' 4 | 5 | declare global { 6 | interface Window { 7 | utils: Record 8 | } 9 | } 10 | 11 | // 提供给插件 preload 的公共函数 12 | window.utils = { addStyle, whenDom, ipcRenderer: withPrototype(ipcRenderer) } 13 | -------------------------------------------------------------------------------- /packages/inject/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "useDefineForClassFields": true, 5 | "lib": ["esnext", "dom"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["./*"] 9 | } 10 | }, 11 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/inject/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 向文档添加样式 3 | * @param text - 样式内容 4 | * @returns entry method 5 | */ 6 | export const addStyle = (text: string) => { 7 | const style = document.createElement('style') 8 | style.textContent = text 9 | const styleElement = document.head.insertAdjacentElement('beforeend', style) 10 | return { 11 | unload: () => { 12 | styleElement.remove() 13 | }, 14 | } 15 | } 16 | 17 | /** 18 | * 判断dom是否存在 19 | * @param dom 需要判断元素 20 | * @param target dom的父元素,如果传入参数则会使用observer观察dom出现 21 | * @param options observer的options 22 | * @returns Promise 23 | */ 24 | export const whenDom = (dom: string[], target?: string, options: MutationObserverInit = {}) => { 25 | let $_reject: any 26 | let observer: MutationObserver 27 | const promise = new Promise((resolve, reject) => { 28 | $_reject = reject 29 | if (document.querySelector(dom.join(','))) { 30 | return resolve() 31 | } 32 | if (!target) { 33 | return reject() 34 | } 35 | observer = new MutationObserver((mutations) => { 36 | mutations.forEach(({ addedNodes }) => { 37 | if (addedNodes.length === 0) return 38 | const node = addedNodes[0] 39 | const reg = new RegExp(`(${dom.join('|')})`) 40 | if (reg.test(`.${node.className}`)) { 41 | resolve() 42 | } 43 | }) 44 | }) 45 | // 合并默认配置 46 | const defaultOtions = { childList: true, subtree: true } 47 | options = Object.assign(defaultOtions, options) 48 | const element = document.querySelector(target) 49 | // 开始观察 50 | if (element) { 51 | observer.observe(element, options) 52 | } else { 53 | reject() 54 | } 55 | }) 56 | return { 57 | promise, 58 | abort: () => { 59 | observer && observer.disconnect() 60 | $_reject() 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/inject/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module' 2 | import { defineConfig } from 'vite' 3 | import { resolve } from 'path' 4 | import pkg from '../../package.json' 5 | 6 | export default defineConfig({ 7 | root: __dirname, 8 | base: './', 9 | resolve: { 10 | alias: { 11 | '@': resolve(__dirname, './'), 12 | }, 13 | }, 14 | build: { 15 | outDir: '../../dist/inject', 16 | lib: { 17 | entry: 'index.ts', 18 | formats: ['cjs'], 19 | fileName: () => '[name].cjs', 20 | }, 21 | emptyOutDir: true, 22 | rollupOptions: { 23 | external: ['electron', ...builtinModules, ...Object.keys(pkg.dependencies || {})], 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/interfaces/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | -------------------------------------------------------------------------------- /packages/interfaces/cookies.d.ts: -------------------------------------------------------------------------------- 1 | interface CookiesApi { 2 | get: (filter: Electron.CookiesGetFilter) => Promise 3 | } 4 | 5 | export { CookiesApi } 6 | -------------------------------------------------------------------------------- /packages/interfaces/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './plugin' 2 | export * from './tabs' 3 | export * from './view' 4 | export * from './net' 5 | export * from './window' 6 | export * from './storage' 7 | export * from './cookies' 8 | -------------------------------------------------------------------------------- /packages/interfaces/net.d.ts: -------------------------------------------------------------------------------- 1 | interface FetchOptions { 2 | method: string 3 | body: string | null 4 | headers: { [key: string]: string } | null 5 | } 6 | 7 | interface NetReturn { 8 | ok: boolean 9 | status: number 10 | statusText: string 11 | headers: Record 12 | text: () => Promise 13 | json: () => Promise 14 | } 15 | 16 | interface NetApi { 17 | fetch: (url: string, options?: Partial) => Promise> 18 | } 19 | 20 | export { NetApi, FetchOptions, NetReturn } 21 | -------------------------------------------------------------------------------- /packages/interfaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmini-types", 3 | "version": "1.0.7", 4 | "description": "webmini plugin types", 5 | "main": "", 6 | "types": "index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/liumingye/webmini.git" 10 | }, 11 | "keywords": [ 12 | "webmini" 13 | ], 14 | "author": "liumingye", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/liumingye/webmini/issues" 18 | }, 19 | "homepage": "https://github.com/liumingye/webmini#readme", 20 | "dependencies": { 21 | "@types/pouchdb": "^6.4.0", 22 | "axios": "^0.26.1", 23 | "electron": "^18.0.1", 24 | "vue": "^3.2.31" 25 | }, 26 | "devDependencies": { 27 | "typescript": "^4.4.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/interfaces/plugin.ts: -------------------------------------------------------------------------------- 1 | import type { WebContents } from 'electron' 2 | import type axios from 'axios' 3 | import type { ShallowRef } from 'vue' 4 | import type { NetApi } from './net' 5 | import type { CommonWindowApi } from './window' 6 | import type { StorageServiceApi } from './storage' 7 | import type { CookiesApi } from './cookies' 8 | 9 | export type { WebContents } 10 | 11 | /** 插件初始化时的传入参数, 可以解构并调用 */ 12 | export interface PluginApiParameters { 13 | application: { 14 | mainWindow: CommonWindowApi 15 | selectPartWindow: CommonWindowApi 16 | } 17 | net: NetApi 18 | db: StorageServiceApi 19 | axios: typeof axios 20 | cookies: CookiesApi 21 | } 22 | 23 | export type ThemeColor = { 24 | bg?: string 25 | text?: string 26 | } 27 | 28 | export type Theme = { 29 | light?: ThemeColor 30 | dark?: ThemeColor 31 | } 32 | 33 | export type UserAgent = { 34 | mobile?: (string | RegExp)[] 35 | desktop?: (string | RegExp)[] 36 | } 37 | 38 | export type WindowType = { 39 | mini?: (string | RegExp)[] 40 | } 41 | 42 | export type Search = { 43 | link: string 44 | placeholder?: string 45 | links?: { 46 | test: RegExp 47 | link: string 48 | }[] 49 | } 50 | 51 | export type Nav = Record< 52 | string, 53 | { 54 | name: string 55 | url: string 56 | }[] 57 | > 58 | 59 | export type Replace = { 60 | search: string 61 | replace: string 62 | }[] 63 | 64 | export type WebNav = { 65 | search?: Search 66 | nav?: Nav 67 | replace?: Replace 68 | } 69 | 70 | export type UrlChangeData = { 71 | url: URL 72 | } 73 | 74 | /** 插件基本信息 */ 75 | export interface PluginMinimalData { 76 | /** 插件名称 */ 77 | name?: string 78 | /** preloads */ 79 | preloads?: string[] 80 | 81 | /** 初始化函数, 可在其中注册数据, 添加代码注入等 */ 82 | load?: () => void | Promise 83 | /** 卸载函数 */ 84 | unload?: () => void | Promise 85 | 86 | /** 设置匹配的URL, 不匹配则不运行此组件 */ 87 | urlInclude?: (string | RegExp)[] 88 | /** 设置不匹配的URL, 不匹配则不运行此组件, 优先级高于`urlInclude` */ 89 | urlExclude?: (string | RegExp)[] 90 | 91 | // 数据 92 | themeColor?: Theme 93 | userAgent?: UserAgent 94 | windowType?: WindowType 95 | webNav?: WebNav 96 | 97 | // 事件 98 | onUrlChanged?(data: UrlChangeData, webContents: WebContents): void | Promise 99 | } 100 | 101 | type PartialRequired = Target & { 102 | [P in Props]-?: Target[P] 103 | } 104 | 105 | export type PluginMetadata = PartialRequired 106 | 107 | /** 108 | * 插件管理器配置 109 | * @param baseDir 插件安装目录 110 | * @export 111 | * @interface AdapterHandlerOptions 112 | */ 113 | export interface AdapterHandlerOptions { 114 | baseDir: string 115 | } 116 | 117 | /** 本地插件信息 */ 118 | export interface LocalPluginInfo { 119 | /** 插件名称 */ 120 | readonly name: string 121 | /** 可读插件名称 */ 122 | readonly displayName: string 123 | /** 开始页 */ 124 | readonly start: string 125 | /** 图标 */ 126 | icon?: string | ShallowRef 127 | /** 版本 */ 128 | readonly version: string 129 | /** 状态 */ 130 | status?: PluginStatus 131 | } 132 | 133 | /** 134 | * 在线插件信息 135 | * https://gitee.com/liumingye/webmini-database/blob/master/plugins.json 136 | * @export 137 | * @interface AdapterInfo 138 | */ 139 | export interface AdapterInfo { 140 | /** 插件名称 */ 141 | readonly name: string 142 | /** 可读插件名称 */ 143 | readonly pluginName: string 144 | /** 描述 */ 145 | readonly description: string 146 | /** 作者 */ 147 | readonly author: string 148 | /** 版本 */ 149 | readonly version: string 150 | /** 本地插件信息 */ 151 | local?: LocalPluginInfo 152 | } 153 | 154 | export enum PluginStatus { 155 | /** 安装中 */ 156 | INSTALLING = 'INSTALLING', 157 | /** 安装完成 */ 158 | INSTALLING_COMPLETE = 'INSTALLING_COMPLETE', 159 | /** 安装失败 */ 160 | INSTALL_FAIL = 'INSTALL_FAIL', 161 | /** 卸载中 */ 162 | UNINSTALLING = 'UNINSTALLING', 163 | /** 卸载失败 */ 164 | UNINSTALL_FAIL = 'UNINSTALL_FAIL', 165 | /** 卸载完成 */ 166 | UNINSTALL_COMPLETE = 'UNINSTALL_COMPLETE', 167 | /** 卸载完成 */ 168 | UPGRADE = 'UPGRADE', 169 | } 170 | -------------------------------------------------------------------------------- /packages/interfaces/storage.d.ts: -------------------------------------------------------------------------------- 1 | interface StorageServiceApi { 2 | /** 3 | * 查询 4 | * @param id 5 | * @param key 6 | * @returns 7 | */ 8 | get: ( 9 | id: string, 10 | key?: string, 11 | ) => Promise<(Model & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta) | null> 12 | 13 | /** 14 | * 更改 15 | * @param doc 16 | * @param key 17 | * @returns 18 | */ 19 | put: ( 20 | doc: PouchDB.Core.PutDocument, 21 | key?: string, 22 | ) => Promise 23 | 24 | /** 25 | * 删除 26 | * @param doc 27 | * @param key 28 | * @returns 29 | */ 30 | remove: ( 31 | doc: PouchDB.Core.RemoveDocument, 32 | key?: string, 33 | ) => Promise 34 | } 35 | 36 | export { StorageServiceApi } 37 | -------------------------------------------------------------------------------- /packages/interfaces/tabs.d.ts: -------------------------------------------------------------------------------- 1 | import type { LoadURLOptions } from 'electron' 2 | import type { LocalPluginInfo } from './plugin' 3 | 4 | type EventName = `${T}-updated` 5 | 6 | export type TabEvent = EventName<'url' | 'title'> | 'loading' 7 | 8 | export interface CreateProperties { 9 | plugin: LocalPluginInfo | undefined 10 | url: string 11 | active?: boolean 12 | options?: LoadURLOptions 13 | index?: number 14 | } 15 | -------------------------------------------------------------------------------- /packages/interfaces/view.ts: -------------------------------------------------------------------------------- 1 | export enum WindowTypeEnum { 2 | MOBILE = 'mobile', 3 | DESKTOP = 'desktop', 4 | MINI = 'mini', 5 | FEED = 'feed', 6 | LOGIN = 'login', 7 | } 8 | 9 | export type WindowType = Record 10 | 11 | export const WindowTypeDefault: WindowType = { 12 | [WindowTypeEnum.MOBILE]: [376, 500], 13 | [WindowTypeEnum.DESKTOP]: [1100, 600], 14 | [WindowTypeEnum.MINI]: [300, 170], 15 | [WindowTypeEnum.FEED]: [650, 760], 16 | [WindowTypeEnum.LOGIN]: [490, 394], 17 | } 18 | -------------------------------------------------------------------------------- /packages/interfaces/window.d.ts: -------------------------------------------------------------------------------- 1 | import type { WebContents, Session } from 'electron' 2 | 3 | export interface CommonWindowApi { 4 | id: number 5 | show(): void 6 | hide(): void 7 | toggle(): void 8 | isDestroyed(): boolean 9 | webContents: WebContents 10 | session: Session 11 | send(channel: string, ...args: any[]): void 12 | } 13 | -------------------------------------------------------------------------------- /packages/main/application.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, session, dialog } from 'electron' 2 | import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer' 3 | import is from 'electron-is' 4 | import ipcMainInit from './ipcMain' 5 | import { getMainMenu } from './menus/main' 6 | import { AdblockerService } from './services/adblocker' 7 | import autoUpdaterService from './services/autoUpdater' 8 | import { MainWindow } from './windows/main' 9 | import { SelectPartWindow } from './windows/selectPart' 10 | import { registerProtocol } from './models/protocol' 11 | 12 | export class Application { 13 | public static INSTANCE = new this() 14 | 15 | public mainWindow: MainWindow | undefined 16 | 17 | public selectPartWindow: SelectPartWindow | undefined 18 | 19 | public start(): void { 20 | app.on('window-all-closed', this.onWindowAllClosed.bind(this)) 21 | app.on('second-instance', this.onSecondInstance.bind(this)) 22 | app.on('activate', this.onActivate.bind(this)) 23 | app.on('ready', this.onReady.bind(this)) 24 | } 25 | 26 | private onActivate() { 27 | // On macOS it's common to re-create a window in the app when the 28 | // dock icon is clicked and there are no other windows open. 29 | this.createAllWindow() 30 | } 31 | 32 | private onSecondInstance() { 33 | // Focus on the main window if the user tried to open another 34 | if (!this.mainWindow) return 35 | const win = this.mainWindow.win 36 | if (win) { 37 | if (win.isDestroyed()) { 38 | return this.createAllWindow() 39 | } 40 | if (win.isMinimized()) { 41 | win.restore() 42 | } 43 | win.focus() 44 | } 45 | } 46 | 47 | private onWindowAllClosed() { 48 | // On macOS it is common for applications and their menu bar 49 | // to stay active until the user quits explicitly with Cmd + Q 50 | if (!is.macOS()) app.quit() 51 | } 52 | 53 | private onReady() { 54 | // todo 设置添加主题 55 | // nativeTheme.themeSource = 'system' 56 | 57 | this.createAllWindow() 58 | 59 | Menu.setApplicationMenu(getMainMenu()) 60 | 61 | ipcMainInit() 62 | 63 | // service 64 | new AdblockerService(session.defaultSession).enable() 65 | autoUpdaterService() 66 | 67 | // protocol 68 | registerProtocol(session.defaultSession) 69 | 70 | // vue-devtools 71 | if (is.dev()) { 72 | installExtension(VUEJS3_DEVTOOLS.id, { 73 | loadExtensionOptions: { allowFileAccess: true }, 74 | }) 75 | .then((name) => console.log(`Added Extension: ${name}`)) 76 | .catch((err) => console.log('An error occurred: ', err)) 77 | } 78 | } 79 | 80 | private getAllWindowID() { 81 | const winIDs: Record = {} 82 | if (this.mainWindow) winIDs.mainWindow = this.mainWindow.id 83 | if (this.selectPartWindow) winIDs.selectPartWindow = this.selectPartWindow.id 84 | return winIDs 85 | } 86 | 87 | private sendWindowID() { 88 | const windowID = this.getAllWindowID() 89 | this.mainWindow?.send('windowID', windowID) 90 | this.selectPartWindow?.send('windowID', windowID) 91 | } 92 | 93 | private createAllWindow() { 94 | this.mainWindow = this.createMainWindow() 95 | this.selectPartWindow = this.createSelectPartWindow() 96 | } 97 | 98 | // 初始化主窗口 99 | private createMainWindow() { 100 | if (!this.mainWindow || this.mainWindow.isDestroyed()) { 101 | const mainWindow = new MainWindow() 102 | mainWindow.webContents.on('dom-ready', this.sendWindowID.bind(this)) 103 | return mainWindow 104 | } 105 | return this.mainWindow 106 | } 107 | 108 | // 初始化选分p窗口 109 | private createSelectPartWindow() { 110 | if (!this.selectPartWindow || this.selectPartWindow.isDestroyed()) { 111 | const selectPartWindow = new SelectPartWindow() 112 | selectPartWindow.webContents.on('dom-ready', this.sendWindowID.bind(this)) 113 | return selectPartWindow 114 | } 115 | return this.selectPartWindow 116 | } 117 | 118 | public relaunchApp(): void { 119 | const relaunchOptions = { 120 | execPath: process.execPath, 121 | args: process.argv, 122 | } 123 | /** 124 | * Fix for AppImage on Linux. 125 | */ 126 | if (process.env.APPIMAGE) { 127 | relaunchOptions.execPath = process.env.APPIMAGE 128 | relaunchOptions.args.unshift('--appimage-extract-and-run') 129 | } 130 | app.relaunch(relaunchOptions) 131 | app.exit() 132 | } 133 | 134 | public getFocusedWindow(): BrowserWindow | undefined { 135 | const win = BrowserWindow.getFocusedWindow() 136 | if (win) return win 137 | return undefined 138 | } 139 | 140 | public async clearAllUserData(): Promise { 141 | const focusedWindow = this.getFocusedWindow() 142 | if (!focusedWindow) return 143 | const answer = dialog.showMessageBoxSync(focusedWindow, { 144 | type: 'question', 145 | title: '确认', 146 | message: `确认重置应用?`, 147 | detail: `你的浏览器数据将被清空,包括缓存、本地存储、登录状态`, 148 | buttons: ['确认', '取消'], 149 | defaultId: 1, 150 | }) 151 | 152 | if (answer === 1) { 153 | return 154 | } 155 | 156 | const { webContents } = focusedWindow 157 | webContents.session.flushStorageData() 158 | 159 | await Promise.all([ 160 | webContents.session.clearCache(), 161 | webContents.session.clearStorageData(), 162 | // ... 163 | ]) 164 | 165 | this.relaunchApp() 166 | } 167 | 168 | /** 169 | * Removes directories containing leveldb databases. 170 | * Each directory is reinitialized after re-launching the application. 171 | */ 172 | public async clearSensitiveDirectories(restart = false): Promise { 173 | const focusedWindow = this.getFocusedWindow() 174 | if (!focusedWindow) return 175 | const { webContents } = focusedWindow 176 | webContents.session.flushStorageData() 177 | 178 | await Promise.all([ 179 | webContents.session.clearCache(), 180 | webContents.session.clearStorageData({ 181 | storages: [ 182 | 'appcache', 183 | 'cachestorage', 184 | 'serviceworkers', 185 | 'shadercache', 186 | 'indexdb', 187 | 'websql', 188 | ], 189 | }), 190 | ]) 191 | 192 | if (restart) { 193 | this.relaunchApp() 194 | return 195 | } 196 | 197 | dialog.showMessageBoxSync(focusedWindow, { 198 | type: 'info', 199 | title: `提示`, 200 | message: `清理缓存完成`, 201 | buttons: ['好的'], 202 | }) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /packages/main/core/db/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import PouchDB from 'pouchdb' 4 | import { DBError, Model } from './types' 5 | 6 | export default class { 7 | readonly docMaxByteLength 8 | readonly docAttachmentMaxByteLength 9 | public dbpath 10 | public defaultDbName 11 | public pouchDB: PouchDB.Database 12 | 13 | constructor(dbPath: string) { 14 | this.docMaxByteLength = 2 * 1024 * 1024 // 2M 15 | this.docAttachmentMaxByteLength = 20 * 1024 * 1024 // 20M 16 | this.dbpath = dbPath 17 | this.defaultDbName = path.join(dbPath, 'config') 18 | fs.existsSync(this.dbpath) || fs.mkdirSync(this.dbpath) 19 | this.pouchDB = new PouchDB(this.defaultDbName, { auto_compaction: true }) 20 | } 21 | 22 | getDocId(name: string, id: string): string { 23 | return name + '/' + id 24 | } 25 | 26 | replaceDocId(name: string, id: string): string { 27 | return id.replace(name + '/', '') 28 | } 29 | 30 | errorInfo(name: string, message: string): DBError { 31 | return { error: true, name, message } 32 | } 33 | 34 | private checkDocSize(doc: Model) { 35 | if (Buffer.byteLength(JSON.stringify(doc)) > this.docMaxByteLength) { 36 | return this.errorInfo('exception', `doc max size ${this.docMaxByteLength / 1024 / 1024} M`) 37 | } 38 | return false 39 | } 40 | 41 | async put( 42 | name: string, 43 | doc: PouchDB.Core.PutDocument, 44 | strict = true, 45 | ): Promise { 46 | if (strict) { 47 | const err = this.checkDocSize(doc) 48 | if (err) return err 49 | } 50 | doc._id = this.getDocId(name, doc._id) 51 | try { 52 | const result = await this.pouchDB.put(doc) 53 | doc._id = result.id = this.replaceDocId(name, result.id) 54 | return result 55 | } catch (e: any) { 56 | doc._id = this.replaceDocId(name, doc._id) 57 | return { id: doc._id, name: e.name, error: !0, message: e.message } 58 | } 59 | } 60 | 61 | async get( 62 | name: string, 63 | id: string, 64 | ): Promise<(Model & PouchDB.Core.IdMeta & PouchDB.Core.GetMeta) | null> { 65 | try { 66 | const result = await this.pouchDB.get(this.getDocId(name, id)) 67 | result._id = this.replaceDocId(name, result._id) 68 | return result 69 | } catch (e) { 70 | return null 71 | } 72 | } 73 | 74 | async remove( 75 | name: string, 76 | doc: PouchDB.Core.RemoveDocument, 77 | ): Promise { 78 | try { 79 | let target 80 | if ('object' == typeof doc) { 81 | target = doc 82 | if (!target._id || 'string' !== typeof target._id) { 83 | return this.errorInfo('exception', 'doc _id error') 84 | } 85 | target._id = this.getDocId(name, target._id) 86 | } else { 87 | if ('string' !== typeof doc) { 88 | return this.errorInfo('exception', 'param error') 89 | } 90 | target = await this.pouchDB.get(this.getDocId(name, doc)) 91 | } 92 | const result = await this.pouchDB.remove(target) 93 | target._id = result.id = this.replaceDocId(name, result.id) 94 | return result 95 | } catch (e: any) { 96 | if ('object' === typeof doc) { 97 | doc._id = this.replaceDocId(name, doc._id) 98 | } 99 | return this.errorInfo(e.name, e.message) 100 | } 101 | } 102 | 103 | // async bulkDocs( 104 | // name: string, 105 | // docs: PouchDB.Core.PutDocument[], 106 | // ): Promise { 107 | // let result 108 | // try { 109 | // if (!Array.isArray(docs)) return this.errorInfo('exception', 'not array') 110 | // if (docs.find((e) => !e._id)) return this.errorInfo('exception', 'doc not _id field') 111 | // if (new Set(docs.map((e) => e._id)).size !== docs.length) 112 | // return this.errorInfo('exception', '_id value exists as') 113 | // for (const doc of docs) { 114 | // const err = this.checkDocSize(doc) 115 | // if (err) return err 116 | // doc._id = this.getDocId(name, doc._id) 117 | // } 118 | // result = await this.pouchDB.bulkDocs(docs) 119 | // result = result.map((res: any) => { 120 | // res.id = this.replaceDocId(name, res.id) 121 | // return res.error 122 | // ? { 123 | // id: res.id, 124 | // name: res.name, 125 | // error: true, 126 | // message: res.message, 127 | // } 128 | // : res 129 | // }) 130 | // docs.forEach((doc) => { 131 | // doc._id = this.replaceDocId(name, doc._id) 132 | // }) 133 | // } catch (e) { 134 | // // 135 | // } 136 | // return result as unknown as Promise 137 | // } 138 | 139 | // async allDocs(name: string, key: string | Array): Promise { 140 | // const config: any = { include_docs: true } 141 | // if (key) { 142 | // if ('string' == typeof key) { 143 | // config.startkey = this.getDocId(name, key) 144 | // config.endkey = config.startkey + '￰' 145 | // } else { 146 | // if (!Array.isArray(key)) 147 | // return this.errorInfo('exception', 'param only key(string) or keys(Array[string])') 148 | // config.keys = key.map((key) => this.getDocId(name, key)) 149 | // } 150 | // } else { 151 | // config.startkey = this.getDocId(name, '') 152 | // config.endkey = config.startkey + '￰' 153 | // } 154 | // const result: Array = [] 155 | // try { 156 | // ;(await this.pouchDB.allDocs(config)).rows.forEach((res: any) => { 157 | // if (!res.error && res.doc) { 158 | // res.doc._id = this.replaceDocId(name, res.doc._id) 159 | // result.push(res.doc) 160 | // } 161 | // }) 162 | // } catch (e) { 163 | // // 164 | // } 165 | // return result 166 | // } 167 | } 168 | -------------------------------------------------------------------------------- /packages/main/core/db/types.ts: -------------------------------------------------------------------------------- 1 | type RevisionId = string 2 | 3 | export interface Model { 4 | _id: string 5 | data: any 6 | _rev?: string 7 | } 8 | 9 | export interface DBError { 10 | /** 11 | * HTTP Status Code during HTTP or HTTP-like operations 12 | */ 13 | status?: number | undefined 14 | name?: string | undefined 15 | message?: string | undefined 16 | reason?: string | undefined 17 | error?: string | boolean | undefined 18 | id?: string | undefined 19 | rev?: RevisionId | undefined 20 | } 21 | -------------------------------------------------------------------------------- /packages/main/core/plugin/handler.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import Logger from '~/common/logger' 4 | import type { AdapterHandlerOptions } from '~/interfaces/plugin' 5 | import axios from 'axios' 6 | import tar from 'tar' 7 | 8 | /** 9 | * 系统插件管理器 10 | * @class AdapterHandler 11 | */ 12 | export class AdapterHandler { 13 | // 插件安装地址 14 | public baseDir: string 15 | 16 | /** 17 | * Creates an instance of AdapterHandler. 18 | * @param {AdapterHandlerOptions} options 19 | * @memberof AdapterHandler 20 | */ 21 | constructor(options: AdapterHandlerOptions) { 22 | // 初始化插件存放 23 | const nodeModulesPath = path.resolve(options.baseDir, 'node_modules') 24 | 25 | if (!fs.existsSync(nodeModulesPath)) { 26 | fs.mkdirsSync(nodeModulesPath) 27 | fs.writeFileSync(path.resolve(options.baseDir, 'README.md'), '该目录为webmini插件存放目录') 28 | } 29 | this.baseDir = options.baseDir 30 | } 31 | 32 | /** 33 | * 检查链接地址是否可用 34 | * @param url url地址 35 | * @returns Promise 36 | */ 37 | private async isUrlValid(url: string): Promise { 38 | return await axios.head(url).then( 39 | (response) => { 40 | return response.status === 200 41 | }, 42 | () => { 43 | return false 44 | }, 45 | ) 46 | } 47 | 48 | /** 49 | * 下载Tar包 50 | * @param name 包の名称 51 | * @param version 包の版本 52 | * @param dist 保存路径 53 | * @returns Promise 54 | */ 55 | private async downloadTarball(name: string, version: string, dist: string): Promise { 56 | const registrys = [ 57 | // 淘宝源 58 | `https://registry.npmmirror.com`, 59 | // 腾讯源 60 | `https://mirrors.cloud.tencent.com/npm`, 61 | // 中国科学技术大学源 62 | `https://npmreg.proxy.ustclug.org`, 63 | // npm源 64 | `https://registry.npmjs.org`, 65 | ] 66 | 67 | // 查询可用的源 68 | let usebalUrl = '' 69 | for (const registry of registrys) { 70 | const tarUrl = `${registry}/${name}/-/${name}-${version}.tgz` 71 | if (await this.isUrlValid(tarUrl)) { 72 | usebalUrl = tarUrl 73 | break 74 | } 75 | } 76 | 77 | // 无可用源 78 | // console.log(usebalUrl) 79 | if (usebalUrl === '') { 80 | return Promise.reject('无可用源') 81 | } 82 | 83 | // 下载 84 | const res = await axios 85 | .get(usebalUrl, { 86 | responseType: 'stream', 87 | }) 88 | .catch(() => { 89 | // 下载失败 90 | return Promise.reject('下载失败') 91 | }) 92 | 93 | // 保存 94 | const file = fs.createWriteStream(dist) 95 | 96 | // 写入 97 | res.data.pipe(file) 98 | 99 | return new Promise((resolve, reject) => { 100 | // 监听写入完成 101 | file.on('finish', () => { 102 | file.close() 103 | resolve(true) 104 | }) 105 | // 写入错误 106 | file.on('error', (err) => { 107 | reject(err) 108 | fs.unlink(dist) 109 | }) 110 | }) 111 | } 112 | 113 | /** 114 | * 解压tar包 115 | * @param file 包路径 116 | * @param name 包名称 117 | * @returns Promise 118 | */ 119 | private async unpack(file: string, name: string): Promise { 120 | const nodeModulesPath = path.resolve(this.baseDir, 'node_modules') 121 | return new Promise((resolve, reject) => { 122 | tar 123 | .x({ 124 | file, 125 | cwd: nodeModulesPath, 126 | }) 127 | .then(() => { 128 | fs.rename( 129 | path.resolve(nodeModulesPath, 'package'), 130 | path.resolve(nodeModulesPath, name), 131 | (err) => { 132 | if (err) { 133 | reject(err) 134 | } 135 | }, 136 | ) 137 | fs.remove(file) 138 | resolve(true) 139 | }) 140 | .catch((err) => { 141 | reject(err) 142 | }) 143 | }) 144 | } 145 | 146 | public async install(name: string, version: string) { 147 | const dist = path.resolve(this.baseDir, 'node_modules', name) 148 | if (await fs.pathExists(dist)) { 149 | Logger.info(`${name}@${version} 已安装`) 150 | this.uninstall(name) 151 | // return 152 | } 153 | 154 | const tarball = path.resolve(this.baseDir, 'node_modules', `${name}-${version}.tgz`) 155 | if (await fs.pathExists(tarball)) { 156 | Logger.info(`${name}@${version} 已下载`) 157 | await this.unpack(tarball, name) 158 | return 159 | } 160 | 161 | await this.downloadTarball(name, version, tarball) 162 | 163 | await this.unpack(tarball, name) 164 | } 165 | 166 | public async uninstall(name: string) { 167 | const dist = path.resolve(this.baseDir, 'node_modules', name) 168 | if (!(await fs.pathExists(dist))) { 169 | Logger.info(`${name} 未安装`) 170 | return 171 | } 172 | return fs.remove(dist) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /packages/main/core/plugin/index.ts: -------------------------------------------------------------------------------- 1 | export { AdapterHandler } from './handler' 2 | export { Plugin } from './plugin' 3 | export { TabPlugin } from './tab' 4 | -------------------------------------------------------------------------------- /packages/main/core/plugin/plugin.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { app } from 'electron' 3 | import fs from 'fs' 4 | import { isString } from 'lodash' 5 | import { createRequire } from 'module' 6 | import { join, resolve } from 'path' 7 | import Cookies from '~/common/cookies' 8 | import Logger from '~/common/logger' 9 | import Net from '~/common/net' 10 | import { isURI } from '~/common/uri' 11 | import type { AdapterInfo, LocalPluginInfo, PluginMetadata } from '~/interfaces/plugin' 12 | import { PluginStatus } from '~/interfaces/plugin' 13 | import { Application } from '../../application' 14 | import { StorageService } from '../../services/storage' 15 | import { AdapterHandler } from './handler' 16 | 17 | const requireFresh = createRequire(import.meta.url) 18 | const baseDir = join(app.getPath('userData'), './plugins') 19 | 20 | export class Plugin { 21 | // 插件实例 22 | public static INSTANCE = new this() 23 | 24 | private _allPlugins: PluginMetadata[] 25 | public get allPlugins(): PluginMetadata[] { 26 | return this._allPlugins 27 | } 28 | private set allPlugins(value: PluginMetadata[]) { 29 | // 更新静态数据 30 | this._allPlugins = Plugin.INSTANCE.allPlugins = value 31 | } 32 | 33 | private readonly handler: AdapterHandler 34 | 35 | public constructor() { 36 | this._allPlugins = [] 37 | 38 | this.handler = new AdapterHandler({ 39 | baseDir, 40 | }) 41 | 42 | // 加载本地插件 43 | this.getLocalPlugins().then((localPlugins) => { 44 | localPlugins.forEach((plugin) => { 45 | this.addPlugin(plugin.name) 46 | }) 47 | }) 48 | } 49 | 50 | /** 51 | * 运行插件加载方法 52 | * @param plugin 53 | */ 54 | public async loadPlugin(plugin: PluginMetadata) { 55 | if (typeof plugin.load === 'function') { 56 | await plugin.load() 57 | } 58 | } 59 | 60 | /** 61 | * 运行插件释放方法 62 | * @param plugin 63 | */ 64 | public async unloadPlugin(plugin: PluginMetadata) { 65 | if (typeof plugin.unload === 'function') { 66 | await plugin.unload() 67 | } 68 | } 69 | 70 | /** 71 | * 添加插件 72 | * @param name 73 | * @returns 74 | */ 75 | public async addPlugin(name: string) { 76 | try { 77 | const pluginPath = this.getPluginPath(name) 78 | 79 | const pluginPkg = JSON.parse(fs.readFileSync(join(pluginPath, 'package.json'), 'utf8')) 80 | 81 | const pluginImport = requireFresh(resolve(pluginPath, pluginPkg.main)) 82 | 83 | const pluginApi = { 84 | net: new Net(), 85 | application: { 86 | mainWindow: { 87 | send: Application.INSTANCE.mainWindow?.send.bind(Application.INSTANCE.mainWindow), 88 | }, 89 | selectPartWindow: { 90 | send: Application.INSTANCE.selectPartWindow?.send.bind( 91 | Application.INSTANCE.selectPartWindow, 92 | ), 93 | }, 94 | }, 95 | db: new StorageService(pluginPkg.name), 96 | axios, 97 | cookies: new Cookies(), 98 | } 99 | 100 | const plugin = new pluginImport.extension(pluginApi) 101 | 102 | plugin.name = pluginPkg.name 103 | 104 | this.allPlugins.push(plugin) 105 | 106 | return plugin 107 | } catch (error) { 108 | Logger.error(error) 109 | return false 110 | } 111 | } 112 | 113 | /** 114 | * 删除插件 115 | * @param name 116 | * @returns 117 | */ 118 | public async deletePlugin(name: string) { 119 | const index = this.allPlugins.findIndex((p) => name === p.name) 120 | if (index >= 0) { 121 | await this.unloadPlugin(this.allPlugins[index]) 122 | this.allPlugins.splice(index, 1) 123 | } 124 | return this.allPlugins 125 | } 126 | 127 | /** 128 | * 获取本地插件 129 | * @returns 130 | */ 131 | public async getLocalPlugins(): Promise { 132 | const pluginDb = await StorageService.INSTANCE.get('pluginDb') 133 | if (!pluginDb) return [] 134 | const res = Object.entries(pluginDb.data).reduce((previousValue, currentValue) => { 135 | try { 136 | const [name, status] = currentValue 137 | if (status === PluginStatus.INSTALLING_COMPLETE) { 138 | const pluginPath = this.getPluginPath(name) 139 | const pluginInfo: LocalPluginInfo = JSON.parse( 140 | fs.readFileSync(join(pluginPath, 'package.json'), 'utf8'), 141 | ) 142 | if (pluginInfo.icon && isString(pluginInfo.icon) && !isURI(pluginInfo.icon)) { 143 | pluginInfo.icon = join(pluginPath, pluginInfo.icon) 144 | } 145 | previousValue.push({ ...pluginInfo, status }) 146 | } 147 | } catch (error) { 148 | Logger.error(error) 149 | } 150 | return previousValue 151 | }, [] as LocalPluginInfo[]) 152 | return res 153 | } 154 | 155 | /** 156 | * 获取插件路径 157 | * @param name 158 | * @returns 159 | */ 160 | public getPluginPath(name: string) { 161 | return resolve(baseDir, 'node_modules', name) 162 | } 163 | 164 | /** 165 | * 更新插件状态 166 | * @param plugin 167 | * @param status 168 | */ 169 | private async updateStatus(plugin: AdapterInfo, status: PluginStatus) { 170 | Application.INSTANCE.mainWindow?.send('plugin-status-update', plugin, status) 171 | 172 | if (status === PluginStatus.UNINSTALL_FAIL) { 173 | status = PluginStatus.INSTALLING_COMPLETE 174 | } 175 | 176 | if ([PluginStatus.INSTALL_FAIL, PluginStatus.UNINSTALL_COMPLETE].includes(status)) { 177 | const pluginDb = await StorageService.INSTANCE.get('pluginDb') 178 | if (pluginDb) { 179 | delete pluginDb.data[plugin.name] 180 | await StorageService.INSTANCE.put({ 181 | _id: 'pluginDb', 182 | data: pluginDb.data, 183 | }) 184 | } 185 | } else { 186 | await StorageService.INSTANCE.put({ 187 | _id: 'pluginDb', 188 | data: { [plugin.name]: status }, 189 | }) 190 | } 191 | } 192 | 193 | /** 194 | * 安装插件 195 | * @param plugin 196 | * @returns 197 | */ 198 | public async install(plugin: AdapterInfo) { 199 | Logger.info(`${plugin.name}@${plugin.version} 开始安装`) 200 | await this.updateStatus(plugin, PluginStatus.INSTALLING) 201 | 202 | await this.handler 203 | .install(plugin.name, plugin.version) 204 | .then(() => { 205 | Logger.info(`${plugin.name}@${plugin.version} 安装成功`) 206 | this.addPlugin(plugin.name) 207 | this.updateStatus(plugin, PluginStatus.INSTALLING_COMPLETE) 208 | }) 209 | .catch(() => { 210 | Logger.info(`${plugin.name}@${plugin.version} 安装失败`) 211 | this.updateStatus(plugin, PluginStatus.INSTALL_FAIL) 212 | }) 213 | } 214 | 215 | /** 216 | * 卸载插件 217 | * @param plugin 218 | * @returns 219 | */ 220 | public async uninstall(plugin: AdapterInfo) { 221 | Logger.info(`开始卸载 - ${plugin.name}`) 222 | await this.updateStatus(plugin, PluginStatus.UNINSTALLING) 223 | 224 | await this.handler 225 | .uninstall(plugin.name) 226 | .then(() => { 227 | Logger.info(`卸载成功 - ${plugin.name}`) 228 | this.deletePlugin(plugin.name) 229 | this.updateStatus(plugin, PluginStatus.UNINSTALL_COMPLETE) 230 | }) 231 | .catch(() => { 232 | Logger.info(`卸载失败 - ${plugin.name}`) 233 | this.updateStatus(plugin, PluginStatus.UNINSTALL_FAIL) 234 | }) 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /packages/main/core/plugin/tab.ts: -------------------------------------------------------------------------------- 1 | import { app, WebContents } from 'electron' 2 | import { negate } from 'lodash' 3 | import type { PluginMetadata } from '~/interfaces/plugin' 4 | import { matchPattern } from '../../utils' 5 | import { Plugin } from './index' 6 | 7 | export class TabPlugin { 8 | public readonly enablePlugins: PluginMetadata[] = [] 9 | 10 | public readonly plugins: Plugin 11 | 12 | public constructor(public webContents: WebContents) { 13 | this.plugins = new Plugin() 14 | } 15 | 16 | /** 17 | * 载入单个插件 18 | * @param url 19 | * @returns 20 | */ 21 | public loadPlugin(url: string) { 22 | return (plugin: PluginMetadata) => { 23 | // 若指定了排除URL, 任意URL匹配就不加载 24 | if (plugin.urlExclude && plugin.urlExclude.some(matchPattern(url))) { 25 | return undefined 26 | } 27 | 28 | // 若指定了包含URL, 所有URL都不匹配时不加载 29 | if (plugin.urlInclude && plugin.urlInclude.every(negate(matchPattern(url)))) { 30 | return undefined 31 | } 32 | 33 | // 插入插件的preloads, 并且过滤已存在的preload 34 | if (plugin.preloads) { 35 | const preloads = this.webContents.session.getPreloads() 36 | plugin.preloads.forEach((preload) => { 37 | if (preloads.indexOf(preload) === -1) { 38 | preloads.push(preload) 39 | } 40 | }) 41 | this.webContents.session.setPreloads(preloads) 42 | } 43 | 44 | // 加载插件 45 | this.plugins.loadPlugin(plugin) 46 | 47 | this.enablePlugins.push(plugin) 48 | 49 | return plugin 50 | } 51 | } 52 | 53 | /** 54 | * 载入指定url的所有插件 55 | * @param url 56 | * @returns 57 | */ 58 | public loadTabPlugins(url: string) { 59 | // 优先将inject加载到webview中 60 | const preloads = this.webContents.session.getPreloads() 61 | const injectPath = `${app.getAppPath()}/dist/inject/index.cjs` 62 | if (preloads.indexOf(injectPath) === -1) { 63 | preloads.push(injectPath) 64 | } 65 | this.webContents.session.setPreloads(preloads) 66 | 67 | const res = this.plugins.allPlugins 68 | .map(this.loadPlugin(url)) 69 | .filter(Boolean) as typeof this.plugins.allPlugins 70 | 71 | return res 72 | } 73 | 74 | /** 75 | * 释放所有插件或指定插件 76 | * @param plugins 需要释放的插件 77 | */ 78 | public unloadTabPlugins(plugins?: PluginMetadata[]): void { 79 | let _plugins = [] 80 | 81 | if (plugins) { 82 | _plugins = plugins 83 | // 从enablePlugins中移除 84 | plugins.forEach((plugin) => { 85 | const index = this.enablePlugins.indexOf(plugin) 86 | if (index > -1) { 87 | this.enablePlugins.splice(index, 1) 88 | } 89 | }) 90 | } else { 91 | _plugins = this.enablePlugins 92 | // 清空enablePlugins 93 | this.enablePlugins.length = 0 94 | } 95 | 96 | // 释放插件 & 移除preloads 97 | for (const plugin of _plugins) { 98 | // 释放插件 99 | this.plugins.unloadPlugin(plugin) 100 | 101 | // 移除插件的preloads 102 | if (plugin.preloads) { 103 | const preloads = this.webContents.session.getPreloads() 104 | for (const preload of plugin.preloads) { 105 | const index = preloads.indexOf(preload) 106 | if (index === -1) continue 107 | preloads.splice(index, 1) 108 | } 109 | this.webContents.session.setPreloads(preloads) 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/main/index.ts: -------------------------------------------------------------------------------- 1 | import { initialize } from '@electron/remote/main' 2 | import { app, ipcMain, webContents } from 'electron' 3 | import is from 'electron-is' 4 | import { release } from 'os' 5 | import { build } from '../../package.json' 6 | import { Application } from './application' 7 | 8 | if (!app.requestSingleInstanceLock()) { 9 | app.quit() 10 | process.exit(0) 11 | } 12 | 13 | // Disable GPU Acceleration for Windows 7 14 | if (release().startsWith('6.1')) app.disableHardwareAcceleration() 15 | 16 | // Set application name for Windows 10+ notifications 17 | if (is.windows()) app.setAppUserModelId(build.appId) 18 | 19 | process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' 20 | 21 | /** 22 | * initialize the main-process side of the remote module 23 | */ 24 | initialize() 25 | 26 | // app.commandLine.appendSwitch('--enable-transparent-visuals') 27 | // 叠加滚动条 28 | app.commandLine.appendSwitch('--enable-features', 'OverlayScrollbar') 29 | 30 | ipcMain.setMaxListeners(0) 31 | 32 | // start app 33 | const application = Application.INSTANCE 34 | application.start() 35 | 36 | ipcMain.handle(`web-contents-call`, async (_e, { webContentsId, method, args = [] }) => { 37 | const wc = webContents.fromId(webContentsId) 38 | const result = (wc as any)[method](...args) 39 | 40 | if (result) { 41 | if (result instanceof Promise) { 42 | return await result 43 | } 44 | 45 | return result 46 | } 47 | }) 48 | -------------------------------------------------------------------------------- /packages/main/ipcMain.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron' 2 | import is from 'electron-is' 3 | import type { AdapterInfo, PluginMinimalData } from '~/interfaces/plugin' 4 | import { Application } from './application' 5 | import { Plugin } from './core/plugin' 6 | import { hookThemeColor } from './utils' 7 | import { StorageService } from './services/storage' 8 | 9 | export default () => { 10 | // UI 11 | ipcMain.on('close-main-window', () => { 12 | if (is.macOS()) { 13 | if (Application.INSTANCE.mainWindow) { 14 | Application.INSTANCE.mainWindow.viewManager.clearViewContainer() 15 | Application.INSTANCE.mainWindow.win.close() 16 | Application.INSTANCE.mainWindow = undefined 17 | } 18 | if (Application.INSTANCE.selectPartWindow) { 19 | Application.INSTANCE.selectPartWindow.win.close() 20 | Application.INSTANCE.selectPartWindow = undefined 21 | } 22 | } else { 23 | app.quit() 24 | } 25 | }) 26 | 27 | // 设置 28 | ipcMain.on('clear-sensitive-directories', () => { 29 | Application.INSTANCE.clearSensitiveDirectories() 30 | }) 31 | ipcMain.on('clear-all-user-data', () => { 32 | Application.INSTANCE.clearAllUserData() 33 | }) 34 | 35 | // plugin 插件 36 | ipcMain.handle('get-local-plugins', async () => { 37 | return await Plugin.INSTANCE.getLocalPlugins() 38 | }) 39 | ipcMain.handle('plugin-install', async (e, plugin: AdapterInfo) => { 40 | return await Plugin.INSTANCE.install(plugin) 41 | }) 42 | ipcMain.handle('plugin-uninstall', async (e, plugin: AdapterInfo) => { 43 | return await Plugin.INSTANCE.uninstall(plugin) 44 | }) 45 | ipcMain.handle('plugin-get-data', async (e, name: string, key: keyof PluginMinimalData) => { 46 | const plugin = Plugin.INSTANCE.allPlugins.find((plugin) => plugin.name === name) 47 | if (!plugin) return 48 | return plugin[key] 49 | }) 50 | 51 | // db 数据库 52 | ipcMain.handle('db-put', async (e, data, key?) => { 53 | return await StorageService.INSTANCE.put(data, key) 54 | }) 55 | ipcMain.handle('db-get', async (e, id, key?) => { 56 | return await StorageService.INSTANCE.get(id, key) 57 | }) 58 | 59 | // renderer ipc注册完成 60 | ipcMain.on('ipc-init-complete', () => { 61 | hookThemeColor() 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /packages/main/menus/main.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu, MenuItem, MenuItemConstructorOptions, shell } from 'electron' 2 | import is from 'electron-is' 3 | import { Application } from '../application' 4 | 5 | export const getMainMenu = () => { 6 | const application = Application.INSTANCE 7 | const template: Array = [ 8 | { 9 | label: app.name, 10 | submenu: [ 11 | { label: `关于 ${app.name}`, role: 'about' }, 12 | { type: 'separator' }, 13 | { label: '服务', role: 'services' }, 14 | { type: 'separator' }, 15 | { label: `隐藏 ${app.name}`, role: 'hide' }, 16 | { label: '隐藏其他', role: 'hideOthers' }, 17 | { label: '全部显示', role: 'unhide' }, 18 | { type: 'separator' }, 19 | { label: `退出 ${app.name}`, role: 'quit' }, 20 | ], 21 | }, 22 | { 23 | label: '编辑', 24 | submenu: [ 25 | { label: '撤销', role: 'undo' }, 26 | { label: '恢复', role: 'redo' }, 27 | { type: 'separator' }, 28 | { label: '剪切', role: 'cut' }, 29 | { label: '拷贝', role: 'copy' }, 30 | { label: '粘贴', role: 'paste' }, 31 | { label: '删除', role: 'delete' }, 32 | { label: '全选', role: 'selectAll' }, 33 | { type: 'separator' }, 34 | { 35 | label: '返回', 36 | accelerator: 'Esc', 37 | click() { 38 | application.mainWindow?.send('pressEsc') 39 | }, 40 | }, 41 | { 42 | label: '提高音量', 43 | accelerator: 'Up', 44 | click() { 45 | application.mainWindow?.send('changeVolume', 'up') 46 | }, 47 | }, 48 | { 49 | label: '降低音量', 50 | accelerator: 'Down', 51 | click() { 52 | application.mainWindow?.send('changeVolume', 'down') 53 | }, 54 | }, 55 | ], 56 | }, 57 | { 58 | label: '窗口', 59 | role: 'window', 60 | submenu: [ 61 | { label: '最小化', role: 'minimize' }, 62 | { label: '缩放', role: 'zoom' }, 63 | { label: '关闭', role: 'close' }, 64 | ], 65 | }, 66 | { 67 | label: '帮助', 68 | role: 'help', 69 | submenu: [ 70 | { 71 | label: '清理缓存', 72 | click() { 73 | application.clearSensitiveDirectories() 74 | }, 75 | }, 76 | { 77 | label: '重置应用', 78 | click() { 79 | application.clearAllUserData() 80 | }, 81 | }, 82 | { type: 'separator' }, 83 | { 84 | label: '报告问题', 85 | click() { 86 | shell.openExternal('https://github.com/liumingye/webmini/issues') 87 | }, 88 | }, 89 | { type: 'separator' }, 90 | { label: '开发者工具', role: 'toggleDevTools' }, 91 | { 92 | label: '显示网页检查器', 93 | accelerator: 'CmdOrCtrl+i', 94 | click() { 95 | Application.INSTANCE.mainWindow?.viewManager.selected?.webContents.toggleDevTools() 96 | }, 97 | }, 98 | ], 99 | }, 100 | ] 101 | if (is.dev()) { 102 | template.push({ 103 | label: 'DEBUG', 104 | submenu: [ 105 | { 106 | label: 'New tab', 107 | click() { 108 | Application.INSTANCE.mainWindow?.viewManager.registerViewContainer({ 109 | url: 'https://www.baidu.com', 110 | active: true, 111 | }) 112 | }, 113 | }, 114 | { 115 | label: 'Select next tab', 116 | click() { 117 | Application.INSTANCE.mainWindow?.webContents.send('select-next-tab') 118 | }, 119 | }, 120 | { 121 | label: 'Select previous tab', 122 | click() { 123 | Application.INSTANCE.mainWindow?.webContents.send('select-previous-tab') 124 | }, 125 | }, 126 | ], 127 | }) 128 | } 129 | return Menu.buildFromTemplate(template) 130 | } 131 | -------------------------------------------------------------------------------- /packages/main/menus/view.ts: -------------------------------------------------------------------------------- 1 | import { 2 | clipboard, 3 | ContextMenuParams, 4 | dialog, 5 | Menu, 6 | MenuItemConstructorOptions, 7 | WebContents, 8 | } from 'electron' 9 | import { extname } from 'path' 10 | import { isURI, prefixHttp } from '~/common/uri' 11 | import { MainWindow } from '../windows/main' 12 | 13 | export const saveAs = async (mainWindow: MainWindow) => { 14 | const selected = mainWindow.viewManager.selected 15 | if (!selected) return 16 | const { title, webContents } = selected 17 | 18 | const { canceled, filePath } = await dialog.showSaveDialog({ 19 | defaultPath: title, 20 | filters: [ 21 | { name: '页面源码', extensions: ['html', 'htm'] }, 22 | { name: '网页归档', extensions: ['*'] }, 23 | ], 24 | }) 25 | 26 | if (canceled || !filePath) return 27 | 28 | const ext = extname(filePath) 29 | 30 | webContents.savePage(filePath, ext === '.htm' ? 'HTMLOnly' : 'HTMLComplete') 31 | } 32 | 33 | export const getViewMenu = ( 34 | mainWindow: MainWindow, 35 | params: ContextMenuParams, 36 | webContents: WebContents, 37 | ) => { 38 | let menuItems: MenuItemConstructorOptions[] = [] 39 | 40 | if (params.linkURL !== '') { 41 | menuItems = menuItems.concat([ 42 | { 43 | label: '打开链接', 44 | click: () => { 45 | const selected = mainWindow.viewManager.selected 46 | if (!selected) return 47 | selected.webContents.loadURL(params.linkURL) 48 | }, 49 | }, 50 | { 51 | type: 'separator', 52 | }, 53 | { 54 | label: '拷贝链接', 55 | click: () => { 56 | clipboard.clear() 57 | clipboard.writeText(params.linkURL) 58 | }, 59 | }, 60 | { 61 | type: 'separator', 62 | }, 63 | ]) 64 | } 65 | 66 | if (params.hasImageContents) { 67 | menuItems = menuItems.concat([ 68 | { 69 | label: '拷贝图像', 70 | click: () => webContents.copyImageAt(params.x, params.y), 71 | }, 72 | { 73 | label: '拷贝图像地址', 74 | click: () => { 75 | clipboard.clear() 76 | clipboard.writeText(params.srcURL) 77 | }, 78 | }, 79 | { 80 | label: '存储图片为...', 81 | click: () => { 82 | mainWindow.webContents.downloadURL(params.srcURL) 83 | }, 84 | }, 85 | { 86 | type: 'separator', 87 | }, 88 | ]) 89 | } 90 | 91 | if (params.isEditable) { 92 | menuItems = menuItems.concat([ 93 | { 94 | label: '撤销', 95 | role: 'undo', 96 | accelerator: 'CmdOrCtrl+Z', 97 | }, 98 | { 99 | label: '重做', 100 | role: 'redo', 101 | accelerator: 'CmdOrCtrl+Shift+Z', 102 | }, 103 | { 104 | type: 'separator', 105 | }, 106 | { 107 | label: '剪切', 108 | role: 'cut', 109 | accelerator: 'CmdOrCtrl+X', 110 | }, 111 | { 112 | label: '拷贝', 113 | role: 'copy', 114 | accelerator: 'CmdOrCtrl+C', 115 | }, 116 | { 117 | label: '粘贴', 118 | role: 'paste', 119 | accelerator: 'CmdOrCtrl+Shift+V', 120 | }, 121 | { 122 | label: '粘贴并匹配样式', 123 | role: 'pasteAndMatchStyle', 124 | accelerator: 'CmdOrCtrl+V', 125 | }, 126 | { 127 | label: '全选', 128 | role: 'selectAll', 129 | accelerator: 'CmdOrCtrl+A', 130 | }, 131 | { 132 | type: 'separator', 133 | }, 134 | ]) 135 | } 136 | 137 | if (!params.isEditable && params.selectionText !== '') { 138 | menuItems = menuItems.concat([ 139 | { 140 | label: '拷贝', 141 | role: 'copy', 142 | accelerator: 'CmdOrCtrl+C', 143 | }, 144 | { 145 | type: 'separator', 146 | }, 147 | ]) 148 | } 149 | 150 | if (params.selectionText !== '') { 151 | const trimmedText = params.selectionText.trim() 152 | 153 | if (isURI(trimmedText)) { 154 | menuItems = menuItems.concat([ 155 | { 156 | label: '打开URL ' + trimmedText, 157 | click: () => { 158 | mainWindow.viewManager.registerViewContainer( 159 | { 160 | url: prefixHttp(trimmedText), 161 | active: true, 162 | }, 163 | true, 164 | ) 165 | }, 166 | }, 167 | { 168 | type: 'separator', 169 | }, 170 | ]) 171 | } 172 | } 173 | 174 | if ( 175 | !params.hasImageContents && 176 | params.linkURL === '' && 177 | params.selectionText === '' && 178 | !params.isEditable 179 | ) { 180 | menuItems = menuItems.concat([ 181 | { 182 | label: '返回', 183 | accelerator: 'Alt+Left', 184 | enabled: webContents.canGoBack(), 185 | click: () => { 186 | webContents.goBack() 187 | }, 188 | }, 189 | { 190 | label: '前进', 191 | accelerator: 'Alt+Right', 192 | enabled: webContents.canGoForward(), 193 | click: () => { 194 | webContents.goForward() 195 | }, 196 | }, 197 | { 198 | label: '重新载入页面', 199 | accelerator: 'CmdOrCtrl+R', 200 | click: () => { 201 | webContents.reload() 202 | }, 203 | }, 204 | { 205 | type: 'separator', 206 | }, 207 | { 208 | label: '将网页存储为...', 209 | accelerator: 'CmdOrCtrl+S', 210 | click: async () => { 211 | saveAs(mainWindow) 212 | }, 213 | }, 214 | { 215 | label: '打印页面...', 216 | accelerator: 'CmdOrCtrl+P', 217 | click: async () => { 218 | webContents.print() 219 | }, 220 | }, 221 | { 222 | type: 'separator', 223 | }, 224 | { 225 | label: '查看页面源文件', 226 | accelerator: 'CmdOrCtrl+U', 227 | click: () => { 228 | const viewManager = mainWindow.viewManager 229 | viewManager.registerViewContainer( 230 | { 231 | url: `view-source:${viewManager.selected?.url?.href}`, 232 | active: true, 233 | }, 234 | true, 235 | ) 236 | }, 237 | }, 238 | ]) 239 | } 240 | 241 | menuItems.push({ 242 | label: '检查元素', 243 | accelerator: 'CmdOrCtrl+Shift+I', 244 | click: () => { 245 | webContents.inspectElement(params.x, params.y) 246 | if (webContents.isDevToolsOpened()) { 247 | webContents.devToolsWebContents?.focus() 248 | } 249 | }, 250 | }) 251 | 252 | return Menu.buildFromTemplate(menuItems) 253 | } 254 | -------------------------------------------------------------------------------- /packages/main/models/protocol.ts: -------------------------------------------------------------------------------- 1 | import { protocol } from 'electron' 2 | import { join } from 'path' 3 | import { ERROR_PROTOCOL } from '~/common/constant' 4 | 5 | protocol.registerSchemesAsPrivileged([ 6 | { 7 | scheme: 'webmini', 8 | privileges: { 9 | bypassCSP: true, 10 | secure: true, 11 | standard: true, 12 | supportFetchAPI: true, 13 | allowServiceWorkers: true, 14 | corsEnabled: false, 15 | }, 16 | }, 17 | ]) 18 | 19 | export const registerProtocol = (session: Electron.Session) => { 20 | session.protocol.registerFileProtocol(ERROR_PROTOCOL, (request, callback: any) => { 21 | const _URL = new URL(request.url) 22 | if (_URL.hostname === 'network-error') { 23 | return callback({ 24 | path: join(__dirname, '../../resources/pages/network-error.html'), 25 | }) 26 | } 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /packages/main/models/sessions.ts: -------------------------------------------------------------------------------- 1 | interface Listener { 2 | onBeforeRequest: ( 3 | details: Electron.OnBeforeRequestListenerDetails, 4 | ) => Electron.Response | undefined 5 | onBeforeSendHeaders: ( 6 | details: Electron.OnBeforeSendHeadersListenerDetails, 7 | ) => Electron.BeforeSendResponse | undefined 8 | onHeadersReceived: ( 9 | details: Electron.OnHeadersReceivedListenerDetails, 10 | ) => Electron.HeadersReceivedResponse | undefined 11 | } 12 | 13 | export class Sessions { 14 | private readonly listeners = { 15 | beforeRequest: [] as Listener['onBeforeRequest'][], 16 | beforeSendHeaders: [] as Listener['onBeforeSendHeaders'][], 17 | headersReceived: [] as Listener['onHeadersReceived'][], 18 | } 19 | 20 | /** 21 | * Sessions 助手 22 | * @param sess 23 | */ 24 | constructor(public sess: Electron.Session, urls: string[] = []) { 25 | this.sess.webRequest.onBeforeRequest({ urls }, (details, callback) => { 26 | let _callback = {} 27 | for (const beforeRequest of this.listeners.beforeRequest) { 28 | _callback = { ..._callback, ...beforeRequest(details) } 29 | } 30 | callback(_callback) 31 | }) 32 | 33 | this.sess.webRequest.onBeforeSendHeaders({ urls }, (details, callback) => { 34 | let _callback = {} 35 | for (const beforeSendHeaders of this.listeners.beforeSendHeaders) { 36 | _callback = { ..._callback, ...beforeSendHeaders(details) } 37 | } 38 | callback(_callback) 39 | }) 40 | 41 | this.sess.webRequest.onHeadersReceived({ urls }, (details, callback) => { 42 | let _callback = {} 43 | for (const headersReceived of this.listeners.headersReceived) { 44 | _callback = { ..._callback, ...headersReceived(details) } 45 | } 46 | callback(_callback) 47 | }) 48 | } 49 | 50 | /** 51 | * 注册 52 | * @param event 53 | * @param listener 54 | * @returns 取消函数 55 | */ 56 | public register(event: 'onBeforeRequest', listener: Listener['onBeforeRequest']): () => boolean 57 | public register( 58 | event: 'onBeforeSendHeaders', 59 | listener: Listener['onBeforeSendHeaders'], 60 | ): () => boolean 61 | public register( 62 | event: 'onHeadersReceived', 63 | listener: Listener['onHeadersReceived'], 64 | ): () => boolean 65 | public register(event: T, listener: any): () => boolean { 66 | switch (event) { 67 | case 'onBeforeRequest': 68 | this.listeners.beforeRequest.push(listener) 69 | break 70 | case 'onBeforeSendHeaders': 71 | this.listeners.beforeSendHeaders.push(listener) 72 | break 73 | case 'onHeadersReceived': 74 | this.listeners.headersReceived.push(listener) 75 | break 76 | } 77 | // returns a unregister function 78 | return () => { 79 | return this.unregister(event, listener) 80 | } 81 | } 82 | 83 | /** 84 | * 取消注册 85 | * @param event 86 | * @param listener 87 | * @returns 是否成功 88 | */ 89 | public unregister(event: T, listener: Listener[T]): boolean { 90 | let list: unknown[] | undefined = undefined 91 | 92 | switch (event) { 93 | case 'onBeforeRequest': 94 | list = this.listeners.beforeRequest 95 | break 96 | case 'onBeforeSendHeaders': 97 | list = this.listeners.beforeSendHeaders 98 | break 99 | case 'onHeadersReceived': 100 | list = this.listeners.headersReceived 101 | break 102 | } 103 | 104 | if (list) { 105 | const index = list.indexOf(listener) 106 | if (index > -1) { 107 | list.splice(index, 1) 108 | return true 109 | } 110 | } 111 | 112 | return false 113 | } 114 | 115 | /** 116 | * 清空监听器 117 | */ 118 | public emptyListeners() { 119 | this.listeners.beforeRequest.length = 0 120 | this.listeners.beforeSendHeaders.length = 0 121 | this.listeners.headersReceived.length = 0 122 | } 123 | 124 | /** 125 | * 销毁 126 | */ 127 | public destroy() { 128 | this.emptyListeners() 129 | this.sess.webRequest.onBeforeRequest(null) 130 | this.sess.webRequest.onBeforeSendHeaders(null) 131 | this.sess.webRequest.onHeadersReceived(null) 132 | } 133 | 134 | /** 135 | * The user agent for this session. 136 | */ 137 | public get userAgent(): string { 138 | return this.sess.getUserAgent() 139 | } 140 | 141 | /** 142 | * Overrides the `userAgent` for this session. 143 | * This doesn't affect existing `WebContents`, and each `WebContents` can use 144 | * `webContents.setUserAgent` to override the session-wide user agent. 145 | */ 146 | public set userAgent(userAgent: string) { 147 | this.sess.setUserAgent(userAgent) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /packages/main/services/adblocker.ts: -------------------------------------------------------------------------------- 1 | import { ElectronBlocker } from '@cliqz/adblocker-electron' 2 | import { app, Session } from 'electron' 3 | import fetch from 'electron-fetch' 4 | import { promises } from 'fs' 5 | import { join } from 'path' 6 | 7 | export class AdblockerService { 8 | public blocker: ElectronBlocker | null = null 9 | 10 | private PREFIX = 'https://liumingye.coding.net/p/bilimini/d' 11 | 12 | private adsLists = [ 13 | `${this.PREFIX}/easylistchina/git/raw/master/easylistchina.txt`, 14 | `${this.PREFIX}/AdRules/git/raw/main/rules/ADgk.txt`, 15 | ] 16 | 17 | private config = { 18 | enableCompression: true, 19 | } 20 | 21 | private enginePath = join(app.getPath('userData'), 'engine.bin') 22 | 23 | private caching = { 24 | path: this.enginePath, 25 | read: promises.readFile, 26 | write: promises.writeFile, 27 | } 28 | 29 | constructor(private session: Session) {} 30 | 31 | public enable() { 32 | ElectronBlocker.fromLists(fetch, this.adsLists, this.config, this.caching).then((blocker) => { 33 | this.blocker = blocker 34 | this.blocker.enableBlockingInSession(this.session) 35 | console.log('blocked load complete') 36 | }) 37 | } 38 | 39 | public disable() { 40 | if (!this.blocker) { 41 | return 42 | } 43 | this.blocker.disableBlockingInSession(this.session) 44 | this.blocker = null 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/main/services/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import { app, dialog } from 'electron' 2 | import is from 'electron-is' 3 | import { autoUpdater } from 'electron-updater' 4 | import ms from 'ms' 5 | import Logger from '~/common/logger' 6 | import { sample } from 'lodash' 7 | import { Timer } from '~/common/timer' 8 | 9 | // Uninitialized 10 | // | 11 | // Idle --------- 12 | // (unavailable) / | | 13 | // CheckingForUpdate | 14 | // | | (error) 15 | // UpdateAvailable | 16 | // | | 17 | // Downloading ------ 18 | // | 19 | // Downloaded 20 | // | 21 | // Installing 22 | 23 | export interface IUpdateElectronAppOptions { 24 | /** 25 | * @param {String} updateInterval 26 | * How frequently to check for updates. 27 | */ 28 | readonly updateInterval?: string 29 | /** 30 | * @param {Object} logger 31 | * A custom logger object that defines a `log` function. 32 | * Defaults to `console`. See electron-log, a module 33 | * that aggregates logs from main and renderer processes into a single file. 34 | */ 35 | readonly logger?: typeof Logger 36 | /** 37 | * @param {Boolean} notifyUser 38 | * When enabled the user will be 39 | * prompted to apply the update immediately after download. 40 | */ 41 | readonly notifyUser?: boolean 42 | } 43 | 44 | const autoUpdaterService = (): void => { 45 | if (is.dev()) return 46 | // check for bad input early, so it will be logged during development 47 | const opts: IUpdateElectronAppOptions = { 48 | updateInterval: '15 minutes', 49 | logger: Logger, 50 | notifyUser: true, 51 | } 52 | app.isReady() ? initUpdater(opts) : app.on('ready', () => initUpdater(opts)) 53 | } 54 | 55 | const initUpdater = (opts: IUpdateElectronAppOptions) => { 56 | const { updateInterval, logger } = opts 57 | 58 | const log = (...args: any) => { 59 | logger && logger.info(args) 60 | } 61 | 62 | // using ghproxy accelerate download 63 | autoUpdater.netSession.webRequest.onBeforeRequest( 64 | { urls: ['https://github.com/*/releases/download/*'] }, 65 | ({ url }, callback) => { 66 | const proxyNode = [ 67 | 'https://ghproxy.com/', 68 | 'https://mirror.ghproxy.com/', 69 | 'https://endpoint.fastgit.org/', 70 | ] 71 | return callback({ redirectURL: `${sample(proxyNode)}${url}` }) 72 | }, 73 | ) 74 | 75 | // an error occurred while updating the trigger 76 | autoUpdater.on('error', (err) => { 77 | log('updater error') 78 | log(err) 79 | }) 80 | 81 | // when start to check the update trigger 82 | autoUpdater.on('checking-for-update', () => { 83 | log('checking-for-update') 84 | }) 85 | 86 | // can update the data 87 | autoUpdater.on('update-available', () => { 88 | log('update-available; downloading...') 89 | }) 90 | 91 | // no can update the data 92 | autoUpdater.on('update-not-available', () => { 93 | log('update-not-available') 94 | }) 95 | 96 | // the download is complete 97 | if (opts.notifyUser) { 98 | autoUpdater.on( 99 | 'update-downloaded', 100 | ({ releaseNotes, releaseName, releaseDate, downloadedFile }) => { 101 | log('update-downloaded', [releaseNotes, releaseName, releaseDate, downloadedFile]) 102 | 103 | const dialogOpts = { 104 | type: 'info', 105 | buttons: ['立即更新', '稍后更新'], 106 | title: `${app.name}更新`, 107 | message: `发现新版本${releaseName},重启以应用更新。`, 108 | detail: releaseNotes.replace(/(<([^>]+)>)/gi, ''), 109 | } 110 | 111 | dialog.showMessageBox(dialogOpts).then(({ response }) => { 112 | if (response === 0) autoUpdater.quitAndInstall() 113 | }) 114 | }, 115 | ) 116 | } 117 | 118 | // check for updates right away and keep checking later 119 | autoUpdater.checkForUpdates() 120 | if (updateInterval) { 121 | const timer = new Timer( 122 | () => { 123 | autoUpdater.checkForUpdates() 124 | }, 125 | ms(updateInterval), 126 | { mode: 'interval' }, 127 | ) 128 | timer.start() 129 | } 130 | } 131 | 132 | export default autoUpdaterService 133 | -------------------------------------------------------------------------------- /packages/main/services/storage.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron' 2 | import LocalDb from '../core/db' 3 | import type { Model } from '../core/db/types' 4 | import type { StorageServiceApi } from '~/interfaces' 5 | 6 | export class StorageService implements StorageServiceApi { 7 | public static INSTANCE = new this() 8 | 9 | public readonly localDb: LocalDb 10 | 11 | public constructor(public key = 'WEBMINI_DB_DEFAULT') { 12 | this.localDb = new LocalDb(app.getPath('userData')) 13 | } 14 | 15 | /** 16 | * 查询 17 | * @param id 18 | * @param key 19 | * @returns 20 | */ 21 | public async get(id: string, key = this.key) { 22 | return await this.localDb.get(key, id) 23 | } 24 | 25 | /** 26 | * 更改 27 | * @param doc 28 | * @param key 29 | * @returns 30 | */ 31 | public async put(doc: PouchDB.Core.PutDocument, key = this.key) { 32 | const allDocs = await this.localDb.get(key, doc._id) 33 | if (allDocs) { 34 | doc.data = { ...allDocs.data, ...doc.data } 35 | doc._rev = allDocs._rev 36 | } else { 37 | doc._rev = '' 38 | } 39 | return await this.localDb.put(key, doc) 40 | } 41 | 42 | /** 43 | * 删除 44 | * @param doc 45 | * @param key 46 | * @returns 47 | */ 48 | public async remove(doc: PouchDB.Core.RemoveDocument, key = this.key) { 49 | return await this.localDb.remove(key, doc) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "useDefineForClassFields": true, 5 | "lib": ["esnext", "dom"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "~/*": ["../*"] 9 | } 10 | }, 11 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/main/utils/getUrl.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { app } from 'electron' 3 | 4 | const getBaseUrl = () => { 5 | if (process.env.NODE_ENV === 'development') { 6 | return `http://${process.env['VITE_DEV_SERVER_HOST']}:${process.env['VITE_DEV_SERVER_PORT']}` 7 | } else { 8 | return `file://${path.join(app.getAppPath(), 'dist/renderer')}` 9 | } 10 | } 11 | 12 | const getUrl = (pageName: string, route: string) => { 13 | const baseUrl = getBaseUrl() 14 | return `${path.join(baseUrl, `${pageName}.html`)}${route ? `#/${route}` : ''}` 15 | } 16 | 17 | export { getBaseUrl, getUrl } 18 | -------------------------------------------------------------------------------- /packages/main/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { nativeTheme, screen } from 'electron' 2 | import { isString, isEmpty } from 'lodash' 3 | import { Color } from '~/common/color' 4 | import { Application } from '../application' 5 | import is from 'electron-is' 6 | import { Theme } from '~/interfaces' 7 | 8 | export const matchPattern = (str: string) => { 9 | return (pattern: string | RegExp) => { 10 | if (isString(pattern)) { 11 | return str.includes(pattern) 12 | } 13 | return pattern.test(str) 14 | } 15 | } 16 | 17 | /** 18 | * 主题色更改 19 | */ 20 | export const hookThemeColor = (): void => { 21 | const mainWindow = Application.INSTANCE.mainWindow 22 | if (!mainWindow) return 23 | 24 | let themeData: Theme = { light: { bg: '', text: '' }, dark: { bg: '', text: '' } } 25 | 26 | const view = mainWindow.viewManager.selected 27 | // console.log(view) 28 | if (view && !isEmpty(view.plugins) && view.plugins[0].themeColor) { 29 | themeData = view.plugins[0].themeColor 30 | } 31 | 32 | const onDarkModeChange = () => { 33 | if (mainWindow.isDestroyed()) return 34 | 35 | const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light' 36 | 37 | const themeColor = themeData[theme] 38 | 39 | // 未定义背景颜色则获取网页主题色 40 | if (themeColor && !themeColor.bg) { 41 | const color = mainWindow.viewManager.selected?.themeColor 42 | if (color) { 43 | themeColor.bg = color 44 | } 45 | } 46 | 47 | // 未定义文字颜色则自动获取文字颜色 48 | if (themeColor && !themeColor.text && themeColor.bg) { 49 | const baseColor = Color.Format.CSS.parseHex(themeColor.bg) 50 | if (baseColor) { 51 | const text = baseColor.isDarker() ? baseColor.lighten(100) : baseColor.darken(100) 52 | if (text) { 53 | themeColor.text = text.toString() 54 | } 55 | } 56 | } 57 | 58 | mainWindow.send('setThemeColor', { 59 | theme, 60 | ...themeColor, 61 | }) 62 | } 63 | 64 | nativeTheme.removeListener('updated', onDarkModeChange).addListener('updated', onDarkModeChange) 65 | 66 | onDarkModeChange() 67 | } 68 | 69 | export const getDisplayBounds = () => { 70 | // We want the new window to open on the same display that the parent is in 71 | let displayToUse: Electron.Display | undefined 72 | const displays = screen.getAllDisplays() 73 | // Single Display 74 | if (displays.length === 1) { 75 | displayToUse = displays[0] 76 | } 77 | // Multi Display 78 | else { 79 | // on mac there is 1 menu per window so we need to use the monitor where the cursor currently is 80 | if (is.macOS()) { 81 | const cursorPoint = screen.getCursorScreenPoint() 82 | displayToUse = screen.getDisplayNearestPoint(cursorPoint) 83 | } 84 | // fallback to primary display or first display 85 | if (!displayToUse) { 86 | displayToUse = screen.getPrimaryDisplay() || displays[0] 87 | } 88 | } 89 | return displayToUse.bounds 90 | } 91 | -------------------------------------------------------------------------------- /packages/main/viewManager.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import is from 'electron-is' 3 | import { View } from './view' 4 | import type { MainWindow } from './windows/main' 5 | 6 | export class ViewManager { 7 | private readonly viewContainer = new Map() 8 | 9 | public selectedId = 0 10 | 11 | private window: MainWindow 12 | 13 | private showTopBar = true 14 | 15 | private autoHideBar = false 16 | 17 | public constructor(window: MainWindow) { 18 | this.window = window 19 | 20 | const { id } = window.win 21 | 22 | ipcMain.handle(`view-create-${id}`, (e, details) => { 23 | const view = this.registerViewContainer(details, false, false) 24 | return view.id 25 | }) 26 | 27 | ipcMain.handle(`views-create-${id}`, (e, options) => { 28 | return options.map((option: any) => { 29 | const view = this.registerViewContainer(option, false, false) 30 | return view.id 31 | }) 32 | }) 33 | 34 | ipcMain.handle(`view-select-${id}`, (e, id: number, focus: boolean) => { 35 | this.select(id, focus) 36 | }) 37 | 38 | ipcMain.handle(`browserview-hide-${id}`, () => { 39 | this.hide() 40 | }) 41 | 42 | ipcMain.handle(`browserview-show-${id}`, () => { 43 | this.show() 44 | }) 45 | 46 | ipcMain.handle(`top-bar-status-${id}`, (e, { autoHideBar, showTopBar }) => { 47 | this.autoHideBar = autoHideBar 48 | this.showTopBar = showTopBar 49 | this.fixBounds() 50 | }) 51 | 52 | ipcMain.handle(`resize-window-size-${id}`, (e, windowType) => { 53 | this.selected?.resizeWindowSize(windowType) 54 | }) 55 | } 56 | 57 | public select(id: number, focus = true): void { 58 | // 防止重复执行 59 | if (this.selectedId === id) { 60 | return 61 | } 62 | 63 | const view = this.viewContainer.get(id) 64 | 65 | if (!view) { 66 | return 67 | } 68 | 69 | this.selectedId = id 70 | 71 | if (this.selected) { 72 | this.window.win.removeBrowserView(this.selected.browserView) 73 | } 74 | 75 | this.window.win.addBrowserView(view.browserView) 76 | 77 | if (focus) { 78 | // Also fixes switching tabs with Ctrl + Tab 79 | view.webContents.focus() 80 | } else { 81 | this.window.webContents.focus() 82 | } 83 | 84 | this.fixBounds() 85 | 86 | if (view.firstSelect === true) { 87 | // 第一次选择时跳过,避免重复执行 88 | view.firstSelect = false 89 | } else { 90 | view.browserView.webContents.emit('did-change-theme-color') 91 | view.loadPlugins() 92 | view.setUserAgent() 93 | view.updateTitle() 94 | view.updateNavigationState() 95 | view.resizeWindowSize(undefined, true) 96 | } 97 | } 98 | 99 | /** 100 | * fixBounds 101 | * @description 修复浏览器窗口大小 102 | * @returns void 103 | */ 104 | public fixBounds(): void { 105 | const view = this.selected 106 | 107 | if (!view) return 108 | 109 | const { width, height } = this.window.win.getContentBounds() 110 | 111 | const topbarContentHeight = 32 112 | 113 | const newBounds = { 114 | x: 0, 115 | y: this.showTopBar ? topbarContentHeight : 0, 116 | width, 117 | height: this.autoHideBar ? height : height - topbarContentHeight, 118 | } 119 | 120 | if (newBounds !== view.bounds) { 121 | view.browserView.setBounds(newBounds) 122 | view.bounds = newBounds 123 | } 124 | } 125 | 126 | public get selected() { 127 | return this.viewContainer.get(this.selectedId) 128 | } 129 | 130 | public registerViewContainer(details: any, isNext = false, sendMessage = true): View { 131 | const view = new View(this.window, details) 132 | 133 | const { webContents } = view.browserView 134 | const { id } = view 135 | 136 | this.viewContainer.set(id, view) 137 | 138 | if (details.active) { 139 | this.select(id, true) 140 | } 141 | 142 | webContents.once('destroyed', () => { 143 | this.viewContainer.delete(id) 144 | }) 145 | 146 | if (sendMessage) { 147 | this.window.send('create-tab', details, isNext, id) 148 | } 149 | 150 | /** 151 | * [mac] 下 setAutoResize 会有偏移 152 | * 这里关闭 setAutoResize 使用 fixBounds 手动改变大小 153 | */ 154 | if (!is.macOS()) { 155 | view.browserView.setAutoResize({ 156 | width: true, 157 | height: true, 158 | }) 159 | } 160 | 161 | return view 162 | } 163 | 164 | public deregisterViewContainer(id: number): void { 165 | console.log('deregisterViewContainer' + id) 166 | 167 | const view = this.viewContainer.get(id) 168 | this.viewContainer.delete(id) 169 | 170 | if (view && !view.isDestroyed()) { 171 | this.window.win.removeBrowserView(view.browserView) 172 | view.destroy() 173 | } 174 | } 175 | 176 | public clearViewContainer(): void { 177 | this.window.win.setBrowserView(null) 178 | this.viewContainer.forEach((view) => { 179 | this.deregisterViewContainer(view.id) 180 | }) 181 | } 182 | 183 | public hide(): void { 184 | const browserView = this.selected?.browserView 185 | if (!browserView) return 186 | this.window.win.removeBrowserView(browserView) 187 | } 188 | 189 | public show(): void { 190 | const browserView = this.selected?.browserView 191 | if (!browserView) return 192 | this.window.win.addBrowserView(browserView) 193 | this.fixBounds() 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /packages/main/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module' 2 | import { defineConfig } from 'vite' 3 | import { resolve } from 'path' 4 | import pkg from '../../package.json' 5 | 6 | export default defineConfig({ 7 | root: __dirname, 8 | base: './', 9 | resolve: { 10 | alias: { 11 | '~': resolve(__dirname, '../'), 12 | }, 13 | }, 14 | build: { 15 | outDir: '../../dist/main', 16 | lib: { 17 | entry: 'index.ts', 18 | formats: ['cjs'], 19 | fileName: () => '[name].cjs', 20 | }, 21 | emptyOutDir: true, 22 | rollupOptions: { 23 | external: ['electron', ...builtinModules, ...Object.keys(pkg.dependencies || {})], 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/main/windows/common.ts: -------------------------------------------------------------------------------- 1 | import { enable } from '@electron/remote/main' 2 | import type { BrowserWindow } from 'electron' 3 | import type { CommonWindowApi } from '~/interfaces' 4 | 5 | export abstract class CommonWindow implements CommonWindowApi { 6 | // abstract name: string 7 | 8 | protected constructor(public win: BrowserWindow) { 9 | enable(this.webContents) 10 | } 11 | 12 | public show(): void { 13 | if (this.win.isMinimized()) { 14 | this.win.restore() 15 | } 16 | this.win.show() 17 | } 18 | 19 | public hide(): void { 20 | this.win.hide() 21 | } 22 | 23 | public toggle(): void { 24 | if (this.isDestroyed()) return 25 | if (this.win.isVisible()) { 26 | this.hide() 27 | } else { 28 | this.show() 29 | } 30 | } 31 | 32 | public isDestroyed(): boolean { 33 | return this.win.isDestroyed() 34 | } 35 | 36 | public get id() { 37 | return this.win.webContents.id 38 | } 39 | 40 | public get webContents() { 41 | return this.win.webContents 42 | } 43 | 44 | public get session() { 45 | return this.win.webContents.session 46 | } 47 | 48 | public send(channel: string, ...args: any[]): void { 49 | this.webContents.send(channel, ...args) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/main/windows/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, BrowserWindowConstructorOptions, shell, nativeTheme } from 'electron' 2 | import is from 'electron-is' 3 | import { throttle } from 'lodash' 4 | import { join } from 'path' 5 | import { StorageService } from '../services/storage' 6 | import { ViewManager } from '../viewManager' 7 | import { CommonWindow } from './common' 8 | import { Sessions } from '../models/sessions' 9 | import { getUrl } from '../utils/getUrl' 10 | import { WindowTypeEnum, WindowType, WindowTypeDefault } from '~/interfaces/view' 11 | 12 | export class MainWindow extends CommonWindow { 13 | public viewManager: ViewManager 14 | 15 | private crossDomainSess: Sessions | undefined 16 | 17 | public constructor() { 18 | const window = new BrowserWindow({ 19 | show: false, 20 | width: 300, 21 | height: 500, 22 | minHeight: 170, 23 | minWidth: 300, 24 | frame: false, // 是否有边框 25 | maximizable: true, // 加好 保存窗口大小 窗口位置 开启 26 | alwaysOnTop: true, 27 | webPreferences: { 28 | preload: join(__dirname, '../preload/index.cjs'), // 预先加载指定的脚本 29 | }, 30 | }) 31 | 32 | super(window) 33 | 34 | this.win.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#2e2c29' : '#fff') 35 | 36 | this.setBoundsFromDb().then(() => { 37 | this.show() 38 | }) 39 | 40 | this.win.loadURL(getUrl('index', 'home')) 41 | 42 | this.viewManager = new ViewManager(this) 43 | 44 | // Make all links open with the browser, not with the application 45 | this.webContents.setWindowOpenHandler(({ url }) => { 46 | if (url.startsWith('https:')) shell.openExternal(url) 47 | return { action: 'deny' } 48 | }) 49 | 50 | this.webContents.on('dom-ready', this.eventDomReady.bind(this)) 51 | this.win.on('close', this.eventClose.bind(this)) 52 | this.win.on('moved', this.eventMoved.bind(this)) 53 | this.win.on('resized', this.eventResized.bind(this)) 54 | /** 55 | * [mac] 下 setAutoResize 会有偏移 56 | * 这里关闭 setAutoResize 使用 fixBounds 手动改变大小 57 | */ 58 | if (is.macOS()) { 59 | this.win.on('resize', this.eventResize.bind(this)) 60 | } 61 | } 62 | 63 | private eventResize = throttle(() => { 64 | this.viewManager.fixBounds() 65 | }, 150) 66 | 67 | private async setBoundsFromDb() { 68 | const appDb = await StorageService.INSTANCE.get('appDb') 69 | const bound: BrowserWindowConstructorOptions = {} 70 | if (appDb) { 71 | if (appDb.data.windowPosition) { 72 | bound.x = appDb.data.windowPosition[0] 73 | bound.y = appDb.data.windowPosition[1] 74 | } 75 | if (appDb.data.windowSize) { 76 | bound.width = appDb.data.windowSize[WindowTypeEnum.MOBILE][0] 77 | bound.height = appDb.data.windowSize[WindowTypeEnum.MOBILE][1] 78 | } 79 | this.win.setBounds(bound) 80 | } 81 | } 82 | 83 | private eventDomReady() { 84 | // 处理跨域 85 | this.crossDomainSess = new Sessions(this.session, [ 86 | 'https://gitee.com/liumingye/webmini-database/raw/master/*', 87 | ]) 88 | this.crossDomainSess.register('onBeforeSendHeaders', (details) => { 89 | if (details.resourceType === 'xhr' && details.requestHeaders) { 90 | details.requestHeaders['Referer'] = 'https://gitee.com' 91 | return { requestHeaders: details.requestHeaders } 92 | } 93 | }) 94 | this.crossDomainSess.register('onHeadersReceived', (details) => { 95 | if (details.resourceType === 'xhr' && details.responseHeaders) { 96 | details.responseHeaders['Access-Control-Allow-Origin'] = ['*'] 97 | return { responseHeaders: details.responseHeaders } 98 | } 99 | }) 100 | } 101 | 102 | private eventClose() { 103 | this.crossDomainSess?.destroy() 104 | this.viewManager.clearViewContainer() 105 | if (!is.macOS()) { 106 | process.nextTick(() => { 107 | app.quit() 108 | }) 109 | } 110 | } 111 | 112 | private async eventResized() { 113 | // 保存窗口大小 114 | const isMaximized = this.win.isMaximized() 115 | const isFullScreen = this.win.isFullScreen() 116 | if (isMaximized || isFullScreen) return 117 | 118 | const appDb = await StorageService.INSTANCE.get('appDb') 119 | 120 | // 查询 121 | const windowSize: WindowType = appDb?.data.windowSize || WindowTypeDefault 122 | const windowType = this.viewManager.selected?.windowType || WindowTypeEnum.MOBILE 123 | 124 | // 获取 125 | windowSize[windowType] = this.win.getSize() 126 | 127 | // 写入 128 | StorageService.INSTANCE.put({ 129 | _id: 'appDb', 130 | data: { windowSize }, 131 | }) 132 | 133 | // 保存一下窗口位置 134 | setTimeout(() => { 135 | this.win.emit('moved') 136 | }, 15) 137 | } 138 | 139 | private eventMoved() { 140 | const windowType = this.viewManager.selected?.windowType || WindowTypeEnum.MOBILE 141 | if (windowType !== WindowTypeEnum.MOBILE) return 142 | 143 | // 获取 144 | const windowPosition = this.win.getPosition() 145 | 146 | // 写入 147 | StorageService.INSTANCE.put({ 148 | _id: 'appDb', 149 | data: { windowPosition }, 150 | }) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /packages/main/windows/selectPart.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain } from 'electron' 2 | import { join } from 'path' 3 | import { Application } from '../application' 4 | import { CommonWindow } from './common' 5 | import { getDisplayBounds } from '../utils' 6 | import { getUrl } from '../utils/getUrl' 7 | 8 | export class SelectPartWindow extends CommonWindow { 9 | public constructor() { 10 | const window = new BrowserWindow({ 11 | show: false, 12 | width: 200, 13 | height: 300, 14 | frame: false, 15 | maximizable: false, 16 | alwaysOnTop: true, 17 | webPreferences: { 18 | preload: join(__dirname, '../preload/index.cjs'), // 预先加载指定的脚本 19 | }, 20 | }) 21 | 22 | super(window) 23 | 24 | this.win.loadURL(getUrl('index', 'select-part')) 25 | 26 | ipcMain.on('toggle-select-part-window', () => { 27 | this.toggle() 28 | }) 29 | 30 | ipcMain.on('show-select-part-window', () => { 31 | this.show() 32 | }) 33 | } 34 | 35 | public show(): void { 36 | const mainWindow = Application.INSTANCE.mainWindow 37 | 38 | if (this.isDestroyed() || !mainWindow) return 39 | 40 | const mainPos = mainWindow.win.getPosition() 41 | const selectPartSize = this.win.getSize() 42 | 43 | const x: number = (() => { 44 | // 默认显示在窗口左面 45 | // the default display in the window on the left 46 | const x = mainPos[0] - selectPartSize[0] 47 | // 超出显示器 显示在窗口右面 48 | // beyond the display in the window is on the right 49 | if (x < getDisplayBounds().x) { 50 | const mainSize = mainWindow.win.getSize() 51 | return mainPos[0] + mainSize[0] 52 | } 53 | return x 54 | })() 55 | const y: number = mainPos[1] 56 | 57 | this.win.setPosition(x, y) 58 | super.show() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentWindow, screen } from '@electron/remote' 2 | import { contextBridge, ipcRenderer } from 'electron' 3 | import { withPrototype } from '~/common' 4 | import Cookies from '~/common/cookies' 5 | import { domContentLoaded } from '~/common/dom' 6 | import Logger from '~/common/logger' 7 | import Net from '~/common/net' 8 | import useLoading from './utils/loading' 9 | import Versions from './utils/versions' 10 | 11 | const { appendLoading, removeLoading } = useLoading() 12 | 13 | domContentLoaded().then(appendLoading) 14 | 15 | // --------- Expose some API to the Renderer process. --------- 16 | contextBridge.exposeInMainWorld('removeLoading', removeLoading) 17 | contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer)) 18 | 19 | const { 20 | minimize, 21 | setBounds, 22 | getPosition, 23 | getSize, 24 | hide, 25 | on, 26 | once, 27 | isDestroyed, 28 | isAlwaysOnTop, 29 | setAlwaysOnTop, 30 | id, 31 | } = getCurrentWindow() 32 | 33 | contextBridge.exposeInMainWorld('app', { 34 | cookies: new Cookies(), 35 | versions: new Versions(), 36 | screen: withPrototype(screen), 37 | currentWindow: { 38 | minimize, 39 | setBounds, 40 | getPosition, 41 | getSize, 42 | hide, 43 | on, 44 | once, 45 | isDestroyed, 46 | isAlwaysOnTop, 47 | setAlwaysOnTop, 48 | id, 49 | }, 50 | net: new Net(), 51 | logger: withPrototype(Logger), 52 | }) 53 | -------------------------------------------------------------------------------- /packages/preload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "useDefineForClassFields": true, 5 | "lib": ["esnext", "dom"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "~/*": ["../*"] 9 | } 10 | }, 11 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/preload/utils/loading.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://tobiasahlin.com/spinkit 3 | * https://connoratherton.com/loaders 4 | * https://projects.lukehaas.me/css-loaders 5 | * https://matejkustec.github.io/SpinThatShit 6 | */ 7 | export default () => { 8 | let lightColor = '#fff' 9 | let darkColor = '#fb7299' 10 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 11 | lightColor = '#282c34' 12 | darkColor = '#fff' 13 | } 14 | const className = `loaders-css__square-spin` 15 | const styleContent = ` 16 | @keyframes square-spin { 17 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 18 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 19 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 20 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 21 | } 22 | .${className} > div { 23 | animation-fill-mode: both; 24 | width: 50px; 25 | height: 50px; 26 | background: ${darkColor}; 27 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 28 | } 29 | .app-loading-wrap { 30 | position: fixed; 31 | top: 0; 32 | left: 0; 33 | width: 100vw; 34 | height: 100vh; 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | background: ${lightColor}; 39 | z-index: 9; 40 | } 41 | ` 42 | const oStyle = document.createElement('style') 43 | const oDiv = document.createElement('div') 44 | 45 | oStyle.id = 'app-loading-style' 46 | oStyle.innerHTML = styleContent 47 | oDiv.className = 'app-loading-wrap' 48 | oDiv.innerHTML = `
` 49 | 50 | return { 51 | appendLoading() { 52 | document.head.appendChild(oStyle) 53 | document.body.appendChild(oDiv) 54 | }, 55 | removeLoading() { 56 | document.head.removeChild(oStyle) 57 | document.body.removeChild(oDiv) 58 | }, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/preload/utils/versions.ts: -------------------------------------------------------------------------------- 1 | import { app } from '@electron/remote' 2 | import is from 'electron-is' 3 | import { arch, release, type } from 'os' 4 | import { version as VueVersion } from 'vue' 5 | 6 | class Versions { 7 | private isLinuxSnap() { 8 | return is.linux() && !!process.env['SNAP'] && !!process.env['SNAP_REVISION'] 9 | } 10 | public App = app.getVersion() 11 | public 'Vue.js' = VueVersion 12 | public Electron = process.versions.electron 13 | public Chromium = process.versions.chrome 14 | public 'Node.js' = process.versions.node 15 | public V8 = process.versions.v8 16 | public OS = `${type} ${arch} ${release}${this.isLinuxSnap() ? ' snap' : ''}` 17 | } 18 | 19 | export default Versions 20 | -------------------------------------------------------------------------------- /packages/preload/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { builtinModules } from 'module' 2 | import { defineConfig } from 'vite' 3 | import { resolve } from 'path' 4 | import pkg from '../../package.json' 5 | 6 | export default defineConfig({ 7 | root: __dirname, 8 | base: './', 9 | resolve: { 10 | alias: { 11 | '~': resolve(__dirname, '../'), 12 | }, 13 | }, 14 | build: { 15 | outDir: '../../dist/preload', 16 | lib: { 17 | entry: 'index.ts', 18 | formats: ['cjs'], 19 | fileName: () => '[name].cjs', 20 | }, 21 | emptyOutDir: true, 22 | rollupOptions: { 23 | external: ['electron', ...builtinModules, ...Object.keys(pkg.dependencies || {})], 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /packages/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | webmini 8 | 9 | 10 |
load failed
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/renderer/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /packages/renderer/src/apis/plugin.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | import type { AxiosPromise } from 'axios' 3 | 4 | export const fetchTotalPlugins = (): AxiosPromise => { 5 | return request({ 6 | baseURL: 'https://gitee.com/liumingye/webmini-database/raw/master', 7 | url: 'plugins.json', 8 | method: 'GET', 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /packages/renderer/src/assets/css/css-variables.less: -------------------------------------------------------------------------------- 1 | body { 2 | --theme-color-bg: var(--color-bg-3); 3 | --theme-color-text: var(--color-text-1); 4 | --border-radius-medium: 8px; 5 | } 6 | 7 | @color-app-bg: #fb7299; 8 | -------------------------------------------------------------------------------- /packages/renderer/src/assets/css/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | html { 7 | /* 2 */ 8 | -webkit-font-smoothing: inherit; 9 | 10 | /* chrome、safari */ 11 | -moz-osx-font-smoothing: inherit; 12 | } 13 | 14 | // nprogress 15 | #nprogress .bar { 16 | height: 3px; 17 | } 18 | 19 | // 路由动画 20 | .slide-left-enter-from { 21 | transform: translate3d(100%, 0, 0); 22 | } 23 | 24 | .slide-left-enter-active { 25 | position: fixed; 26 | z-index: 9999; 27 | width: 100%; 28 | transition: transform 0.25s ease-out; 29 | } 30 | 31 | .slide-left-leave-to { 32 | transform: translate3d(-20%, 0, 0); 33 | } 34 | 35 | .slide-left-leave-active { 36 | position: absolute; 37 | width: 100%; 38 | transition: transform 0.25s ease-in; 39 | } 40 | 41 | .slide-right-enter-from { 42 | transform: translate3d(-20%, 0, 0); 43 | } 44 | 45 | .slide-right-enter-active { 46 | position: fixed; 47 | width: 100%; 48 | transition: transform 0.25s ease-out; 49 | } 50 | 51 | .slide-right-leave-to { 52 | transform: translate3d(100%, 0, 0); 53 | } 54 | 55 | .slide-right-leave-active { 56 | position: absolute; 57 | z-index: 9999; 58 | width: 100%; 59 | transition: transform 0.25s ease-in; 60 | } 61 | -------------------------------------------------------------------------------- /packages/renderer/src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumingye/webmini/9ec67af3fcab8034fdd787becd57426e22c0f087/packages/renderer/src/assets/images/icon.png -------------------------------------------------------------------------------- /packages/renderer/src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | // We suggest you to commit this file into source control 3 | declare global { 4 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 5 | const axios: typeof import('axios')['default'] 6 | const computed: typeof import('vue')['computed'] 7 | const createApp: typeof import('vue')['createApp'] 8 | const createPinia: typeof import('pinia')['createPinia'] 9 | const customRef: typeof import('vue')['customRef'] 10 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 11 | const defineComponent: typeof import('vue')['defineComponent'] 12 | const defineStore: typeof import('pinia')['defineStore'] 13 | const effectScope: typeof import('vue')['effectScope'] 14 | const EffectScope: typeof import('vue')['EffectScope'] 15 | const getActivePinia: typeof import('pinia')['getActivePinia'] 16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 17 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 18 | const h: typeof import('vue')['h'] 19 | const inject: typeof import('vue')['inject'] 20 | const isReadonly: typeof import('vue')['isReadonly'] 21 | const isRef: typeof import('vue')['isRef'] 22 | const mapActions: typeof import('pinia')['mapActions'] 23 | const mapGetters: typeof import('pinia')['mapGetters'] 24 | const mapState: typeof import('pinia')['mapState'] 25 | const mapStores: typeof import('pinia')['mapStores'] 26 | const mapWritableState: typeof import('pinia')['mapWritableState'] 27 | const markRaw: typeof import('vue')['markRaw'] 28 | const nextTick: typeof import('vue')['nextTick'] 29 | const onActivated: typeof import('vue')['onActivated'] 30 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 31 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 32 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 33 | const onDeactivated: typeof import('vue')['onDeactivated'] 34 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 35 | const onMounted: typeof import('vue')['onMounted'] 36 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 37 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 38 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 39 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 40 | const onUnmounted: typeof import('vue')['onUnmounted'] 41 | const onUpdated: typeof import('vue')['onUpdated'] 42 | const provide: typeof import('vue')['provide'] 43 | const reactive: typeof import('vue')['reactive'] 44 | const readonly: typeof import('vue')['readonly'] 45 | const ref: typeof import('vue')['ref'] 46 | const resolveComponent: typeof import('vue')['resolveComponent'] 47 | const setActivePinia: typeof import('pinia')['setActivePinia'] 48 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 49 | const shallowReactive: typeof import('vue')['shallowReactive'] 50 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 51 | const shallowRef: typeof import('vue')['shallowRef'] 52 | const storeToRefs: typeof import('pinia')['storeToRefs'] 53 | const toRaw: typeof import('vue')['toRaw'] 54 | const toRef: typeof import('vue')['toRef'] 55 | const toRefs: typeof import('vue')['toRefs'] 56 | const triggerRef: typeof import('vue')['triggerRef'] 57 | const unref: typeof import('vue')['unref'] 58 | const useAttrs: typeof import('vue')['useAttrs'] 59 | const useCssModule: typeof import('vue')['useCssModule'] 60 | const useCssVars: typeof import('vue')['useCssVars'] 61 | const useRoute: typeof import('vue-router')['useRoute'] 62 | const useRouter: typeof import('vue-router')['useRouter'] 63 | const useSlots: typeof import('vue')['useSlots'] 64 | const watch: typeof import('vue')['watch'] 65 | const watchEffect: typeof import('vue')['watchEffect'] 66 | } 67 | export {} 68 | -------------------------------------------------------------------------------- /packages/renderer/src/components/TopBar.vue: -------------------------------------------------------------------------------- 1 | 151 | 152 | 203 | 204 | 238 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/_utils/global-config.ts: -------------------------------------------------------------------------------- 1 | const COMPONENT_PREFIX = 'b' 2 | 3 | export const getComponentPrefix = (): string => { 4 | return `${COMPONENT_PREFIX}-` 5 | } 6 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/button/Button.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 31 | 32 | 50 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/button/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { getComponentPrefix } from '../_utils/global-config' 3 | import _Button from './Button.vue' 4 | 5 | const Button = { 6 | install: (app: App): void => { 7 | const componentPrefix = getComponentPrefix() 8 | app.component(componentPrefix + 'button', _Button) 9 | }, 10 | } 11 | 12 | export default Button 13 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/icon/icons/TargetTwo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @file TargetTwo 枪靶 3 | * @author Auto Generated by IconPark 4 | */ 5 | 6 | /* tslint:disable: max-line-length */ 7 | /* eslint-disable max-len */ 8 | import { ISvgIconProps, IconWrapper } from '../runtime' 9 | 10 | export default IconWrapper('target-two', false, (props: ISvgIconProps) => ( 11 | 12 | 19 | 26 | 34 | 42 | 43 | )) 44 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/icon/icons/Windmill.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Windmill 风车 3 | * @author Auto Generated by IconPark 4 | */ 5 | 6 | /* tslint:disable: max-line-length */ 7 | /* eslint-disable max-len */ 8 | import { ISvgIconProps, IconWrapper } from '../runtime' 9 | 10 | export default IconWrapper('windmill', true, (props: ISvgIconProps) => ( 11 | 12 | 22 | 32 | 42 | 52 | 53 | )) 54 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/icon/index.tsx: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import * as IconMap from './map' 3 | 4 | const install = (app: App): void => { 5 | Object.values(IconMap).forEach((icon) => { 6 | app.component(icon.name, icon) 7 | }) 8 | } 9 | 10 | export default install 11 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/icon/map.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @file map 引用出口 3 | * @author Auto Generated by IconPark 4 | */ 5 | 6 | export { default as TargetTwo } from './icons/TargetTwo' 7 | export { default as Windmill } from './icons/Windmill' 8 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/icon/runtime/index.less: -------------------------------------------------------------------------------- 1 | /** 2 | * @file index 样式文件 3 | * @author Auto Generated by IconPark 4 | */ 5 | 6 | .i-icon { 7 | display: inline-block; 8 | color: inherit; 9 | font-style: normal; 10 | line-height: 0; 11 | text-align: center; 12 | text-transform: none; 13 | vertical-align: -0.125em; 14 | text-rendering: optimizelegibility; 15 | -webkit-font-smoothing: antialiased; 16 | -moz-osx-font-smoothing: grayscale; 17 | 18 | &-spin svg { 19 | animation: i-icon-spin 1s infinite linear; 20 | } 21 | 22 | &-rtl { 23 | transform: scaleX(-1); 24 | } 25 | } 26 | 27 | @keyframes i-icon-spin { 28 | 100% { 29 | transform: rotate(360deg); 30 | } 31 | } 32 | 33 | @keyframes i-icon-spin { 34 | 100% { 35 | transform: rotate(360deg); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/icon/runtime/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @file runtime 运行时 3 | * @author Auto Generated by IconPark 4 | */ 5 | 6 | import { ComponentOptions, DefineComponent, inject, provide } from 'vue' 7 | 8 | // 描边连接类型 9 | export type StrokeLinejoin = 'miter' | 'round' | 'bevel' 10 | 11 | // 描边端点类型 12 | export type StrokeLinecap = 'butt' | 'round' | 'square' 13 | 14 | // 主题 15 | export type Theme = 'outline' | 'filled' | 'two-tone' | 'multi-color' 16 | 17 | // 包裹前的图标属性 18 | export interface ISvgIconProps { 19 | // 当前图标的唯一Id 20 | id: string 21 | 22 | // 图标尺寸大小,默认1em 23 | size: number | string 24 | 25 | // 描边宽度 26 | strokeWidth: number 27 | 28 | // 描边端点类型 29 | strokeLinecap: StrokeLinecap 30 | 31 | // 描边连接线类型 32 | strokeLinejoin: StrokeLinejoin 33 | 34 | // 换肤的颜色数组 35 | colors: string[] 36 | } 37 | 38 | // 图标配置属性 39 | export interface IIconConfig { 40 | // 图标尺寸大小,默认1em 41 | size: number | string 42 | 43 | // 描边宽度 44 | strokeWidth: number 45 | 46 | // 描边端点类型 47 | strokeLinecap: StrokeLinecap 48 | 49 | // 描边连接线类型 50 | strokeLinejoin: StrokeLinejoin 51 | 52 | // CSS前缀 53 | prefix: string 54 | 55 | // RTL是否开启 56 | rtl: boolean 57 | 58 | // 默认主题 59 | theme: Theme 60 | 61 | // 主题默认颜色 62 | colors: { 63 | outline: { 64 | fill: string 65 | background: string 66 | } 67 | 68 | filled: { 69 | fill: string 70 | background: string 71 | } 72 | 73 | twoTone: { 74 | fill: string 75 | twoTone: string 76 | } 77 | 78 | multiColor: { 79 | outStrokeColor: string 80 | outFillColor: string 81 | innerStrokeColor: string 82 | innerFillColor: string 83 | } 84 | } 85 | } 86 | 87 | // 图标基础属性 88 | export interface IIconBase { 89 | // 图标尺寸大小,默认1em 90 | size?: number | string 91 | 92 | // 描边宽度 93 | strokeWidth?: number 94 | 95 | // 描边端点类型 96 | strokeLinecap?: StrokeLinecap 97 | 98 | // 描边连接线类型 99 | strokeLinejoin?: StrokeLinejoin 100 | 101 | // 默认主题 102 | theme?: Theme 103 | 104 | // 填充色 105 | fill?: string | string[] 106 | } 107 | 108 | // 包裹后的图标属性 109 | export interface IIconProps extends IIconBase { 110 | spin?: boolean 111 | } 112 | 113 | // 包裹后的图标属性 114 | export type IconOptions = ComponentOptions 115 | 116 | // 包裹前的图标渲染器 117 | export type IconRender = (props: ISvgIconProps) => JSX.Element 118 | 119 | // 包裹后的图标 120 | export type Icon = DefineComponent 121 | 122 | // 默认属性 123 | export const DEFAULT_ICON_CONFIGS: IIconConfig = { 124 | size: '1em', 125 | strokeWidth: 4, 126 | strokeLinecap: 'round', 127 | strokeLinejoin: 'round', 128 | rtl: false, 129 | theme: 'outline', 130 | colors: { 131 | outline: { 132 | fill: '#333', 133 | background: 'transparent', 134 | }, 135 | filled: { 136 | fill: '#333', 137 | background: '#FFF', 138 | }, 139 | twoTone: { 140 | fill: '#333', 141 | twoTone: '#2F88FF', 142 | }, 143 | multiColor: { 144 | outStrokeColor: '#333', 145 | outFillColor: '#2F88FF', 146 | innerStrokeColor: '#FFF', 147 | innerFillColor: '#43CCF8', 148 | }, 149 | }, 150 | prefix: 'i', 151 | } 152 | 153 | function guid(): string { 154 | return 'icon-' + (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1) 155 | } 156 | 157 | // 属性转换函数 158 | export function IconConverter(id: string, icon: IIconBase, config: IIconConfig): ISvgIconProps { 159 | const fill = typeof icon.fill === 'string' ? [icon.fill] : icon.fill || [] 160 | const colors: string[] = [] 161 | 162 | const theme: Theme = icon.theme || config.theme 163 | 164 | switch (theme) { 165 | case 'outline': 166 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 167 | colors.push('none') 168 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 169 | colors.push('none') 170 | break 171 | case 'filled': 172 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 173 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 174 | colors.push('#FFF') 175 | colors.push('#FFF') 176 | break 177 | case 'two-tone': 178 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 179 | colors.push(typeof fill[1] === 'string' ? fill[1] : config.colors.twoTone.twoTone) 180 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 181 | colors.push(typeof fill[1] === 'string' ? fill[1] : config.colors.twoTone.twoTone) 182 | break 183 | case 'multi-color': 184 | colors.push(typeof fill[0] === 'string' ? fill[0] : 'currentColor') 185 | colors.push(typeof fill[1] === 'string' ? fill[1] : config.colors.multiColor.outFillColor) 186 | colors.push(typeof fill[2] === 'string' ? fill[2] : config.colors.multiColor.innerStrokeColor) 187 | colors.push(typeof fill[3] === 'string' ? fill[3] : config.colors.multiColor.innerFillColor) 188 | break 189 | } 190 | 191 | return { 192 | size: icon.size || config.size, 193 | strokeWidth: icon.strokeWidth || config.strokeWidth, 194 | strokeLinecap: icon.strokeLinecap || config.strokeLinecap, 195 | strokeLinejoin: icon.strokeLinejoin || config.strokeLinejoin, 196 | colors, 197 | id, 198 | } 199 | } 200 | 201 | const IconContext = Symbol('icon-context') 202 | 203 | // 图标配置Provider 204 | export const IconProvider = (config: IIconConfig): void => { 205 | provide(IconContext, config) 206 | } 207 | 208 | // 图标Wrapper 209 | export function IconWrapper(name: string, rtl: boolean, render: IconRender): Icon { 210 | const options: IconOptions = { 211 | name: 'icon-' + name, 212 | props: ['size', 'strokeWidth', 'strokeLinecap', 'strokeLinejoin', 'theme', 'fill', 'spin'], 213 | setup: (props) => { 214 | const id = guid() 215 | 216 | const ICON_CONFIGS = inject(IconContext, DEFAULT_ICON_CONFIGS) 217 | 218 | return () => { 219 | const { size, strokeWidth, strokeLinecap, strokeLinejoin, theme, fill, spin } = props 220 | 221 | const svgProps = IconConverter( 222 | id, 223 | { 224 | size, 225 | strokeWidth, 226 | strokeLinecap, 227 | strokeLinejoin, 228 | theme, 229 | fill, 230 | }, 231 | ICON_CONFIGS, 232 | ) 233 | 234 | const cls: string[] = [ICON_CONFIGS.prefix + '-icon'] 235 | 236 | cls.push(ICON_CONFIGS.prefix + '-icon' + '-' + name) 237 | 238 | if (rtl && ICON_CONFIGS.rtl) { 239 | cls.push(ICON_CONFIGS.prefix + '-icon-rtl') 240 | } 241 | 242 | if (spin) { 243 | cls.push(ICON_CONFIGS.prefix + '-icon-spin') 244 | } 245 | 246 | return {render(svgProps)} 247 | } 248 | }, 249 | } 250 | 251 | return options as Icon 252 | } 253 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | import type { App, Plugin } from 'vue' 2 | import Button from './button' 3 | import Settings from './settings' 4 | 5 | const components: Record = { 6 | Button, 7 | Settings, 8 | } 9 | 10 | const install = (app: App): void => { 11 | for (const key of Object.keys(components)) { 12 | app.use(components[key]) 13 | } 14 | } 15 | 16 | export default { 17 | ...components, 18 | install, 19 | } 20 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/settings/SettingsContainer.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/settings/SettingsTile.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 70 | 71 | 82 | -------------------------------------------------------------------------------- /packages/renderer/src/components/ui/settings/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { getComponentPrefix } from '../_utils/global-config' 3 | import _Settings from './SettingsContainer.vue' 4 | import _Tile from './SettingsTile.vue' 5 | 6 | const Settings = { 7 | install: (app: App): void => { 8 | const componentPrefix = getComponentPrefix() 9 | app.component(componentPrefix + 'settings', _Settings) 10 | app.component(componentPrefix + 'settings-tile', _Tile) 11 | }, 12 | } 13 | 14 | export default Settings 15 | -------------------------------------------------------------------------------- /packages/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /packages/renderer/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { NetApi } from '~/interfaces' 2 | import type Cookies from '~/common/cookies' 3 | import type Versions from '~/preload/utils/versions' 4 | import type { Logger } from 'winston' 5 | import type { StorageService } from '~/main/services/storage' 6 | 7 | declare global { 8 | interface Window { 9 | // Expose some Api through preload script 10 | ipcRenderer: Electron.IpcRenderer 11 | removeLoading: () => void 12 | app: { 13 | storage: StorageService 14 | cookies: Cookies 15 | versions: Versions 16 | screen: Electron.Screen 17 | currentWindow: Electron.BrowserWindow 18 | net: NetApi 19 | logger: Logger 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/renderer/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from '@/App.vue' 2 | import store from '@/store' 3 | import router from '@/router' 4 | 5 | // windicss 6 | import 'virtual:windi-base.css' 7 | import 'virtual:windi-components.css' 8 | import 'virtual:windi-utilities.css' 9 | // nprogress 10 | import 'nprogress/nprogress.css' 11 | // arco-design 12 | import '@arco-design/web-vue/es/index.less' 13 | // UiComponents 14 | import Ui from '@/components/ui' 15 | // IconComponents 16 | import Icon from '@/components/ui/icon' 17 | import '@/components/ui/icon/runtime/index.less' 18 | // OverlayScrollbars 19 | import 'overlayscrollbars/css/OverlayScrollbars.css' 20 | // global style 21 | import '@/assets/css/global.less' 22 | 23 | // NProgress 24 | import NProgress from 'nprogress' 25 | // VueRequest 26 | import { setGlobalOptions } from 'vue-request' 27 | 28 | createApp(App) 29 | .use(router) 30 | .use(store) 31 | .use(Ui) 32 | .use(Icon) 33 | .mount('#app') 34 | .$nextTick(window.removeLoading) 35 | 36 | // NProgress 37 | NProgress.configure({ easing: 'ease', speed: 200, trickleSpeed: 50, showSpinner: false }) 38 | 39 | // VueRequest 40 | setGlobalOptions({ 41 | manual: true, 42 | errorRetryCount: 2, 43 | debounceInterval: 150, 44 | }) 45 | -------------------------------------------------------------------------------- /packages/renderer/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router' 2 | 3 | const routes: RouteRecordRaw[] = [ 4 | { 5 | path: '/', 6 | name: 'Browser', 7 | component: () => import('@/views/Main.vue'), 8 | meta: { title: '浏览器' }, 9 | children: [ 10 | { 11 | path: 'home', 12 | name: 'Home', 13 | component: () => import('@/views/pages/home/Home.vue'), 14 | meta: { title: 'webmini' }, 15 | }, 16 | { 17 | path: 'home/webnav', 18 | name: 'WebNav', 19 | component: () => import('@/views/pages/home/WebNav.vue'), 20 | meta: { title: '导航' }, 21 | }, 22 | { 23 | path: 'home/plugin', 24 | name: 'Plugin', 25 | component: () => import('@/views/pages/plugin/Plugin.vue'), 26 | meta: { title: '插件市场' }, 27 | }, 28 | { 29 | path: 'home/settings', 30 | name: 'Settings', 31 | component: () => import('@/views/pages/settings/Settings.vue'), 32 | meta: { title: '设置' }, 33 | }, 34 | { 35 | path: 'home/settings/about', 36 | name: 'About', 37 | component: () => import('@/views/pages/settings/About.vue'), 38 | meta: { title: '关于' }, 39 | }, 40 | ], 41 | }, 42 | { 43 | path: '/select-part', 44 | name: 'SelectPart', 45 | component: () => import('@/views/SelectPart.vue'), 46 | meta: { title: '选p窗口' }, 47 | }, 48 | ] 49 | 50 | const router = createRouter({ 51 | history: createWebHashHistory(), 52 | routes, 53 | }) 54 | 55 | router.beforeEach((to) => { 56 | if (to.meta.title) { 57 | document.title = to.meta.title 58 | } 59 | }) 60 | 61 | export default router 62 | -------------------------------------------------------------------------------- /packages/renderer/src/router/types.d.ts: -------------------------------------------------------------------------------- 1 | import 'vue-router' 2 | 3 | declare module 'vue-router' { 4 | interface RouteMeta { 5 | title?: string 6 | transition?: 'slide-right' | 'slide-left' 7 | scrollTop?: number 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/renderer/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore } from './modules/app' 2 | import { useTabsStore } from './modules/tabs' 3 | 4 | const pinia = createPinia() 5 | 6 | export { useAppStore, useTabsStore } 7 | export default pinia 8 | -------------------------------------------------------------------------------- /packages/renderer/src/store/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | import { loadURL } from '@/utils/view' 2 | import { isURI } from '~/common/uri' 3 | import type { AdapterInfo, LocalPluginInfo } from '~/interfaces/plugin' 4 | import { WindowTypeDefault } from '~/interfaces/view' 5 | import { PluginStatus } from '~/interfaces/plugin' 6 | import type { AppConfig, AppStateTypes } from './types' 7 | import { fetchTotalPlugins } from '@/apis/plugin' 8 | import { useRequest } from 'vue-request' 9 | import { IconSettings, IconApps } from '@arco-design/web-vue/es/icon' 10 | 11 | export const useAppStore = defineStore('app', { 12 | state: (): AppStateTypes => ({ 13 | alwaysOnTop: 'on', 14 | title: 'webmini', 15 | windowSize: WindowTypeDefault, 16 | disablePartButton: true, 17 | disableDanmakuButton: true, 18 | autoHideBar: false, 19 | showTopBar: true, 20 | currentWindowID: window.app.currentWindow.id, 21 | windowID: {}, 22 | navigationState: { 23 | canGoBack: false, 24 | canGoForward: false, 25 | }, 26 | localPlugins: [], 27 | totalPlugins: [], 28 | }), 29 | actions: { 30 | /** 31 | * 初始化 32 | */ 33 | async init() { 34 | // console.log('init') 35 | const appDb = await window.ipcRenderer.invoke('db-get', 'appDb') 36 | if (appDb) { 37 | for (const key in appDb.data) { 38 | // @ts-ignore 39 | this.$state[key] = appDb.data[key] 40 | } 41 | } else { 42 | this.saveConfig('windowSize', toRaw(this.windowSize)) 43 | } 44 | }, 45 | /** 46 | * 保存配置 47 | * @param key 配置项 48 | * @param value 配置值 49 | */ 50 | async saveConfig(key: T, value: AppConfig[T]) { 51 | // const oldDb = await window.ipcRenderer.invoke('db-get', 'appDb') 52 | // const newDb = oldDb ? { ...oldDb.data, [key]: value } : { [key]: value } 53 | window.ipcRenderer.invoke('db-put', { 54 | _id: 'appDb', 55 | data: { [key]: value }, 56 | // data: newDb, 57 | }) 58 | }, 59 | updateURL(url: string, tabId: number) { 60 | window.app.logger.info(`updateURL - ${url} - tabId - ${tabId}`, { label: 'appStore' }) 61 | }, 62 | go(value: string, plugin?: LocalPluginInfo) { 63 | let url = value 64 | if (isURI(url)) { 65 | url = value.indexOf('://') === -1 ? `http://${value}` : value 66 | loadURL(plugin, url) 67 | } 68 | }, 69 | /** 70 | * 获取本地插件列表 71 | */ 72 | getLocalPlugins(): void { 73 | window.ipcRenderer.invoke('get-local-plugins').then((localPlugins: LocalPluginInfo[]) => { 74 | localPlugins.push({ 75 | name: 'Router', 76 | displayName: '插件市场', 77 | start: 'Plugin', 78 | icon: shallowRef(IconApps), 79 | version: '', 80 | }) 81 | localPlugins.push({ 82 | name: 'Router', 83 | displayName: '设置', 84 | start: 'Settings', 85 | icon: shallowRef(IconSettings), 86 | version: '', 87 | }) 88 | this.localPlugins = localPlugins 89 | }) 90 | }, 91 | /** 92 | * 获取远程插件列表 93 | * @returns Promise 94 | */ 95 | getTotalPlugins(): Promise { 96 | if (this.localPlugins.length === 0) { 97 | Promise.all([this.getLocalPlugins()]) 98 | } 99 | return new Promise((resolve, reject) => { 100 | const { run } = useRequest(fetchTotalPlugins, { 101 | formatResult: (res) => { 102 | return res.data === undefined ? [] : res.data 103 | }, 104 | onSuccess: (res: AdapterInfo[]) => { 105 | this.totalPlugins = res.map((info) => { 106 | const localPlugin = this.localPlugins.find((p) => p.name === info.name) 107 | if (!localPlugin) { 108 | // 本地不存在 109 | info.local = {} as LocalPluginInfo 110 | info.local.status = undefined 111 | } else { 112 | // 本地存在 113 | info.local = localPlugin 114 | // 可升级 115 | if (info.version !== localPlugin.version) { 116 | info.local.status = PluginStatus.UPGRADE 117 | } 118 | // 防止状态卡在ing中 119 | else if (info.local?.status === PluginStatus.UNINSTALLING) { 120 | info.local.status = PluginStatus.INSTALLING_COMPLETE 121 | } else if (info.local?.status === PluginStatus.INSTALLING) { 122 | info.local.status = undefined 123 | } 124 | } 125 | return info 126 | }) 127 | resolve(this.totalPlugins) 128 | }, 129 | onError(error) { 130 | window.app.logger.error(error) 131 | reject(error) 132 | }, 133 | }) 134 | run() 135 | }) 136 | }, 137 | }, 138 | }) 139 | -------------------------------------------------------------------------------- /packages/renderer/src/store/modules/app/types.ts: -------------------------------------------------------------------------------- 1 | import type { AdapterInfo, LocalPluginInfo } from '~/interfaces/plugin' 2 | import type { WindowType } from '~/interfaces/view' 3 | 4 | export interface AppStateTypes { 5 | alwaysOnTop: 'on' | 'off' | 'playing' 6 | title: string 7 | windowSize: WindowType 8 | disablePartButton: boolean 9 | disableDanmakuButton: boolean 10 | autoHideBar: boolean 11 | showTopBar: boolean 12 | currentWindowID: number 13 | windowID: { mainWindow?: number; selectPartWindow?: number } 14 | navigationState: { 15 | canGoBack: boolean 16 | canGoForward: boolean 17 | } 18 | localPlugins: LocalPluginInfo[] 19 | totalPlugins: AdapterInfo[] 20 | } 21 | export interface AppConfig 22 | extends Omit< 23 | AppStateTypes, 24 | 'title' | 'currentWindowID' | 'windowID' | 'navigationState' | 'localPlugins' | 'totalPlugins' 25 | > { 26 | windowPosition: number[] 27 | } 28 | -------------------------------------------------------------------------------- /packages/renderer/src/store/modules/tabs/index.ts: -------------------------------------------------------------------------------- 1 | import type { TabsStateTypes } from './type' 2 | import { ITab } from './model' 3 | import { useAppStore } from '@/store' 4 | import type { CreateProperties } from '~/interfaces/tabs' 5 | import { cloneDeep, merge } from 'lodash' 6 | 7 | export const useTabsStore = defineStore('tabs', { 8 | state: (): TabsStateTypes => ({ 9 | list: [], 10 | tabId: -1, // webContentsId 11 | }), 12 | actions: { 13 | /** 14 | * 创建新的标签页 15 | * @param options 创建标签页的参数 16 | * @param id 创建的标签页的 webContentsId 17 | * @returns ITab 18 | */ 19 | createTab(options: CreateProperties, id: number): ITab { 20 | const tab = new ITab(options, id) 21 | if (options.index) { 22 | this.list.splice(options.index, 0, tab) 23 | } else { 24 | this.list.push(tab) 25 | } 26 | return tab 27 | }, 28 | /** 29 | * 创建多个标签页 30 | * @param options 创建标签页的参数 31 | * @param ids 创建的标签页的 webContentsId 32 | * @returns Promise 33 | */ 34 | createTabs(options: CreateProperties[], ids: number[]): ITab[] { 35 | const tabs = options.map((option, i) => { 36 | const tab = new ITab(option, ids[i]) 37 | this.list.push(tab) 38 | return tab 39 | }) 40 | return tabs 41 | }, 42 | /** 43 | * 添加标签页 44 | * @param options 添加标签页的参数 45 | * @returns Promise 46 | */ 47 | async addTab(options: CreateProperties): Promise { 48 | const appStore = useAppStore() 49 | const opts = merge({ active: true }, options) 50 | const id: number = await window.ipcRenderer.invoke( 51 | `view-create-${appStore.currentWindowID}`, 52 | cloneDeep(opts), 53 | ) 54 | return this.createTab(opts, id) 55 | }, 56 | /** 57 | * 添加多个标签页 58 | * @param options 添加标签页的参数 59 | * @returns Promise 60 | */ 61 | async addTabs(options: CreateProperties[]): Promise { 62 | const appStore = useAppStore() 63 | for (let i = 0; i < options.length; i++) { 64 | if (i === options.length - 1) { 65 | options[i].active = true 66 | } else { 67 | options[i].active = false 68 | } 69 | } 70 | const ids = await window.ipcRenderer.invoke( 71 | `views-create-${appStore.currentWindowID}`, 72 | cloneDeep(options), 73 | ) 74 | return this.createTabs(options, ids) 75 | }, 76 | /** 77 | * 获取集中标签页 78 | * @returns ITab | undefined 79 | */ 80 | getFocusedTab(): ITab | undefined { 81 | return this.getTabById(this.tabId) 82 | }, 83 | /** 84 | * 获取标签页 85 | * @param tabId 标签页id 86 | * @returns ITab | undefined 87 | */ 88 | getTabById(tabId: number): ITab | undefined { 89 | return this.list.find((tab) => tab.id === tabId) 90 | }, 91 | }, 92 | getters: {}, 93 | }) 94 | -------------------------------------------------------------------------------- /packages/renderer/src/store/modules/tabs/model.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore, useTabsStore } from '@/store' 2 | import { replaceTitle } from '@/utils' 3 | import { callViewMethod } from '@/utils/view' 4 | import NProgress from 'nprogress' // progress bar 5 | import type { CreateProperties } from '~/interfaces/tabs' 6 | import type { LocalPluginInfo } from '~/interfaces/plugin' 7 | 8 | export class ITab { 9 | public id = -1 10 | 11 | public url = '' 12 | 13 | public _title = 'webmini' 14 | 15 | public plugin: LocalPluginInfo | undefined 16 | 17 | public constructor({ url, plugin, active }: CreateProperties, id: number) { 18 | this.plugin = plugin 19 | this.url = url 20 | this.id = id 21 | if (active) { 22 | this.select() 23 | } 24 | } 25 | 26 | /** 27 | * 获取标签页的标题 28 | */ 29 | public get title() { 30 | return this._title 31 | } 32 | 33 | /** 34 | * 设置标签页的标题 35 | */ 36 | public set title(value: string) { 37 | const appStore = useAppStore() 38 | this._title = replaceTitle(value) 39 | appStore.title = document.title = this._title 40 | } 41 | 42 | /** 43 | * 设置标签页的加载状态 44 | */ 45 | public set loading(value: boolean) { 46 | if (value) { 47 | NProgress.start().inc() 48 | } else { 49 | NProgress.done() 50 | } 51 | } 52 | 53 | /** 54 | * 选中标签页 55 | */ 56 | public async select() { 57 | const appStore = useAppStore() 58 | const tabsStore = useTabsStore() 59 | tabsStore.tabId = this.id 60 | await window.ipcRenderer.invoke(`view-select-${appStore.currentWindowID}`, this.id) 61 | } 62 | 63 | /** 64 | * 调用标签页的方法 65 | * @param scope 方法的作用域 66 | * @param args 方法的参数 67 | * @returns Promise 68 | */ 69 | public callViewMethod(scope: string, ...args: any[]): Promise { 70 | return callViewMethod(this.id, scope, ...args) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/renderer/src/store/modules/tabs/type.ts: -------------------------------------------------------------------------------- 1 | import type { ITab } from './model' 2 | 3 | export interface TabsStateTypes { 4 | list: ITab[] 5 | tabId: number 6 | } 7 | -------------------------------------------------------------------------------- /packages/renderer/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { WatchStopHandle } from 'vue' 2 | import { useAppStore } from '@/store' 3 | import { WindowTypeEnum } from '~/interfaces/view' 4 | import { Timer } from '~/common/timer' 5 | import { replaceAll } from '~/common/string' 6 | 7 | const { screen, currentWindow, logger } = window.app 8 | 9 | export const currentWindowType = ref(WindowTypeEnum.MOBILE) 10 | 11 | export const resizeMainWindow = (windowType?: WindowTypeEnum): void => { 12 | const appStore = useAppStore() 13 | window.ipcRenderer.invoke(`resize-window-size-${appStore.currentWindowID}`, windowType) 14 | } 15 | 16 | // todo 移动到插件里 使用hook 17 | export const replaceTitle = (title: string): string => { 18 | title = replaceAll( 19 | title, 20 | [ 21 | '_哔哩哔哩_bilibili', 22 | '-高清正版在线观看-bilibili-哔哩哔哩', 23 | ' - 哔哩哔哩弹幕视频网 - ( ゜- ゜)つロ 乾杯~', 24 | '哔哩哔哩 (゜-゜)つロ 干杯~-', 25 | ], 26 | '', 27 | ) 28 | return title 29 | } 30 | 31 | /** 32 | * 监测鼠标进入离开窗口, 显示隐藏 topbar 33 | */ 34 | export const initMouseStateDirtyCheck = (): void => { 35 | const appStore = useAppStore() 36 | 37 | const Fn = () => { 38 | const { x, y } = screen.getCursorScreenPoint() 39 | const [posX, posY] = currentWindow.getPosition() 40 | const windowSize = currentWindow.getSize() 41 | 42 | const getTriggerAreaWidth = () => { 43 | return 0 44 | // return lastStatus === 'IN' ? 0 : 16 45 | } 46 | 47 | const getTriggerAreaHeight = () => { 48 | const h = 0.1 * windowSize[1] 49 | const minHeight = appStore.showTopBar ? 120 : 32 50 | return h > minHeight ? h : minHeight 51 | } 52 | 53 | const isMouseInWindow = 54 | x > posX && 55 | x < posX + windowSize[0] - getTriggerAreaWidth() && 56 | y > posY - 10 && 57 | y < posY + getTriggerAreaHeight() 58 | 59 | if (isMouseInWindow !== appStore.showTopBar) { 60 | appStore.showTopBar = isMouseInWindow 61 | } 62 | } 63 | 64 | const time = new Timer(Fn, 150, { mode: 'interval' }) 65 | 66 | watch( 67 | () => appStore.autoHideBar, 68 | (value) => { 69 | logger.debug(`watchEffect - autoHideBar - ${value}`, { label: 'Main.vue' }) 70 | 71 | time.clear() 72 | 73 | if (value) { 74 | time.start() 75 | } else { 76 | appStore.showTopBar = true 77 | } 78 | }, 79 | ) 80 | 81 | watch( 82 | () => [appStore.autoHideBar, appStore.showTopBar], 83 | () => { 84 | window.ipcRenderer.invoke(`top-bar-status-${appStore.currentWindowID}`, { 85 | autoHideBar: appStore.autoHideBar, 86 | showTopBar: appStore.showTopBar, 87 | }) 88 | }, 89 | ) 90 | } 91 | 92 | const watchWindowType = (): WatchStopHandle => { 93 | return watch( 94 | () => currentWindowType.value, 95 | (value) => { 96 | if (value === WindowTypeEnum.MINI) { 97 | return currentWindow.setAlwaysOnTop(true) 98 | } 99 | currentWindow.setAlwaysOnTop(false) 100 | }, 101 | { 102 | immediate: true, 103 | }, 104 | ) 105 | } 106 | 107 | export const watchAlwaysOnTop = (): void => { 108 | const appStore = useAppStore() 109 | 110 | let stopWatchWindowType: WatchStopHandle | null 111 | 112 | watchEffect(() => { 113 | if (stopWatchWindowType) { 114 | stopWatchWindowType() 115 | stopWatchWindowType = null 116 | } 117 | switch (appStore.alwaysOnTop) { 118 | case 'on': 119 | currentWindow.setAlwaysOnTop(true) 120 | break 121 | case 'off': 122 | currentWindow.setAlwaysOnTop(false) 123 | break 124 | default: 125 | stopWatchWindowType = watchWindowType() 126 | break 127 | } 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /packages/renderer/src/utils/ipc.ts: -------------------------------------------------------------------------------- 1 | import { useAppStore, useTabsStore } from '@/store' 2 | import { callViewMethod } from '@/utils/view' 3 | import type { TabEvent, CreateProperties } from '~/interfaces/tabs' 4 | import type { AppStateTypes } from '@/store/modules/app/types' 5 | import { currentWindowType } from '@/utils' 6 | 7 | export const ipcRendererOn = (): void => { 8 | const appStore = useAppStore() 9 | const tabsStore = useTabsStore() 10 | 11 | // browser事件 12 | window.ipcRenderer.on('tab-event', (ev, event: TabEvent, tabId, args) => { 13 | const tab = tabsStore.getTabById(tabId) 14 | if (tab) { 15 | if (event === 'title-updated') tab.title = args[0] 16 | if (event === 'loading') tab.loading = args[0] 17 | if (event === 'url-updated') { 18 | const [url] = args 19 | tab.url = url 20 | appStore.updateURL(url, tabId) 21 | } 22 | } 23 | }) 24 | 25 | // 设置主题 26 | window.ipcRenderer.on('setThemeColor', (ev, theme) => { 27 | document.body.style.setProperty('--theme-color-bg', theme.bg) 28 | document.body.style.setProperty('--theme-color-text', theme.text) 29 | document.body.setAttribute('arco-theme', theme.theme) 30 | }) 31 | 32 | // navigation state 33 | window.ipcRenderer.on('updateNavigationState', (e, data) => { 34 | appStore.navigationState = data 35 | }) 36 | 37 | // 收到选p消息时跳p 38 | window.ipcRenderer.on('go', (ev, url) => { 39 | appStore.go(url, tabsStore.getFocusedTab()?.plugin) 40 | }) 41 | 42 | // 用户按↑、↓键时,把事件传递到webview里去实现修改音量功能 43 | window.ipcRenderer.on('changeVolume', (ev, arg) => { 44 | callViewMethod(tabsStore.tabId, 'send', 'changeVolume', arg) 45 | // webview.value.send('changeVolume', arg) 46 | }) 47 | 48 | // set-currentWindow-type 49 | window.ipcRenderer.on('set-currentWindow-type', (e, windowType) => { 50 | currentWindowType.value = windowType 51 | }) 52 | 53 | // 按下ESC键 54 | window.ipcRenderer.on('pressEsc', () => { 55 | tabsStore.getFocusedTab()?.callViewMethod('goBack') 56 | }) 57 | 58 | // setAppState 59 | window.ipcRenderer.on( 60 | 'setAppState', 61 | ( 62 | e: Electron.IpcRendererEvent, 63 | key: T, 64 | value: AppStateTypes[T], 65 | ) => { 66 | appStore.$state[key] = value 67 | }, 68 | ) 69 | 70 | window.ipcRenderer.on( 71 | 'create-tab', 72 | (e, options: CreateProperties, isNext: boolean, id: number) => { 73 | const selectedTab = tabsStore.getFocusedTab() 74 | if (isNext && selectedTab) { 75 | const index = tabsStore.list.indexOf(selectedTab) + 1 76 | options.index = index 77 | } 78 | tabsStore.createTab(options, id) 79 | }, 80 | ) 81 | 82 | window.ipcRenderer.on('select-next-tab', () => { 83 | const selectedTab = tabsStore.getFocusedTab() 84 | if (!selectedTab) return 85 | const i = tabsStore.list.indexOf(selectedTab) 86 | const nextTab = tabsStore.list[i + 1] 87 | 88 | if (!nextTab) { 89 | if (tabsStore.list[0]) { 90 | tabsStore.list[0].select() 91 | } 92 | } else { 93 | nextTab.select() 94 | } 95 | }) 96 | 97 | window.ipcRenderer.on('select-previous-tab', () => { 98 | const selectedTab = tabsStore.getFocusedTab() 99 | if (!selectedTab) return 100 | const i = tabsStore.list.indexOf(selectedTab) 101 | const prevTab = tabsStore.list[i - 1] 102 | 103 | if (!prevTab) { 104 | if (tabsStore.list[tabsStore.list.length - 1]) { 105 | tabsStore.list[tabsStore.list.length - 1].select() 106 | } 107 | } else { 108 | prevTab.select() 109 | } 110 | }) 111 | 112 | window.ipcRenderer.send('ipc-init-complete') 113 | } 114 | -------------------------------------------------------------------------------- /packages/renderer/src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosRequestConfig } from 'axios' 2 | 3 | const request = axios.create({ 4 | timeout: 10000, 5 | }) 6 | 7 | // axios实例拦截响应 8 | request.interceptors.response.use( 9 | (response) => { 10 | return response 11 | }, 12 | (error: any) => { 13 | return Promise.reject(error) 14 | }, 15 | ) 16 | 17 | // axios实例拦截请求 18 | request.interceptors.request.use( 19 | (config: AxiosRequestConfig) => { 20 | // 此处对请求进行配置 21 | return config 22 | }, 23 | (error: any) => { 24 | // 对请求错误做些什么 25 | return Promise.reject(error) 26 | }, 27 | ) 28 | 29 | export default request 30 | -------------------------------------------------------------------------------- /packages/renderer/src/utils/view.ts: -------------------------------------------------------------------------------- 1 | import { useTabsStore } from '@/store' 2 | import type { LocalPluginInfo } from '~/interfaces/plugin' 3 | 4 | export const callViewMethod = async ( 5 | webContentsId: number, 6 | method: string, 7 | ...args: any[] 8 | ): Promise => { 9 | if (webContentsId === -1) return 10 | return await window.ipcRenderer.invoke(`web-contents-call`, { 11 | args, 12 | method, 13 | webContentsId, 14 | }) 15 | } 16 | 17 | /** 18 | * 载入url 19 | * @param plugin 插件信息 20 | * @param url 要载入的url 21 | * @param args 要传递的参数 22 | */ 23 | export const loadURL = (plugin: LocalPluginInfo | undefined, url: string, ...args: any[]): void => { 24 | const tabsStore = useTabsStore() 25 | const tab = tabsStore.getFocusedTab() 26 | if (!tab) { 27 | tabsStore.addTabs([{ url, plugin, active: true, ...args }]) 28 | } else { 29 | tab.url = url 30 | tab.plugin = plugin 31 | tab.callViewMethod('loadURL', url, ...args) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/renderer/src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 103 | 104 | 114 | -------------------------------------------------------------------------------- /packages/renderer/src/views/SelectPart.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 102 | 103 | 142 | -------------------------------------------------------------------------------- /packages/renderer/src/views/pages/home/Home.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 63 | -------------------------------------------------------------------------------- /packages/renderer/src/views/pages/home/WebNav.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 129 | -------------------------------------------------------------------------------- /packages/renderer/src/views/pages/plugin/Plugin.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 117 | 118 | 123 | -------------------------------------------------------------------------------- /packages/renderer/src/views/pages/settings/About.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | 35 | 58 | -------------------------------------------------------------------------------- /packages/renderer/src/views/pages/settings/Settings.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 49 | -------------------------------------------------------------------------------- /packages/renderer/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'windicss/helpers' 2 | import plugin from 'windicss/plugin' 3 | 4 | export default defineConfig({ 5 | theme: { 6 | screens: { 7 | '3sm': { max: '350px' }, 8 | '2sm': { min: '350px', max: '640px' }, 9 | sm: { min: '640px', max: '768px' }, 10 | md: { min: '768px', max: '1024px' }, 11 | lg: { min: '1024px', max: '1280px' }, 12 | xl: { min: '1280px', max: '1536px' }, 13 | '2xl': { min: '1536px' }, 14 | }, 15 | }, 16 | plugins: [ 17 | plugin(({ addUtilities }) => { 18 | const newUtilities = { 19 | '.drag': { 20 | '-webkit-app-region': 'drag', 21 | }, 22 | '.no-drag': { 23 | '-webkit-app-region': 'no-drag', 24 | }, 25 | } 26 | addUtilities(newUtilities) 27 | }), 28 | ], 29 | }) 30 | -------------------------------------------------------------------------------- /packages/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "useDefineForClassFields": true, 5 | "lib": ["esnext", "dom"], 6 | "baseUrl": "./", 7 | "paths": { 8 | "@/*": ["src/*"], 9 | "~/*": ["../*"] 10 | } 11 | }, 12 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/renderer/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import vueJsx from '@vitejs/plugin-vue-jsx' 3 | import { resolve } from 'path' 4 | import AutoImport from 'unplugin-auto-import/vite' 5 | import { ArcoResolver } from 'unplugin-vue-components/resolvers' 6 | import Components from 'unplugin-vue-components/vite' 7 | import { defineConfig } from 'vite' 8 | import OptimizationPersist from 'vite-plugin-optimize-persist' 9 | import PkgConfig from 'vite-plugin-package-config' 10 | import WindiCSS from 'vite-plugin-windicss' 11 | import pkg from '../../package.json' 12 | // import { dirResolver, DirResolverHelper } from 'vite-auto-import-resolvers' 13 | 14 | // https://vitejs.dev/config/ 15 | export default defineConfig({ 16 | mode: process.env.NODE_ENV, 17 | root: __dirname, 18 | plugins: [ 19 | // 将包信息文件作为 vite 的配置文件之一,为 vite-plugin-optimize-persist 所用 20 | PkgConfig(), 21 | // 依赖预构建分析,提高大型项目性能 22 | OptimizationPersist(), 23 | // vue 官方插件,用来解析 sfc 24 | vue(), 25 | // 按需加载 26 | Components({ 27 | dts: 'src/components.d.ts', 28 | resolvers: [ArcoResolver({ importStyle: false })], 29 | }), 30 | // tsx 支持 31 | vueJsx(), 32 | // windicss 插件 33 | WindiCSS(), 34 | // api 自动按需引入 35 | AutoImport({ 36 | dts: 'src/auto-imports.d.ts', 37 | // global imports to register 38 | imports: [ 39 | // presets 40 | 'vue', 41 | 'pinia', 42 | 'vue-router', 43 | // custom 44 | { 45 | axios: [ 46 | // default imports 47 | ['default', 'axios'], // import { default as axios } from 'axios', 48 | ], 49 | }, 50 | ], 51 | // Generate corresponding .eslintrc-auto-import.json file. 52 | eslintrc: { 53 | enabled: true, 54 | globalsPropValue: 'readonly', 55 | }, 56 | }), 57 | ], 58 | base: './', 59 | build: { 60 | emptyOutDir: true, 61 | outDir: '../../dist/renderer', 62 | }, 63 | server: { 64 | port: pkg.env.PORT, 65 | }, 66 | resolve: { 67 | alias: { 68 | '@': resolve(__dirname, './src'), 69 | '~': resolve(__dirname, '../'), 70 | }, 71 | }, 72 | css: { 73 | preprocessorOptions: { 74 | less: { 75 | additionalData: `@import "${resolve(__dirname, './src/assets/css/css-variables.less')}";`, 76 | }, 77 | }, 78 | }, 79 | }) 80 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | jsxSingleQuote: true, 4 | jsxBracketSameLine: true, 5 | printWidth: 100, 6 | singleQuote: true, 7 | semi: false, 8 | overrides: [ 9 | { 10 | files: '*.json', 11 | options: { 12 | printWidth: 200, 13 | }, 14 | }, 15 | ], 16 | arrowParens: 'always', 17 | endOfLine: 'auto', 18 | vueIndentScriptAndStyle: true, 19 | trailingComma: 'all', 20 | proseWrap: 'never', 21 | htmlWhitespaceSensitivity: 'strict', 22 | } 23 | -------------------------------------------------------------------------------- /resources/pages/network-error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | 51 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'vite' 2 | 3 | await build({ configFile: 'packages/renderer/vite.config.ts' }) 4 | await build({ configFile: 'packages/preload/vite.config.ts' }) 5 | await build({ configFile: 'packages/inject/vite.config.ts' }) 6 | await build({ configFile: 'packages/main/vite.config.ts' }) 7 | -------------------------------------------------------------------------------- /scripts/watch.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process' 2 | import { createServer, build } from 'vite' 3 | import electron from 'electron' 4 | 5 | const RESET = '\x1b[0m' 6 | const FG_RED = '\x1b[31m' 7 | 8 | // https://github.com/yarnpkg/yarn/issues/5063 9 | function disallowNpm() { 10 | const execPath = process.env.npm_execpath 11 | if (!execPath.includes('yarn')) { 12 | console.log(FG_RED) 13 | console.log(`\twebmini supports only Yarn package manager.`) 14 | console.log(RESET) 15 | console.log( 16 | '\n\tPlease visit https://yarnpkg.com/getting-started/install to find instructions on how to install Yarn.\n', 17 | ) 18 | throw new Error('Invalid package manager') 19 | } 20 | } 21 | 22 | try { 23 | disallowNpm() 24 | } catch (e) { 25 | process.exit(1) 26 | } 27 | 28 | /** 29 | * @type {(server: import('vite').ViteDevServer) => Promise} 30 | */ 31 | function watchMain(server) { 32 | /** 33 | * @type {import('child_process').ChildProcessWithoutNullStreams | null} 34 | */ 35 | let electronProcess = null 36 | const address = server.httpServer.address() 37 | const env = Object.assign(process.env, { 38 | VITE_DEV_SERVER_HOST: address.address, 39 | VITE_DEV_SERVER_PORT: address.port, 40 | }) 41 | 42 | return build({ 43 | configFile: 'packages/main/vite.config.ts', 44 | mode: 'development', 45 | plugins: [ 46 | { 47 | name: 'electron-main-watcher', 48 | writeBundle() { 49 | electronProcess && electronProcess.kill() 50 | electronProcess = spawn(electron, ['.'], { stdio: 'inherit', env }) 51 | }, 52 | }, 53 | ], 54 | build: { 55 | watch: true, 56 | }, 57 | }) 58 | } 59 | 60 | /** 61 | * @type {(server: import('vite').ViteDevServer) => Promise} 62 | */ 63 | function watchPreload(server) { 64 | return build({ 65 | configFile: 'packages/preload/vite.config.ts', 66 | mode: 'development', 67 | plugins: [ 68 | { 69 | name: 'electron-preload-watcher', 70 | writeBundle() { 71 | server.ws.send({ type: 'full-reload' }) 72 | }, 73 | }, 74 | ], 75 | build: { 76 | watch: true, 77 | }, 78 | }) 79 | } 80 | 81 | /** 82 | * @type {(server: import('vite').ViteDevServer) => Promise} 83 | */ 84 | function watchInject(server) { 85 | return build({ 86 | configFile: 'packages/inject/vite.config.ts', 87 | mode: 'development', 88 | plugins: [ 89 | { 90 | name: 'electron-inject-watcher', 91 | writeBundle() { 92 | server.ws.send({ type: 'full-reload' }) 93 | }, 94 | }, 95 | ], 96 | build: { 97 | watch: true, 98 | }, 99 | }) 100 | } 101 | 102 | // bootstrap 103 | const server = await createServer({ 104 | configFile: 'packages/renderer/vite.config.ts', 105 | }) 106 | 107 | await server.listen() 108 | await watchPreload(server) 109 | await watchInject(server) 110 | await watchMain(server) 111 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | customSyntax: 'postcss-less', 4 | plugins: ['stylelint-order', 'stylelint-config-rational-order/plugin'], 5 | extends: ['stylelint-config-standard', 'stylelint-config-rational-order'], 6 | overrides: [ 7 | { 8 | files: ['*.html', '**/*.html'], 9 | extends: ['stylelint-config-html/html'], 10 | customSyntax: 'postcss-html', 11 | }, 12 | { 13 | files: ['*.vue', '**/*.vue'], 14 | extends: ['stylelint-config-recommended-vue'], 15 | customSyntax: 'postcss-html', 16 | }, 17 | ], 18 | rules: { 19 | 'no-empty-source': null, 20 | 'keyframes-name-pattern': null, 21 | 'color-no-invalid-hex': true, 22 | 'declaration-colon-space-after': 'always', 23 | 'declaration-colon-space-before': 'never', 24 | 'shorthand-property-no-redundant-values': true, 25 | 'color-hex-case': 'lower', 26 | 'no-descending-specificity': true, 27 | 'block-closing-brace-newline-before': 'always', 28 | 'declaration-empty-line-before': 'never', 29 | 'font-family-name-quotes': 'always-unless-keyword', 30 | 'no-eol-whitespace': true, 31 | 'no-duplicate-selectors': true, 32 | 'max-empty-lines': 1, 33 | 'at-rule-no-unknown': [ 34 | true, 35 | { 36 | ignoreAtRules: ['tailwind', 'apply', 'variants', 'responsive', 'screen'], 37 | }, 38 | ], 39 | 'order/order': [ 40 | 'dollar-variables', 41 | 'custom-properties', 42 | { 43 | type: 'at-rule', 44 | name: 'include', 45 | }, 46 | 'declarations', 47 | { 48 | type: 'at-rule', 49 | name: 'content', 50 | }, 51 | ], 52 | 'plugin/rational-order': [ 53 | true, 54 | { 55 | 'border-in-box-model': false, 56 | 'empty-line-between-groups': false, 57 | }, 58 | ], 59 | }, 60 | } 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true, 10 | "sourceMap": false, 11 | "baseUrl": "./", 12 | "strict": true, 13 | "allowSyntheticDefaultImports": true, 14 | "skipLibCheck": true, 15 | "lib": ["esnext", "dom"], 16 | "types": ["node", "vite/client"] 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | NODE_ENV: 'development' | 'production' 4 | readonly VITE_DEV_SERVER_HOST: string 5 | readonly VITE_DEV_SERVER_PORT: string 6 | } 7 | } 8 | --------------------------------------------------------------------------------