├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report_en-US.md │ ├── bug_report_zh-CN.md │ ├── feature_request_en-US.md │ └── feature_request_zh-CN.md └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarnrc ├── LICENSE ├── README.md ├── bin ├── index.js ├── main.ts └── scripts │ ├── format.ts │ └── index.ts ├── chrome ├── html │ └── error.html ├── icons │ ├── icon-dark.png │ ├── icon-dev.png │ ├── icon.png │ └── piclist.png └── js │ └── icon.js ├── config.json ├── global.d.ts ├── package.json ├── pnpm-lock.yaml ├── script ├── build.js ├── release.ts └── utils │ └── pack.ts ├── src ├── __test__ │ └── utils.ts ├── actions │ ├── account.ts │ ├── clipper.ts │ └── userPreference.ts ├── common │ ├── backend │ │ ├── clients │ │ │ ├── github │ │ │ │ ├── client.test.ts │ │ │ │ ├── client.ts │ │ │ │ └── types.ts │ │ │ ├── joplin │ │ │ │ ├── LegacyJoplinClient.ts │ │ │ │ ├── basic.ts │ │ │ │ ├── client.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── leanote │ │ │ │ ├── client.test.ts │ │ │ │ ├── client.ts │ │ │ │ └── interface.ts │ │ │ └── siyuan │ │ │ │ ├── client.ts │ │ │ │ └── types.ts │ │ ├── imageHosting │ │ │ ├── baklib │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── github │ │ │ │ ├── form.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── service.ts │ │ │ │ └── type.ts │ │ │ ├── imgur │ │ │ │ ├── form.tsx │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── interface.ts │ │ │ ├── joplin │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── leanote │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── piclist │ │ │ │ ├── form.tsx │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── qcloud │ │ │ │ ├── form.tsx │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── siyuan │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── sm.ms │ │ │ │ ├── form.tsx │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ ├── wiznote │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ │ └── yuque_oauth │ │ │ │ ├── index.ts │ │ │ │ └── service.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ └── services │ │ │ ├── baklib │ │ │ ├── complete.tsx │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── bear │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── buildin │ │ │ ├── index.ts │ │ │ ├── service.ts │ │ │ └── type.ts │ │ │ ├── confluence │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── dida365 │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── flomo │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── flowus │ │ │ ├── index.ts │ │ │ ├── service.ts │ │ │ └── type.ts │ │ │ ├── github │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── github_repository │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── interface.ts │ │ │ ├── joplin │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── leanote │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── memos │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── notion │ │ │ ├── index.ts │ │ │ ├── service.ts │ │ │ └── types.ts │ │ │ ├── obsidian │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── onenote_oauth │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── server_chan │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── siyuan │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── ticktick │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── ulysses │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── webdav │ │ │ ├── form.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── wiznote │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ ├── wolai │ │ │ ├── index.ts │ │ │ ├── service.ts │ │ │ └── type.ts │ │ │ ├── youdao │ │ │ ├── index.ts │ │ │ └── service.ts │ │ │ ├── yuque │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ │ │ └── yuque_oauth │ │ │ ├── form.tsx │ │ │ ├── headerForm.tsx │ │ │ ├── index.ts │ │ │ ├── interface.ts │ │ │ └── service.ts │ ├── blob.ts │ ├── buffer.ts │ ├── chrome │ │ └── storage.ts │ ├── error.ts │ ├── getResource.ts │ ├── hooks │ │ ├── useFilterExtensions.ts │ │ ├── useFilterImageHostingServices.ts │ │ ├── useOriginPermission.ts │ │ └── useVerifiedAccount.tsx │ ├── loading.test.ts │ ├── loading.ts │ ├── locales │ │ ├── antd.ts │ │ ├── data │ │ │ ├── de-DE.json │ │ │ ├── de-DE.ts │ │ │ ├── en-US.json │ │ │ ├── en-US.ts │ │ │ ├── ja-JP.json │ │ │ ├── ja-JP.ts │ │ │ ├── ko-KR.json │ │ │ ├── ko-KR.ts │ │ │ ├── ru-RU.json │ │ │ ├── ru-RU.ts │ │ │ ├── zh-CN.json │ │ │ ├── zh-CN.ts │ │ │ ├── zh-TW.json │ │ │ └── zh-TW.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ └── interface.ts │ ├── matchUrl.test.ts │ ├── matchUrl.ts │ ├── modelTypes │ │ ├── account.ts │ │ ├── clipper.ts │ │ ├── extensions.ts │ │ └── userPreference.ts │ ├── object.ts │ ├── storage │ │ ├── __test__ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ ├── interface.ts │ │ └── typedCommonStorage.ts │ ├── strings.ts │ ├── types.ts │ └── version │ │ ├── index.test.ts │ │ └── index.ts ├── components │ ├── ExtensionCard │ │ ├── index.less │ │ └── index.tsx │ ├── IconFont.tsx │ ├── ImageHostingSelect.less │ ├── ImageHostingSelect.tsx │ ├── LinkRender │ │ └── index.tsx │ ├── RepositorySelect.tsx │ ├── accountItem │ │ ├── index.less │ │ └── index.tsx │ ├── avatar │ │ └── index.tsx │ ├── container │ │ ├── index.less │ │ └── index.tsx │ ├── imageHostingSelectOption │ │ ├── index.less │ │ └── index.tsx │ ├── imagehostingListItem │ │ ├── index.less │ │ └── index.tsx │ ├── section │ │ ├── __snapshots__ │ │ │ └── index.test.tsx.snap │ │ ├── index.less │ │ └── index.tsx │ ├── share │ │ └── index.tsx │ └── userItem │ │ ├── index.less │ │ └── index.tsx ├── config.ts ├── extensions │ ├── common.ts │ ├── contextMenus.ts │ ├── contextMenus │ │ └── saveSelection │ │ │ └── saveSelection.ts │ ├── extensions │ │ ├── bookmark.ts │ │ ├── extensions │ │ │ ├── remove.ts │ │ │ ├── selectTool.ts │ │ │ └── uploadImage.ts │ │ ├── fullPage.ts │ │ ├── qrcode.ts │ │ ├── readability.ts │ │ ├── screenshot.ts │ │ ├── select.ts │ │ └── web-clipper │ │ │ ├── clear.ts │ │ │ ├── copyToClipboard.ts │ │ │ ├── download.ts │ │ │ ├── link.tsx │ │ │ └── pangu.ts │ └── index.ts ├── hooks │ └── useOriginForm.tsx ├── index.html ├── main │ ├── background.worker.ts │ ├── contentScript.main.ts │ └── tool.main.chrome.ts ├── models │ ├── account.ts │ ├── clipper.tsx │ └── userPreference.ts ├── pages │ ├── app.less │ ├── app.tsx │ ├── auth.tsx │ ├── complete │ │ ├── complete.less │ │ └── complete.tsx │ ├── locale.tsx │ ├── plugin │ │ ├── Page.tsx │ │ ├── TextEditor.tsx │ │ └── index.less │ ├── preference │ │ ├── account │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── modal │ │ │ │ ├── createAccountModal.tsx │ │ │ │ ├── editAccountModal.tsx │ │ │ │ └── index.less │ │ ├── base.tsx │ │ ├── changelog │ │ │ └── index.tsx │ │ ├── extensions │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── imageHosting │ │ │ ├── form │ │ │ │ └── addImageHosting.tsx │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ └── privacy │ │ │ └── index.tsx │ └── tool │ │ ├── ClipExtension.tsx │ │ ├── Header.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ └── toolExtensions.tsx ├── service │ ├── common │ │ ├── config.ts │ │ ├── configuration.ts │ │ ├── contentScript.ts │ │ ├── cookie.ts │ │ ├── extension.ts │ │ ├── ipc.ts │ │ ├── locale.ts │ │ ├── permissions.ts │ │ ├── preference.ts │ │ ├── request.ts │ │ ├── storage.ts │ │ ├── tab.ts │ │ └── webRequest.ts │ ├── config │ │ └── browser │ │ │ └── configService.ts │ ├── configuration │ │ ├── common │ │ │ └── generate-local-config.ts │ │ └── configuration.ts │ ├── contentScript │ │ ├── browser │ │ │ └── contentScript │ │ │ │ ├── contentScript.less │ │ │ │ └── contentScript.ts │ │ └── common │ │ │ └── contentScriptIPC.ts │ ├── cookie │ │ ├── background │ │ │ └── cookieService.ts │ │ └── common │ │ │ └── cookieIpc.ts │ ├── extension │ │ └── browser │ │ │ ├── extensionContainer.ts │ │ │ └── extensionService.ts │ ├── ipc │ │ └── browser │ │ │ ├── background-main │ │ │ └── ipcService.ts │ │ │ ├── contentScript │ │ │ └── contentScriptIPCServer.ts │ │ │ └── popup │ │ │ └── ipcClient.ts │ ├── permissions │ │ ├── chrome │ │ │ └── permissionsService.ts │ │ └── common │ │ │ └── permissionsIpc.ts │ ├── preference │ │ └── browser │ │ │ └── preferenceService.ts │ ├── request │ │ ├── common │ │ │ ├── request.test.ts │ │ │ └── request.ts │ │ └── tool │ │ │ └── basic.ts │ ├── tab │ │ ├── browser │ │ │ └── background │ │ │ │ └── tabService.ts │ │ └── common │ │ │ └── tabIpc.ts │ ├── webRequest │ │ ├── browser │ │ │ └── background │ │ │ │ └── tabService.ts │ │ ├── chrome │ │ │ └── background │ │ │ │ └── tabService.ts │ │ └── common │ │ │ └── webRequestIPC.ts │ └── worker │ │ ├── common │ │ ├── index.ts │ │ └── workserServiceIPC.ts │ │ └── worker │ │ └── workerService.ts ├── services │ ├── account │ │ └── common.ts │ ├── configuration │ │ └── common │ │ │ ├── configuration.ts │ │ │ └── configurationService.ts │ ├── environment │ │ └── common │ │ │ ├── changelog │ │ │ ├── CHANGELOG.en-US.md │ │ │ └── CHANGELOG.zh-CN.md │ │ │ ├── environment.ts │ │ │ ├── environmentService.ts │ │ │ └── privacy │ │ │ ├── PRIVACY.en-US.md │ │ │ └── PRIVACY.zh-CN.md │ └── log │ │ └── common │ │ └── index.ts ├── setupTests.ts └── vendor │ └── global.d.ts ├── tsconfig.json ├── vitest.config.ts └── webpack ├── plugin └── webpack-create-extension-manifest-plugin.js ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | release 3 | lib 4 | 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | dist/ 3 | coverage/ 4 | node_modules/ 5 | chrome/js/icon.js 6 | releases/ 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@diamondyuan/react-typescript', 'prettier'], 3 | plugins: ['eslint-plugin-prettier'], 4 | rules: { 5 | 'no-use-before-define': 'off', 6 | 'arrow-body-style': 'off', 7 | 'no-redeclare': 'off', 8 | 'prettier/prettier': 'error', 9 | '@typescript-eslint/no-unused-vars': 'off', 10 | }, 11 | settings: { 12 | 'import/resolver': { 13 | webpack: { 14 | config: './webpack/webpack.common.js', 15 | }, 16 | }, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | **Describe the bug** 7 | 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | 12 | Steps to reproduce the behavior: 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected behavior** 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **please complete the following information** 28 | 29 | - Notebook: [e.g. notion,yuque] 30 | - Browser [e.g. chrome, safari] 31 | - Version [e.g. 1.23.0] 32 | 33 | **Additional context** 34 | 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 报告 3 | about: 创建报告以帮助我们改进 4 | --- 5 | 6 | **Bug 描述** 7 | 8 | 清楚简明地描述错误是什么。 9 | 10 | **复现步骤** 11 | 12 | 重现的步骤: 13 | 14 | 1. 打开 '...' 15 | 2. 点击按钮 '....' 16 | 3. 滚动到 '....' 17 | 4. 看到错误 18 | 19 | **预期行为** 20 | 21 | 对您期望发生的事情的简洁明了的描述。 22 | 23 | **截图** 24 | 25 | 如果适用,请添加屏幕截图以帮助解释您的问题。 26 | 27 | **请填写以下信息** 28 | 29 | - 笔记平台: [e.g. notion,yuque] 30 | - 浏览器 [e.g. chrome, safari] 31 | - 版本 [e.g. 1.23.0] 32 | 33 | **其他背景** 34 | 35 | 在此处添加有关该问题的任何其他上下文。 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | --- 5 | 6 | **Is your feature request related to a problem? Please describe.** 7 | 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | 12 | A clear and concise description of what you want to happen. 13 | 14 | **Describe alternatives you've considered** 15 | 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_zh-CN.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求 3 | about: 为这个项目提出一个想法 4 | --- 5 | 6 | **您的功能要求与问题有关吗? 请描述。** 7 | 8 | 清楚,简洁地说明问题所在。 例如 当[...]时,我总是感到沮丧 9 | 10 | **描述您想要的解决方案** 11 | 12 | 对您想要发生的事情的简洁明了的描述。 13 | 14 | **描述您考虑过的替代方案** 15 | 16 | 对您考虑过的所有替代解决方案或功能的简洁明了的描述。 17 | 18 | **其他内容** 19 | 20 | 在此处添加有关功能请求的其他任何上下文或屏幕截图。 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Test 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened, edited, reopened] 6 | push: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-node@v1 20 | with: 21 | node-version: '16.x' 22 | - name: Install Dependencies 23 | run: npm install --force 24 | - run: npm run cov 25 | env: 26 | GITHUB_BRANCH: ${{ github.ref }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | dist/* 4 | !dist/.gitkeep 5 | lib/ 6 | coverage/ 7 | tmp/ 8 | .DS_Store 9 | .idea 10 | .vscode 11 | yarn-error.log 12 | dll/ 13 | webclipper.zip 14 | 15 | .now 16 | release 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "printWidth": 100, 5 | "proseWrap": "never" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "umi_pro.autoGenerateSagaEffectsCommands": true, 3 | "editor.detectIndentation": false, 4 | "eslint.trace.server": "messages", 5 | "eslint.packageManager": "yarn", 6 | "editor.tabSize": 2, 7 | "files.insertFinalNewline": true, 8 | "cSpell.words": ["dida", "hosting", "image", "option", "repos", "ticktick", "yuque"], 9 | "typescript.tsdk": "node_modules/typescript/lib", 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | version-git-message "chore(release): %s :tada:" 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Software License Agreement 2 | Copyright (c) 2020-2020, DiamondYuan. All rights reserved. 3 | 4 | Licensed under the terms of GNU General Public License Version 2 or later. 5 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('ts-node').register({ 3 | transpileOnly: true, 4 | }); 5 | require('./main'); 6 | -------------------------------------------------------------------------------- /bin/main.ts: -------------------------------------------------------------------------------- 1 | import { hideBin } from 'yargs/helpers'; 2 | import { format } from './scripts'; 3 | 4 | const [command] = hideBin(process.argv); 5 | 6 | switch (command) { 7 | case 'format': { 8 | format(); 9 | break; 10 | } 11 | default: { 12 | throw new Error('unknown command'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /bin/scripts/format.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | export function format() { 5 | const localsPath = path.resolve(__dirname, '../../src/common/locales/data'); 6 | const files = fs.readdirSync(localsPath); 7 | 8 | const sortedKeys = Object.keys( 9 | JSON.parse(fs.readFileSync(path.resolve(localsPath, 'en-US.json'), { encoding: 'utf-8' })) 10 | ).sort((a, b) => a.localeCompare(b)); 11 | 12 | files 13 | .filter(file => path.extname(file) === '.json') 14 | .map(file => path.resolve(localsPath, file)) 15 | .forEach(file => { 16 | const messages = JSON.parse(fs.readFileSync(file, 'utf-8')); 17 | const result = {}; 18 | 19 | sortedKeys.forEach(key => { 20 | result[key] = messages[key] || ''; 21 | }); 22 | 23 | fs.writeFileSync(file, JSON.stringify(result, null, 2)); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /bin/scripts/index.ts: -------------------------------------------------------------------------------- 1 | export { format } from './format'; 2 | -------------------------------------------------------------------------------- /chrome/html/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Plugin Installation Notice 8 | 50 | 51 | 52 | 53 |
54 |

Plugin Installation Notice

55 |

56 | After installing the plugin, if you want to use it on pages that were already open before installation, please 57 | refresh the page. 58 |

59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /chrome/icons/icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webclipper/web-clipper/dc978f0154de435824ee3435c5ff4483299f1fdf/chrome/icons/icon-dark.png -------------------------------------------------------------------------------- /chrome/icons/icon-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webclipper/web-clipper/dc978f0154de435824ee3435c5ff4483299f1fdf/chrome/icons/icon-dev.png -------------------------------------------------------------------------------- /chrome/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webclipper/web-clipper/dc978f0154de435824ee3435c5ff4483299f1fdf/chrome/icons/icon.png -------------------------------------------------------------------------------- /chrome/icons/piclist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webclipper/web-clipper/dc978f0154de435824ee3435c5ff4483299f1fdf/chrome/icons/piclist.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "iconfont": "https://at.alicdn.com/t/font_1402208_ghcp6tuu13c.js", 3 | "chromeWebStoreVersion": "1.28.4", 4 | "edgeWebStoreVersion": "1.28.4", 5 | "firefoxWebStoreVersion": "1.28.4", 6 | "privacyLocale": ["en-US", "zh-CN"], 7 | "changelogLocale": ["en-US", "zh-CN"] 8 | } 9 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' { 2 | const src: string; 3 | export default src; 4 | } 5 | -------------------------------------------------------------------------------- /script/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const prodConfig = require('../webpack/webpack.prod'); 3 | const compiler = webpack(prodConfig); 4 | 5 | function send(data) { 6 | if (!process.send) { 7 | return; 8 | } 9 | return new Promise((r) => { 10 | process.send(data, null, {}, r); 11 | }); 12 | } 13 | 14 | compiler.run((err) => { 15 | if (err) { 16 | console.log(err); 17 | } 18 | send({ 19 | type: 'Success', 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /script/release.ts: -------------------------------------------------------------------------------- 1 | import { fork } from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { pack } from './utils/pack'; 5 | 6 | (async () => { 7 | const releaseDir = path.join(__dirname, '../release'); 8 | if (!fs.existsSync(releaseDir)) { 9 | fs.mkdirSync(releaseDir); 10 | } 11 | await build(); 12 | await pack({ 13 | releaseDir, 14 | distDir: path.join(__dirname, '../dist/chrome'), 15 | fileName: 'web-clipper-chrome.zip', 16 | }); 17 | await pack({ 18 | releaseDir, 19 | distDir: path.join(__dirname, '../dist'), 20 | fileName: 'web-clipper-firefox.zip', 21 | }); 22 | const manifestConfig = path.join(__dirname, '../dist/manifest.json'); 23 | const content = fs.readFileSync(manifestConfig, 'utf-8'); 24 | const manifest = JSON.parse(content); 25 | manifest.browser_specific_settings = { 26 | gecko: { 27 | id: '{3fbb1f97-0acf-49a0-8348-36e91bef22ea}', 28 | }, 29 | }; 30 | manifest.name = 'Universal Web Clipper'; 31 | fs.writeFileSync(manifestConfig, JSON.stringify(manifest, null, 2)); 32 | await pack({ 33 | releaseDir, 34 | distDir: path.join(__dirname, '../dist'), 35 | fileName: 'web-clipper-firefox-store.zip', 36 | }); 37 | })(); 38 | 39 | function build() { 40 | const buildScript = require.resolve('./build'); 41 | const buildEnv = Object.create(process.env); 42 | buildEnv.NODE_ENV = 'production'; 43 | const cp = fork(buildScript, [], { 44 | env: buildEnv as unknown as typeof process.env, 45 | stdio: 'inherit', 46 | }); 47 | return new Promise((r) => { 48 | cp.on('message', r); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /script/utils/pack.ts: -------------------------------------------------------------------------------- 1 | import compressing from 'compressing'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | const pump = require('pump'); 5 | interface IPackOptions { 6 | distDir: string; 7 | releaseDir: string; 8 | fileName: string; 9 | } 10 | 11 | export function pack(options: IPackOptions) { 12 | const zipStream = new compressing.zip.Stream(); 13 | const files = fs.readdirSync(options.distDir).filter((p) => !p.match(/^\./)); 14 | for (const file of files) { 15 | zipStream.addEntry(path.join(options.distDir, file)); 16 | } 17 | const dest = path.join(options.releaseDir, options.fileName); 18 | const destStream = fs.createWriteStream(dest); 19 | return new Promise((r) => { 20 | pump(zipStream, destStream, r); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/__test__/utils.ts: -------------------------------------------------------------------------------- 1 | import { IRequestService, TRequestOption } from '@/service/common/request'; 2 | import { vi, Mock } from 'vitest'; 3 | 4 | type TMockRequestServiceHandler = (url: string, options?: TRequestOption) => any; 5 | 6 | export class MockRequestService implements IRequestService { 7 | public mock: { 8 | request: Mock; 9 | }; 10 | private handler: TMockRequestServiceHandler; 11 | constructor(handler: TMockRequestServiceHandler) { 12 | this.mock = { 13 | request: vi.fn(), 14 | }; 15 | this.handler = handler; 16 | } 17 | 18 | request(url: string, options: TRequestOption) { 19 | this.mock.request(url, options); 20 | return this.handler(url, options); 21 | } 22 | 23 | download(url: string): Promise { 24 | return Promise.resolve(new Blob([url])); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/actions/account.ts: -------------------------------------------------------------------------------- 1 | import { UserInfo } from '@/common/backend/services/interface'; 2 | import { AccountPreference } from '@/common/types'; 3 | import { actionCreatorFactory } from 'dva-model-creator'; 4 | 5 | const actionCreator = actionCreatorFactory('account'); 6 | 7 | export const asyncAddAccount = actionCreator.async< 8 | { 9 | id: string; 10 | info: any; 11 | imageHosting?: string; 12 | defaultRepositoryId?: string; 13 | userInfo: UserInfo; 14 | type: string; 15 | callback(): void; 16 | }, 17 | { 18 | accounts: AccountPreference[]; 19 | defaultAccountId: string; 20 | }, 21 | void 22 | >('asyncAddAccount'); 23 | 24 | export const initAccounts = actionCreator.async< 25 | void, 26 | { accounts: AccountPreference[]; defaultAccountId: string } 27 | >('initAccounts'); 28 | 29 | export const asyncDeleteAccount = actionCreator.async< 30 | { id: string }, 31 | { 32 | accounts: AccountPreference[]; 33 | defaultAccountId: string; 34 | }, 35 | void 36 | >('asyncDeleteAccount'); 37 | 38 | export const asyncUpdateDefaultAccountId = actionCreator.async<{ id?: string }, void>( 39 | 'asyncUpdateDefaultAccountId' 40 | ); 41 | 42 | export const asyncUpdateAccount = actionCreator<{ 43 | id: string; 44 | account: { 45 | info: any; 46 | imageHosting?: string; 47 | defaultRepositoryId?: string; 48 | type: string; 49 | }; 50 | newId: string; 51 | userInfo: UserInfo; 52 | callback(): void; 53 | }>('asyncUpdateAccount'); 54 | -------------------------------------------------------------------------------- /src/actions/clipper.ts: -------------------------------------------------------------------------------- 1 | import { ClipperHeaderForm } from 'common/modelTypes/clipper'; 2 | import { Repository, CompleteStatus, CreateDocumentRequest } from 'common/backend/index'; 3 | import { actionCreatorFactory } from 'dva-model-creator'; 4 | 5 | const actionCreator = actionCreatorFactory('clipper'); 6 | 7 | export const asyncCreateDocument = actionCreator.async< 8 | { pathname: string }, 9 | { 10 | result: CompleteStatus; 11 | request: CreateDocumentRequest; 12 | }, 13 | null 14 | >('ASYNC_CREATE_DOCUMENT'); 15 | 16 | export const asyncUploadImage = actionCreator.async('ASYNC_UPLOAD_IMAGE'); 17 | 18 | export const selectRepository = actionCreator<{ repositoryId: string }>('SELECT_REPOSITORY'); 19 | 20 | export const initTabInfo = actionCreator<{ title: string; url: string }>('INIT_TAB_INFO'); 21 | 22 | export const asyncChangeAccount = actionCreator.async< 23 | { 24 | id: string; 25 | }, 26 | { 27 | repositories: Repository[]; 28 | currentImageHostingService?: { type: string }; 29 | }, 30 | any 31 | >('ASYNC_CHANGE_ACCOUNT'); 32 | 33 | export const changeData = actionCreator<{ data: any; pathName: string }>('CHANGE_DATA'); 34 | 35 | export const watchActionChannel = actionCreator('watchActionChannel'); 36 | 37 | export const updateClipperHeader = actionCreator('updateClipperHeader'); 38 | -------------------------------------------------------------------------------- /src/common/backend/clients/github/types.ts: -------------------------------------------------------------------------------- 1 | import { IRequestService } from '@/service/common/request'; 2 | 3 | export interface IGithubClientOptions { 4 | token: string; 5 | request: IRequestService; 6 | } 7 | export interface ICreateIssueOptions { 8 | title: string; 9 | body: string; 10 | labels: string[]; 11 | namespace: string; 12 | } 13 | 14 | export interface ICreateIssueResponse { 15 | html_url: string; 16 | id: number; 17 | } 18 | 19 | export interface IGithubUserInfoResponse { 20 | login: string; 21 | avatar_url: string; 22 | name: string; 23 | bio: string; 24 | html_url: string; 25 | } 26 | 27 | export interface IUploadFileOptions { 28 | owner: string; 29 | repo: string; 30 | path: string; 31 | message: string; 32 | content: string; 33 | branch?: string; 34 | } 35 | 36 | export interface IUploadFileResponse { 37 | content: { 38 | html_url: string; 39 | }; 40 | } 41 | 42 | export interface IListBranchesOptions extends IPageQuery { 43 | owner: string; 44 | repo: string; 45 | protected: boolean; 46 | } 47 | 48 | export interface IBranch { 49 | name: string; 50 | protected: boolean; 51 | } 52 | 53 | export interface IPageQuery { 54 | per_page: number; 55 | page: number; 56 | } 57 | 58 | export type TOmitPage = Omit; 59 | 60 | export type TPageRequest = (option: O) => Promise; 61 | 62 | export interface IGetGithubRepositoryOptions extends IPageQuery { 63 | visibility?: 'all' | 'public' | 'private'; 64 | affiliation?: 'owner' | 'collaborator' | 'organization_member'; 65 | type?: 'all' | 'owner' | 'public' | 'private' | 'member'; 66 | } 67 | 68 | export interface IRepository { 69 | name: string; 70 | /** 71 | *like webclipper/web-clipper 72 | */ 73 | full_name: string; 74 | default_branch: string; 75 | } 76 | -------------------------------------------------------------------------------- /src/common/backend/clients/joplin/LegacyJoplinClient.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from './../../services/interface'; 2 | import { JoplinFolderItem, JoplinTag, JoplinCreateDocumentRequest } from './types'; 3 | import { AbstractJoplinClient } from './basic'; 4 | 5 | export class LegacyJoplinClient extends AbstractJoplinClient { 6 | getRepositories = async () => { 7 | const repositories: Repository[] = []; 8 | const folders = await this.request.get('folders'); 9 | folders.forEach(folder => { 10 | repositories.push({ 11 | id: folder.id, 12 | name: folder.title, 13 | groupId: folder.id, 14 | groupName: folder.title, 15 | }); 16 | if (Array.isArray(folder.children)) { 17 | folder.children.forEach(subFolder => { 18 | repositories.push({ 19 | id: subFolder.id, 20 | name: subFolder.title, 21 | groupId: folder.id, 22 | groupName: folder.title, 23 | }); 24 | }); 25 | } 26 | }); 27 | return repositories; 28 | }; 29 | 30 | getTags = async (filterTags: boolean) => { 31 | let tags = await this.request.get('tags'); 32 | if (filterTags) { 33 | tags = ( 34 | await Promise.all( 35 | tags.map(async tag => { 36 | console.log(this); 37 | const notes = await this.request.get(`tags/${tag.id}/notes`); 38 | if (notes.length === 0) { 39 | return null; 40 | } 41 | return tag; 42 | }) 43 | ) 44 | ).filter((tag): tag is JoplinTag => !!tag); 45 | } 46 | return tags; 47 | }; 48 | 49 | createDocument = async (data: JoplinCreateDocumentRequest) => { 50 | await this.request.post('notes', { 51 | data: { 52 | parent_id: data.repositoryId, 53 | title: data.title, 54 | body: data.content, 55 | tags: data.tags.join(','), 56 | source_url: data.url, 57 | }, 58 | }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/common/backend/clients/joplin/basic.ts: -------------------------------------------------------------------------------- 1 | import { generateUuid } from '@web-clipper/shared/lib/uuid'; 2 | import { IExtendRequestHelper } from '@/service/common/request'; 3 | import { Repository, IJoplinClient, JoplinTag, JoplinCreateDocumentRequest } from './types'; 4 | 5 | export abstract class AbstractJoplinClient implements IJoplinClient { 6 | constructor(protected request: IExtendRequestHelper) {} 7 | 8 | public uploadBlob = async (blob: Blob): Promise => { 9 | let formData = new FormData(); 10 | formData.append('data', blob); 11 | formData.append( 12 | 'props', 13 | JSON.stringify({ 14 | title: generateUuid(), 15 | }) 16 | ); 17 | const result = await this.request.postForm<{ id: string }>(`resources`, { 18 | data: formData, 19 | }); 20 | return `:/${result.id}`; 21 | }; 22 | 23 | abstract getTags(filterTags: boolean): Promise; 24 | abstract getRepositories(): Promise; 25 | abstract createDocument(data: JoplinCreateDocumentRequest): Promise; 26 | } 27 | -------------------------------------------------------------------------------- /src/common/backend/clients/joplin/index.ts: -------------------------------------------------------------------------------- 1 | export { JoplinClient } from './client'; 2 | export { LegacyJoplinClient } from './LegacyJoplinClient'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/common/backend/clients/joplin/types.ts: -------------------------------------------------------------------------------- 1 | import type { Repository, CreateDocumentRequest } from '../../services/interface'; 2 | 3 | export type { Repository } from '../../services/interface'; 4 | export type { CreateDocumentRequest } from '../../services/interface'; 5 | 6 | export interface IJoplinClient { 7 | getTags(filterTags: boolean): Promise; 8 | getRepositories(): Promise; 9 | createDocument(data: JoplinCreateDocumentRequest): Promise; 10 | uploadBlob(blob: Blob): Promise; 11 | } 12 | 13 | export interface JoplinTag { 14 | id: string; 15 | title: string; 16 | } 17 | 18 | export interface JoplinCreateDocumentRequest extends CreateDocumentRequest { 19 | tags: string[]; 20 | } 21 | 22 | export interface JoplinBackendServiceConfig { 23 | token: string; 24 | filterTags: boolean; 25 | } 26 | 27 | export interface JoplinFolderItem { 28 | id: string; 29 | title: string; 30 | children: JoplinFolderItem[]; 31 | } 32 | 33 | export interface JoplinTag { 34 | id: string; 35 | title: string; 36 | } 37 | 38 | export interface IJoplinClient { 39 | getTags(filterTags: boolean): Promise; 40 | getRepositories(): Promise; 41 | createDocument(data: JoplinCreateDocumentRequest): Promise; 42 | } 43 | 44 | export interface JoplinCreateDocumentRequest extends CreateDocumentRequest { 45 | tags: string[]; 46 | } 47 | 48 | export interface IPageRes { 49 | has_more: boolean; 50 | items: T[]; 51 | } 52 | -------------------------------------------------------------------------------- /src/common/backend/clients/leanote/interface.ts: -------------------------------------------------------------------------------- 1 | export interface LeanoteBackendServiceConfig { 2 | leanote_host: string; 3 | email: string; 4 | pwd: string; 5 | token_cached: string; 6 | } 7 | 8 | export interface LeanoteResponse { 9 | Ok: string; 10 | Msg: string; 11 | Token: string; 12 | } 13 | 14 | export interface LeanoteCreateDocumentResponse { 15 | NoteId: string; 16 | } 17 | 18 | export interface LeanoteNotebook { 19 | NotebookId: string; 20 | Title: string; 21 | } 22 | 23 | export interface LeanoteNote { 24 | NotebookId: string; 25 | Title: string; 26 | Content: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/common/backend/clients/siyuan/types.ts: -------------------------------------------------------------------------------- 1 | import { IRequestService } from '@/service/common/request'; 2 | 3 | export interface ISiyuanClientOptions { 4 | request: IRequestService; 5 | accessToken?: string; 6 | } 7 | 8 | export interface ISiyuanUploadImageResponse { 9 | data: { 10 | succMap: { 11 | [key: string]: string; 12 | }; 13 | }; 14 | } 15 | 16 | export interface ISiyuanFetchNotesResponse { 17 | data: { 18 | files?: string[] | { name: string; id: string; closed?: boolean }[]; 19 | notebooks?: string[] | { name: string; id: string; closed?: boolean }[]; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/baklib/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.imageHosting.baklib.name', 9 | defaultMessage: 'Baklib', 10 | }), 11 | icon: 'baklib', 12 | type: 'baklib', 13 | service: Service, 14 | builtIn: true, 15 | builtInRemark: localeService.format({ 16 | id: 'backend.imageHosting.baklib.builtInRemark', 17 | defaultMessage: 'Baklib built in image hosting service.', 18 | }), 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/github/index.ts: -------------------------------------------------------------------------------- 1 | import { ImageHostingServiceMeta } from '../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: 'Github', 8 | icon: 'github', 9 | type: 'github', 10 | form: Form, 11 | service: Service, 12 | permission: { 13 | origins: ['https://api.github.com/*'], 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/github/type.ts: -------------------------------------------------------------------------------- 1 | export interface GithubImageHostingOption { 2 | accessToken: string; 3 | repo: string; 4 | branch?: string; 5 | savePath: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/imgur/form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 3 | import { Form } from '@ant-design/compatible'; 4 | import '@ant-design/compatible/assets/index.less'; 5 | import { Input } from 'antd'; 6 | 7 | interface Props extends FormComponentProps { 8 | info: { 9 | clientId: string; 10 | }; 11 | } 12 | 13 | export default ({ form: { getFieldDecorator }, info }: Props) => { 14 | const initInfo: Partial = info || {}; 15 | return ( 16 | 17 | {getFieldDecorator('clientId', { 18 | initialValue: initInfo.clientId, 19 | rules: [ 20 | { 21 | required: true, 22 | }, 23 | ], 24 | })()} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/imgur/index.ts: -------------------------------------------------------------------------------- 1 | import Form from './form'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: 'Imgur', 8 | icon: 'imgur', 9 | type: 'imgur', 10 | service: Service, 11 | form: Form, 12 | permission: { 13 | origins: ['https://api.imgur.com/*'], 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/imgur/service.ts: -------------------------------------------------------------------------------- 1 | import { IBasicRequestService } from '@/service/common/request'; 2 | import { RequestHelper } from '@/service/request/common/request'; 3 | import { UploadImageRequest, ImageHostingService } from '../interface'; 4 | import { Base64ImageToBlob } from '@/common/blob'; 5 | import Container from 'typedi'; 6 | 7 | export interface ImgurImageHostingOption { 8 | clientId: string; 9 | } 10 | 11 | export default class ImgurImageHostingService implements ImageHostingService { 12 | private config: ImgurImageHostingOption; 13 | 14 | constructor(config: ImgurImageHostingOption) { 15 | this.config = config; 16 | } 17 | 18 | getId = () => { 19 | return this.config.clientId; 20 | }; 21 | 22 | uploadImage = async ({ data }: UploadImageRequest) => { 23 | const blob = Base64ImageToBlob(data); 24 | return this.uploadBlob(blob); 25 | }; 26 | 27 | uploadImageUrl = async (url: string) => { 28 | return this.uploadBlob(url); 29 | }; 30 | 31 | private uploadBlob = async (blob: Blob | string): Promise => { 32 | let formData = new FormData(); 33 | formData.append('image', blob); 34 | const request = new RequestHelper({ request: Container.get(IBasicRequestService) }); 35 | const result = await request.postForm<{ data: { link: string } }>( 36 | `https://api.imgur.com/3/image`, 37 | { 38 | data: formData, 39 | headers: { 40 | Authorization: `Client-ID ${this.config.clientId}`, 41 | }, 42 | } 43 | ); 44 | return result.data.link; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/interface.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from '../services/interface'; 2 | 3 | export interface ImageHostingServiceConstructAble { 4 | new (info: any): ImageHostingService; 5 | } 6 | 7 | export interface ImageHostingService { 8 | getId(): string; 9 | 10 | uploadImage(request: UploadImageRequest): Promise; 11 | 12 | uploadImageUrl(url: string): Promise; 13 | 14 | updateContext?({ currentRepository }: { currentRepository: Repository }): void; 15 | } 16 | 17 | export interface UploadImageRequest { 18 | data: string; 19 | } 20 | 21 | export interface ImageHostingServiceMeta { 22 | name: string; 23 | icon: string; 24 | type: string; 25 | service: ImageHostingServiceConstructAble; 26 | form?: any; 27 | support?: (type: string) => boolean; 28 | builtIn?: boolean; 29 | builtInRemark?: string; 30 | permission?: chrome.permissions.Permissions; 31 | } 32 | 33 | export const BUILT_IN_IMAGE_HOSTING_ID = 'BUILT_IN_IMAGE_HOSTING_ID'; 34 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/joplin/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.imageHosting.joplin.name', 9 | }), 10 | icon: 'joplin', 11 | type: 'joplin', 12 | service: Service, 13 | builtIn: true, 14 | builtInRemark: localeService.format({ 15 | id: 'backend.imageHosting.joplin.builtInRemark', 16 | defaultMessage: 'Joplin built in image hosting service.', 17 | }), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/joplin/service.ts: -------------------------------------------------------------------------------- 1 | import { RequestHelper } from '@/service/request/common/request'; 2 | import { JoplinClient } from './../../clients/joplin/index'; 3 | import { IJoplinClient } from './../../clients/joplin/types'; 4 | import { IBasicRequestService } from '@/service/common/request'; 5 | import { Base64ImageToBlob } from '@/common/blob'; 6 | import { UploadImageRequest, ImageHostingService } from '../interface'; 7 | import Container from 'typedi'; 8 | 9 | export interface JoplinImageHostingOption { 10 | token: string; 11 | } 12 | 13 | export default class JoplinImageHostingService implements ImageHostingService { 14 | private client: IJoplinClient; 15 | private token: string; 16 | 17 | constructor({ token }: JoplinImageHostingOption) { 18 | this.token = token; 19 | const request = new RequestHelper({ 20 | baseURL: 'http://localhost:41184/', 21 | request: Container.get(IBasicRequestService), 22 | params: { 23 | token: token, 24 | }, 25 | }); 26 | this.client = new JoplinClient(request); 27 | } 28 | 29 | getId() { 30 | return this.token; 31 | } 32 | 33 | uploadImage = async ({ data }: UploadImageRequest) => { 34 | const blob = Base64ImageToBlob(data); 35 | return this.client.uploadBlob(blob); 36 | }; 37 | 38 | uploadImageUrl = async (url: string) => { 39 | let blob: Blob = await Container.get(IBasicRequestService).download(url); 40 | if (blob.type === 'image/webp') { 41 | blob = blob.slice(0, blob.size, 'image/jpeg'); 42 | } 43 | return this.client.uploadBlob(blob); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/leanote/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.imageHosting.leanote.name', 9 | }), 10 | icon: 'leanote', 11 | type: 'leanote', 12 | service: Service, 13 | builtIn: true, 14 | builtInRemark: localeService.format({ 15 | id: 'backend.imageHosting.leanote.builtInRemark', 16 | }), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/leanote/service.ts: -------------------------------------------------------------------------------- 1 | import { IBasicRequestService } from '@/service/common/request'; 2 | import { Base64ImageToBlob } from '@/common/blob'; 3 | import { UploadImageRequest, ImageHostingService } from '../interface'; 4 | import backend from '../..'; 5 | import { message } from 'antd'; 6 | import localeService from '@/common/locales'; 7 | import Container from 'typedi'; 8 | 9 | /** 10 | * Use leanote as image hosting service by embbeding images to note body 11 | */ 12 | export default class LeanoteImageHostingService implements ImageHostingService { 13 | getId = () => { 14 | return 'leanote'; 15 | }; 16 | 17 | uploadImage = async ({ data }: UploadImageRequest) => { 18 | const blob = Base64ImageToBlob(data); 19 | return this.uploadBlob(blob); 20 | }; 21 | 22 | uploadImageUrl = async (url: string) => { 23 | let blob: Blob = await Container.get(IBasicRequestService).download(url); 24 | if (blob.type === 'image/webp') { 25 | blob = blob.slice(0, blob.size, 'image/jpeg'); 26 | } 27 | return this.uploadBlob(blob); 28 | }; 29 | 30 | /** 31 | * Delegate image saving to document service 32 | * 33 | * @param blob 34 | * 35 | * @return string image url once hosted 36 | */ 37 | private uploadBlob = async (blob: Blob): Promise => { 38 | message.destroy(); 39 | message.warning( 40 | localeService.format({ 41 | id: 'backend.services.leanote.warning.image.host.saving.delayed', 42 | defaultMessage: 'Image will be attached only if the current clipping is saved', 43 | }) 44 | ); 45 | return (backend.getDocumentService()! as any).uploadBlob(blob); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/piclist/form.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 3 | import { Form } from '@ant-design/compatible'; 4 | import '@ant-design/compatible/assets/index.less'; 5 | import { Input } from 'antd'; 6 | 7 | interface Props extends FormComponentProps { 8 | info: { 9 | uploadUrl: string; 10 | key: string; 11 | }; 12 | } 13 | 14 | export default ({ form: { getFieldDecorator }, info }: Props) => { 15 | const initInfo: Partial = info || {}; 16 | return ( 17 | 18 | 19 | {getFieldDecorator('uploadUrl', { 20 | initialValue: initInfo.uploadUrl, 21 | rules: [ 22 | { 23 | required: true, 24 | }, 25 | ], 26 | })()} 27 | 28 | 29 | {getFieldDecorator('key', { 30 | initialValue: initInfo.key, 31 | })()} 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/piclist/index.ts: -------------------------------------------------------------------------------- 1 | import Form from './form'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: 'piclist', 8 | icon: 'icons/piclist.png', 9 | type: 'piclist', 10 | service: Service, 11 | form: Form, 12 | permission: { 13 | origins: [''], // often to be self-hosted 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/qcloud/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | import form from './form'; 5 | 6 | export default (): ImageHostingServiceMeta => { 7 | return { 8 | name: localeService.format({ 9 | id: 'backend.services.qcloud.name', 10 | }), 11 | icon: 'qcloud', 12 | type: 'qcloud', 13 | form, 14 | service: Service, 15 | permission: { 16 | origins: ['https://*.myqcloud.com/*'], 17 | }, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/siyuan/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.imageHosting.siyuan.name', 9 | }), 10 | icon: 'siyuan', 11 | type: 'siyuan', 12 | service: Service, 13 | builtIn: true, 14 | builtInRemark: localeService.format({ 15 | id: 'backend.imageHosting.siyuan.builtInRemark', 16 | defaultMessage: 'Siyuan Note built in image hosting service.', 17 | }), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/siyuan/service.ts: -------------------------------------------------------------------------------- 1 | import { Base64ImageToBlob } from '@/common/blob'; 2 | import { Container } from 'typedi'; 3 | import { IBasicRequestService } from './../../../../service/common/request'; 4 | import { SiYuanClient } from './../../clients/siyuan/client'; 5 | import { UploadImageRequest, ImageHostingService } from '../interface'; 6 | import { Repository } from '../../services/interface'; 7 | export interface YuqueImageHostingOption { 8 | access_token: string; 9 | } 10 | 11 | export default class SiYuanImageHostingService implements ImageHostingService { 12 | private context: { currentRepository: Repository } | null = null; 13 | private siyuan: SiYuanClient; 14 | constructor(config: { accessToken?: string }) { 15 | this.siyuan = new SiYuanClient({ 16 | request: Container.get(IBasicRequestService), 17 | accessToken: config.accessToken, 18 | }); 19 | } 20 | 21 | getId() { 22 | return 'siyuan'; 23 | } 24 | 25 | uploadImage = async ({ data }: UploadImageRequest) => { 26 | const blob = Base64ImageToBlob(data); 27 | return this.siyuan.uploadImage(blob); 28 | }; 29 | 30 | uploadImageUrl = async (url: string) => { 31 | let blob: Blob = await Container.get(IBasicRequestService).download(url); 32 | if (blob.type === 'image/webp') { 33 | blob = blob.slice(0, blob.size, 'image/jpeg'); 34 | } 35 | return this.siyuan.uploadImage(blob); 36 | }; 37 | 38 | updateContext = (context: { currentRepository: Repository }) => { 39 | this.context = context; 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/sm.ms/form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 3 | import { Form } from '@ant-design/compatible'; 4 | import '@ant-design/compatible/assets/index.less'; 5 | import { Input } from 'antd'; 6 | 7 | interface Props extends FormComponentProps { 8 | info: { 9 | secretToken: string; 10 | }; 11 | } 12 | 13 | export default ({ form: { getFieldDecorator }, info }: Props) => { 14 | const initInfo: Partial = info || {}; 15 | return ( 16 | 17 | {getFieldDecorator('secretToken', { 18 | initialValue: initInfo.secretToken, 19 | })()} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/sm.ms/index.ts: -------------------------------------------------------------------------------- 1 | import { ImageHostingServiceMeta } from '../interface'; 2 | import Service from './service'; 3 | import form from './form'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: 'sm.ms', 8 | icon: 'smms', 9 | type: 'sm.ms', 10 | form, 11 | service: Service, 12 | permission: { 13 | origins: ['https://sm.ms/*'], 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/wiznote/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.imageHosting.wiznote.name', 9 | }), 10 | icon: 'wiznote', 11 | type: 'WizNote', 12 | service: Service, 13 | builtIn: true, 14 | builtInRemark: localeService.format({ 15 | id: 'backend.imageHosting.wiznote.builtInRemark', 16 | }), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/wiznote/service.ts: -------------------------------------------------------------------------------- 1 | import { UploadImageRequest, ImageHostingService } from '../interface'; 2 | import { Base64ImageToBlob } from 'common/blob'; 3 | import Container from 'typedi'; 4 | import { IBasicRequestService } from '@/service/common/request'; 5 | import backend from 'common/backend'; 6 | import WizNoteDocumentService from 'common/backend/services/wiznote/service'; 7 | 8 | export interface WizImageHostingOption { 9 | token: string; 10 | } 11 | 12 | export default class WizNoteImageHostingService implements ImageHostingService { 13 | getId() { 14 | return 'wiznote'; 15 | } 16 | 17 | uploadImage = async ({ data }: UploadImageRequest) => { 18 | const blob = Base64ImageToBlob(data); 19 | return this.uploadBlob(blob); 20 | }; 21 | 22 | uploadImageUrl = async (url: string) => { 23 | let blob: Blob = await Container.get(IBasicRequestService).download(url); 24 | if (blob.type === 'image/webp') { 25 | blob = blob.slice(0, blob.size, 'image/jpeg'); 26 | } 27 | return this.uploadBlob(blob); 28 | }; 29 | 30 | private uploadBlob = async (blob: Blob): Promise => { 31 | return (backend.getDocumentService()! as WizNoteDocumentService).uploadBlob(blob); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/common/backend/imageHosting/yuque_oauth/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ImageHostingServiceMeta } from '../interface'; 3 | import Service from './service'; 4 | 5 | export default (): ImageHostingServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.imageHosting.yuque_oauth.name', 9 | }), 10 | icon: 'yuque', 11 | type: 'yuque_oauth', 12 | service: Service, 13 | builtIn: true, 14 | builtInRemark: localeService.format({ 15 | id: 'backend.imageHosting.yuque_oauth.builtInRemark', 16 | }), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/backend/interface.ts: -------------------------------------------------------------------------------- 1 | export * from './imageHosting/interface'; 2 | export * from './services/interface'; 3 | -------------------------------------------------------------------------------- /src/common/backend/services/baklib/complete.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default ({ status: { edit_url } }: any) => { 4 | return ( 5 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/common/backend/services/baklib/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from './../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | import localeService from '@/common/locales'; 5 | import headerForm from './headerForm'; 6 | import complete from './complete'; 7 | 8 | export default (): ServiceMeta => { 9 | return { 10 | name: localeService.format({ 11 | id: 'backend.services.baklib.name', 12 | }), 13 | complete, 14 | icon: 'baklib', 15 | type: 'baklib', 16 | service: Service, 17 | form: Form, 18 | headerForm, 19 | homePage: 'https://www.baklib.com/', 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/common/backend/services/baklib/interface.ts: -------------------------------------------------------------------------------- 1 | export interface BaklibBackendServiceConfig { 2 | accessToken: string; 3 | origin: string; 4 | } 5 | 6 | export interface BaklibTenantsResponse { 7 | current_tenants: { id: string; name: string; member_role: string[] }[]; 8 | share_tenants: { id: string; name: string; member_role: string[] }[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/backend/services/bear/form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | export default () => ( 5 |
6 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /src/common/backend/services/bear/index.ts: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | import Form from './form'; 3 | 4 | export default () => { 5 | return { 6 | name: 'Bear', 7 | icon: 'bear', 8 | type: 'bear', 9 | service: Service, 10 | form: Form, 11 | homePage: 'https://bear.app/', 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/common/backend/services/bear/service.ts: -------------------------------------------------------------------------------- 1 | import { CompleteStatus } from 'common/backend/interface'; 2 | import { DocumentService, CreateDocumentRequest } from '../../index'; 3 | 4 | export default class GithubDocumentService implements DocumentService { 5 | getId = () => { 6 | return 'bear'; 7 | }; 8 | 9 | getUserInfo = async () => { 10 | return { 11 | name: 'BEAR', 12 | avatar: '', 13 | homePage: 'bear://x-callback-url/search', 14 | description: 'Bear app', 15 | }; 16 | }; 17 | 18 | getRepositories = async () => { 19 | return [ 20 | { 21 | id: 'bear', 22 | name: 'Bear', 23 | groupId: 'bear', 24 | groupName: 'Bear', 25 | }, 26 | ]; 27 | }; 28 | 29 | createDocument = async (info: CreateDocumentRequest): Promise => { 30 | const url = `bear://x-callback-url/create?title=${encodeURIComponent( 31 | info.title 32 | )}&text=${encodeURIComponent(info.content)}&open_note=no`; 33 | window.location.href = url; 34 | return { 35 | href: `bear://x-callback-url/open-note?title=${info.title}`, 36 | }; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/common/backend/services/buildin/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '@/common/backend'; 2 | import Service from './service'; 3 | 4 | export const buildinOrigin = 'https://buildin.ai'; 5 | 6 | export default (): ServiceMeta => { 7 | return { 8 | name: 'Buildin.AI', 9 | icon: 'https://cdn.buildin.ai/s3-public/8ebf3bb6-08c9-40b1-93d5-6d5c5c2fe49c/logo.svg', 10 | type: 'buildin', 11 | homePage: 'https://buildin.ai/', 12 | service: Service, 13 | permission: { 14 | origins: [`${buildinOrigin}/*`, ''], 15 | permissions: ['cookies'], 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/backend/services/confluence/index.ts: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | import Form from './form'; 3 | 4 | export default () => { 5 | return { 6 | name: 'Confluence', 7 | icon: 'confluence', 8 | type: 'confluence', 9 | service: Service, 10 | form: Form, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/common/backend/services/confluence/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ConfluenceListResult { 2 | results: T[]; 3 | start: number; 4 | limit: number; 5 | size: number; 6 | } 7 | 8 | export interface ConfluenceSpace { 9 | id: number; 10 | name: string; 11 | type: string; 12 | _expandable: { 13 | homepage?: string; 14 | }; 15 | } 16 | 17 | export interface ConfluencePage { 18 | id: string; 19 | title: string; 20 | type: string; 21 | } 22 | 23 | export interface ConfluenceServiceConfig { 24 | origin: string; 25 | spaceId: number; 26 | } 27 | 28 | export interface ConfluenceSpace { 29 | id: number; 30 | name: string; 31 | type: string; 32 | } 33 | 34 | export interface ConfluenceUserInfo { 35 | displayName: string; 36 | profilePicture: { 37 | path: string; 38 | }; 39 | } 40 | 41 | /** 42 | * Response of rest/api/content/:id 43 | */ 44 | export interface ConfluenceSpaceContent { 45 | space: { 46 | key: string; 47 | id: number; 48 | name: string; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/common/backend/services/dida365/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Select } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import backend from '../..'; 7 | import Dida365DocumentService from './service'; 8 | import locale from '@/common/locales'; 9 | import { useFetch } from '@shihengtech/hooks'; 10 | 11 | const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { 12 | const service = backend.getDocumentService() as Dida365DocumentService; 13 | const tagsResponse = useFetch(async () => service.getTags(), [service], { 14 | initialState: { 15 | data: [], 16 | }, 17 | }); 18 | 19 | return ( 20 | 21 | 22 | {getFieldDecorator('tags', { 23 | initialValue: [], 24 | })( 25 | 41 | )} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default HeaderForm; 48 | -------------------------------------------------------------------------------- /src/common/backend/services/dida365/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ServiceMeta } from '@/common/backend'; 3 | import Service from './service'; 4 | import headerForm from './headerForm'; 5 | 6 | export default (): ServiceMeta => { 7 | return { 8 | name: localeService.format({ 9 | id: 'backend.services.dida365.name', 10 | }), 11 | icon: 'dida365', 12 | type: 'dida365', 13 | headerForm, 14 | service: Service, 15 | permission: { 16 | origins: ['https://api.dida365.com/*'], 17 | permissions: [], 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/backend/services/flomo/index.ts: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | 3 | export default () => { 4 | return { 5 | name: 'Flomo', 6 | icon: 'flomo', 7 | type: 'flomo', 8 | service: Service, 9 | homePage: 'https://flomoapp.com/', 10 | permission: { 11 | origins: ['https://flomoapp.com/*'], 12 | permissions: ['cookies'], 13 | }, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/backend/services/flowus/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '@/common/backend'; 2 | import Service from './service'; 3 | 4 | export const flowusOrigin = 'https://flowus.cn'; 5 | 6 | export default (): ServiceMeta => { 7 | return { 8 | name: 'FlowUs息流', 9 | icon: 'https://cdn.flowus.cn/icon.png', 10 | type: 'flowus', 11 | homePage: 'https://flowus.cn/', 12 | service: Service, 13 | permission: { 14 | origins: [`${flowusOrigin}/*`, ''], 15 | permissions: ['cookies'], 16 | }, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/backend/services/github/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Select, Badge } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import backend from '../..'; 7 | import GithubDocumentService from './service'; 8 | import locale from '@/common/locales'; 9 | import { useFetch } from '@shihengtech/hooks'; 10 | 11 | const HeaderForm: React.FC = ({ 12 | form: { getFieldDecorator }, 13 | currentRepository, 14 | }) => { 15 | const service = backend.getDocumentService() as GithubDocumentService; 16 | // eslint-disable-next-line react-hooks/rules-of-hooks 17 | const labelsResponse = useFetch( 18 | async () => { 19 | if (currentRepository) { 20 | return service.getRepoLabels(currentRepository); 21 | } 22 | return []; 23 | }, 24 | [currentRepository, service], 25 | { 26 | initialState: { 27 | data: [], 28 | }, 29 | } 30 | ); 31 | 32 | return ( 33 | 34 | 35 | {getFieldDecorator('labels')( 36 | 52 | )} 53 | 54 | 55 | ); 56 | }; 57 | 58 | export default HeaderForm; 59 | -------------------------------------------------------------------------------- /src/common/backend/services/github/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from './../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | import headerForm from './headerForm'; 5 | 6 | export default () => { 7 | return { 8 | name: 'Github', 9 | icon: 'github', 10 | type: 'github', 11 | service: Service, 12 | form: Form, 13 | headerForm: headerForm, 14 | homePage: 'https://github.com/', 15 | permission: { 16 | origins: ['https://api.github.com/*'], 17 | }, 18 | } as ServiceMeta; 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/backend/services/github/interface.ts: -------------------------------------------------------------------------------- 1 | import { Repository, CreateDocumentRequest } from '../interface'; 2 | 3 | export interface GithubBackendServiceConfig { 4 | accessToken: string; 5 | visibility: string; 6 | } 7 | 8 | export interface GithubCreateDocumentRequest extends CreateDocumentRequest { 9 | labels: string[]; 10 | } 11 | 12 | export interface GithubUserInfoResponse { 13 | avatar_url: string; 14 | name: string; 15 | bio: string; 16 | html_url: string; 17 | } 18 | 19 | export interface GithubRepository extends Repository { 20 | namespace: string; 21 | } 22 | 23 | export interface GithubRepositoryResponse { 24 | id: number; 25 | name: string; 26 | full_name: string; 27 | created_at: string; 28 | description: string; 29 | private: boolean; 30 | } 31 | 32 | export interface GithubLabel { 33 | color: string; 34 | description: string; 35 | name: string; 36 | default: boolean; 37 | } 38 | -------------------------------------------------------------------------------- /src/common/backend/services/github_repository/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | 5 | export default () => { 6 | return { 7 | name: 'Github Repository', 8 | icon: 'github_repository', 9 | type: 'github_repository', 10 | service: Service, 11 | form: Form, 12 | homePage: 'https://github.com/', 13 | permission: { 14 | origins: ['https://api.github.com/*'], 15 | }, 16 | } as ServiceMeta; 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/backend/services/github_repository/interface.ts: -------------------------------------------------------------------------------- 1 | import { Repository, CreateDocumentRequest } from '../interface'; 2 | 3 | export interface GithubBackendServiceConfig { 4 | accessToken: string; 5 | visibility: string; 6 | storageLocation: string; 7 | savePath: string; 8 | } 9 | 10 | export interface GithubCreateDocumentRequest extends CreateDocumentRequest { 11 | labels: string[]; 12 | } 13 | 14 | export interface GithubUserInfoResponse { 15 | avatar_url: string; 16 | name: string; 17 | bio: string; 18 | html_url: string; 19 | } 20 | 21 | export interface GithubRepository extends Repository { 22 | namespace: string; 23 | } 24 | 25 | export interface GithubRepositoryResponse { 26 | id: number; 27 | name: string; 28 | full_name: string; 29 | created_at: string; 30 | description: string; 31 | private: boolean; 32 | } 33 | -------------------------------------------------------------------------------- /src/common/backend/services/joplin/form.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Input, Checkbox } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import { JoplinBackendServiceConfig } from '../../clients/joplin'; 7 | import { FormattedMessage } from 'react-intl'; 8 | 9 | interface FormProps extends FormComponentProps { 10 | verified?: boolean; 11 | info?: JoplinBackendServiceConfig; 12 | } 13 | 14 | const InitForm: React.FC = ({ form: { getFieldDecorator }, info }) => { 15 | return ( 16 | 17 | 18 | {getFieldDecorator('token', { 19 | initialValue: info?.token, 20 | rules: [ 21 | { 22 | required: true, 23 | message: 'Authorization token is required!', 24 | }, 25 | ], 26 | })()} 27 | 28 | }> 29 | {getFieldDecorator('filterTags', { 30 | initialValue: info?.filterTags ?? false, 31 | valuePropName: 'checked', 32 | })( 33 | 34 | 35 | 36 | )} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default InitForm; 43 | -------------------------------------------------------------------------------- /src/common/backend/services/joplin/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Select } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import backend from '../..'; 7 | import { useFetch } from '@shihengtech/hooks'; 8 | import JoplinDocumentService from './service'; 9 | import locale from '@/common/locales'; 10 | 11 | const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { 12 | const service = backend.getDocumentService() as JoplinDocumentService; 13 | const tagResponse = useFetch(async () => service.getTags(), [service], { 14 | initialState: { 15 | data: [], 16 | }, 17 | }); 18 | 19 | return ( 20 | 21 | 22 | {getFieldDecorator('tags', { 23 | initialValue: [], 24 | })( 25 | 40 | )} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default HeaderForm; 47 | -------------------------------------------------------------------------------- /src/common/backend/services/joplin/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from './../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | import localeService from '@/common/locales'; 5 | import headerForm from './headerForm'; 6 | 7 | export default (): ServiceMeta => { 8 | return { 9 | name: localeService.format({ 10 | id: 'backend.services.joplin.name', 11 | }), 12 | icon: 'joplin', 13 | type: 'joplin', 14 | service: Service, 15 | headerForm, 16 | form: Form, 17 | homePage: 'https://joplinapp.org/', 18 | permission: { 19 | origins: ['http://localhost:41184/*'], 20 | }, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/common/backend/services/leanote/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '@/common/backend'; 2 | import localeService from '@/common/locales'; 3 | import Service from './service'; 4 | import form from './form'; 5 | 6 | export default (): ServiceMeta => { 7 | return { 8 | name: localeService.format({ 9 | id: 'backend.services.leanote.name', 10 | defaultMessage: 'Leanote', 11 | }), 12 | icon: 'leanote', 13 | type: 'leanote', 14 | service: Service, 15 | form: form, 16 | homePage: 'https://github.com/leanote/leanote', 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/common/backend/services/memos/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | import localeService from '@/common/locales'; 5 | import headerForm from './headerForm'; 6 | 7 | export default (): ServiceMeta => { 8 | return { 9 | name: localeService.format({ 10 | id: 'backend.services.memos.name', 11 | }), 12 | icon: '', 13 | type: 'memos', 14 | service: Service, 15 | headerForm: headerForm, 16 | form: Form, 17 | homePage: 'https://www.usememos.com/', 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/backend/services/memos/interface.ts: -------------------------------------------------------------------------------- 1 | import { CreateDocumentRequest } from './../interface'; 2 | import locales from '@/common/locales'; 3 | 4 | export const VisibilityType = [ 5 | { label: () => locales.format({ id: 'backend.services.memos.headerForm.VisibilityType.private', defaultMessage: 'private' }), value: 'PRIVATE' }, 6 | { label: () => locales.format({ id: 'backend.services.memos.headerForm.VisibilityType.public', defaultMessage: 'public' }), value: 'PUBLIC' }, 7 | ] as const; 8 | 9 | export type VisibilityType = typeof VisibilityType[number]; 10 | 11 | export interface MemosBackendServiceConfig { 12 | accessToken: string; 13 | origin: string; 14 | } 15 | 16 | export interface MemosUserResponse { 17 | name: string; 18 | username: string; 19 | email: string; 20 | avatarUrl: string; 21 | description: string; 22 | } 23 | 24 | export interface MemosUserInfo { 25 | name: string; 26 | avatar: string; 27 | homePage: string; 28 | description: string; 29 | } 30 | 31 | export interface MemoCreateDocumentRequest extends CreateDocumentRequest { 32 | visibility?: VisibilityType; 33 | tags?: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/common/backend/services/notion/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '@/common/backend'; 2 | import Service from './service'; 3 | 4 | export default (): ServiceMeta => { 5 | return { 6 | name: 'Notion', 7 | icon: 'https://www.notion.so/images/favicon.ico', 8 | type: 'notion', 9 | homePage: 'https://www.notion.so/', 10 | service: Service, 11 | permission: { 12 | origins: ['https://www.notion.so/*'], 13 | permissions: ['cookies'], 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/backend/services/notion/types.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from '../interface'; 2 | 3 | export interface NotionUserContent { 4 | recordMap: { 5 | notion_user: { 6 | [uuid: string]: { 7 | role: string; 8 | value: { 9 | name: string; 10 | id: string; 11 | email: string; 12 | profile_photo: string; 13 | }; 14 | }; 15 | }; 16 | space: { 17 | [id: string]: { 18 | role: string; 19 | value: { 20 | id: string; 21 | name: string; 22 | domain: string; 23 | pages: string[]; 24 | }; 25 | }; 26 | }; 27 | block: { 28 | [uuid: string]: { 29 | role: string; 30 | value: { 31 | id: string; 32 | version: string; 33 | parent_id: string; 34 | type: string; 35 | created_time: number; 36 | properties: { 37 | title: string[][]; 38 | content: string[]; 39 | }; 40 | collection_id: string; 41 | }; 42 | }; 43 | }; 44 | collection: { 45 | [uuid: string]: { 46 | role: string; 47 | value: { 48 | id: string; 49 | version: string; 50 | parent_id: string; 51 | name: string[][]; 52 | }; 53 | }; 54 | }; 55 | }; 56 | } 57 | 58 | export interface RecentPages { 59 | recordMap: { 60 | collection?: { 61 | [uuid: string]: { 62 | role: string; 63 | value: { 64 | id: string; 65 | version: string; 66 | parent_id: string; 67 | name: string[][]; 68 | }; 69 | }; 70 | }; 71 | }; 72 | } 73 | 74 | export interface NotionRepository extends Repository { 75 | pageType: string; 76 | } 77 | -------------------------------------------------------------------------------- /src/common/backend/services/obsidian/form.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 4 | import { Input } from 'antd'; 5 | import React, { Component, Fragment } from 'react'; 6 | import { ObsidianFormConfig } from './interface'; 7 | 8 | interface OneNoteProps { 9 | info?: ObsidianFormConfig; 10 | } 11 | 12 | export default class extends Component { 13 | render() { 14 | const { 15 | form: { getFieldDecorator }, 16 | info, 17 | } = this.props; 18 | let initData: Partial = {}; 19 | if (info) { 20 | initData = info; 21 | } 22 | return ( 23 | 24 | 25 | {getFieldDecorator('vault', { 26 | initialValue: initData.vault, 27 | rules: [ 28 | { 29 | required: true, 30 | message: 'Please input your vault!', 31 | }, 32 | ], 33 | })()} 34 | 35 | 36 | {getFieldDecorator('folder', { 37 | initialValue: initData.folder, 38 | rules: [ 39 | { 40 | required: true, 41 | message: 'Please input the folders you want to save!', 42 | }, 43 | ], 44 | })()} 45 | 46 | 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/common/backend/services/obsidian/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ServiceMeta } from './../interface'; 3 | import Service from './service'; 4 | import From from './form'; 5 | 6 | export default (): ServiceMeta => { 7 | return { 8 | name: localeService.format({ 9 | id: 'backend.services.obsidian.name', 10 | defaultMessage: 'Obsidian', 11 | }), 12 | form: From, 13 | icon: 'obsidian', 14 | type: 'obsidian', 15 | service: Service, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/backend/services/obsidian/interface.ts: -------------------------------------------------------------------------------- 1 | export interface ObsidianFormConfig { 2 | vault: string; 3 | folder: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/backend/services/obsidian/service.ts: -------------------------------------------------------------------------------- 1 | import md5 from '@web-clipper/shared/lib/md5'; 2 | import { CreateDocumentRequest, DocumentService } from '../../index'; 3 | import { ObsidianFormConfig } from './interface'; 4 | import QueryString from 'query-string'; 5 | 6 | export default class ObsidianService implements DocumentService { 7 | constructor(private config: ObsidianFormConfig) {} 8 | getId = () => { 9 | return md5(JSON.stringify(this.config)); 10 | }; 11 | 12 | getUserInfo = async () => { 13 | return { 14 | name: 'Obsidian', 15 | avatar: '', 16 | description: `Vault: ${this.config.vault}`, 17 | }; 18 | }; 19 | 20 | getRepositories = async () => { 21 | const folders = (this.config.folder || '').split('\n').map((folder) => { 22 | return { 23 | id: folder, 24 | name: folder, 25 | groupId: 'obsidian', 26 | groupName: this.config.vault, 27 | }; 28 | }); 29 | return folders; 30 | }; 31 | 32 | createDocument = async (info: CreateDocumentRequest) => { 33 | const file = `${info.repositoryId}/${info.title}`; 34 | window.open( 35 | QueryString.stringifyUrl({ 36 | url: 'obsidian://new', 37 | query: { 38 | silent: true, 39 | vault: this.config.vault, 40 | file, 41 | content: info.content, 42 | }, 43 | }) 44 | ); 45 | return { 46 | href: QueryString.stringifyUrl({ 47 | url: 'obsidian://open', 48 | query: { 49 | vault: this.config.vault, 50 | file, 51 | }, 52 | }), 53 | }; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /src/common/backend/services/onenote_oauth/form.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Input } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Component, Fragment } from 'react'; 6 | import { OneNoteBackendServiceConfig } from './interface'; 7 | 8 | interface OneNoteProps { 9 | verified?: boolean; 10 | info?: OneNoteBackendServiceConfig; 11 | } 12 | 13 | export default class extends Component { 14 | render() { 15 | const { 16 | form: { getFieldDecorator }, 17 | info, 18 | verified, 19 | } = this.props; 20 | 21 | let initData: Partial = {}; 22 | if (info) { 23 | initData = info; 24 | } 25 | let editMode = info ? true : false; 26 | return ( 27 | 28 | 29 | {getFieldDecorator('access_token', { 30 | initialValue: initData.access_token, 31 | rules: [ 32 | { 33 | required: true, 34 | message: 'AccessToken is required!', 35 | }, 36 | ], 37 | })()} 38 | 39 | 40 | {getFieldDecorator('refresh_token', { 41 | initialValue: initData.refresh_token, 42 | rules: [ 43 | { 44 | required: true, 45 | message: 'RefreshToken is required!', 46 | }, 47 | ], 48 | })()} 49 | 50 | 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/common/backend/services/onenote_oauth/index.ts: -------------------------------------------------------------------------------- 1 | import { IConfigService } from '@/service/common/config'; 2 | import { Container } from 'typedi'; 3 | import config from '@/config'; 4 | import { ServiceMeta } from './../interface'; 5 | import Service from './service'; 6 | import localeService from '@/common/locales'; 7 | import { stringify } from 'qs'; 8 | import form from './form'; 9 | 10 | export default (): ServiceMeta => { 11 | const oauthUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${stringify({ 12 | scope: 'Notes.Create User.Read offline_access', 13 | client_id: config.oneNoteClientId, 14 | state: Container.get(IConfigService).id, 15 | response_type: 'code', 16 | response_mode: 'query', 17 | redirect_uri: config.oneNoteCallBack, 18 | })}`; 19 | 20 | return { 21 | name: localeService.format({ 22 | id: 'backend.services.onenote_oauth.name', 23 | defaultMessage: 'OneNote', 24 | }), 25 | icon: 'OneNote', 26 | type: 'onenote_oauth', 27 | service: Service, 28 | oauthUrl, 29 | form: form, 30 | homePage: 'https://products.office.com/en-us/onenote/digital-note-taking-app', 31 | permission: { 32 | origins: ['https://graph.microsoft.com/*', 'https://login.microsoftonline.com/*'], 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/common/backend/services/onenote_oauth/interface.ts: -------------------------------------------------------------------------------- 1 | export interface OneNoteBackendServiceConfig { 2 | refresh_token: string; 3 | access_token: string; 4 | } 5 | 6 | export interface OneNoteNotebooksResponse { 7 | value: { 8 | id: string; 9 | displayName: string; 10 | sections: { 11 | id: string; 12 | displayName: string; 13 | }[]; 14 | }[]; 15 | } 16 | 17 | export interface OneNoteUserInfoResponse { 18 | id: string; 19 | displayName: string; 20 | userPrincipalName: string; 21 | } 22 | 23 | export interface OneNoteCreateDocumentResponse { 24 | id: string; 25 | links: { 26 | oneNoteClientUrl: { 27 | href: string; 28 | }; 29 | oneNoteWebUrl: { 30 | href: string; 31 | }; 32 | }; 33 | } 34 | 35 | export interface OneNoteRefreshTokenResponse { 36 | access_token: string; 37 | refresh_token: string; 38 | } 39 | -------------------------------------------------------------------------------- /src/common/backend/services/server_chan/form.tsx: -------------------------------------------------------------------------------- 1 | import { KeyOutlined } from '@ant-design/icons'; 2 | import { Form } from '@ant-design/compatible'; 3 | import '@ant-design/compatible/assets/index.less'; 4 | import { Input } from 'antd'; 5 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 6 | import React, { Fragment } from 'react'; 7 | import { FormattedMessage } from 'react-intl'; 8 | 9 | interface FormProps extends FormComponentProps { 10 | verified?: boolean; 11 | info?: { 12 | accessToken: string; 13 | }; 14 | } 15 | 16 | const ConfigForm: React.FC = ({ form: { getFieldDecorator }, info, verified }) => { 17 | const disabled = verified || !!info; 18 | let initAccessToken; 19 | if (info) { 20 | initAccessToken = info.accessToken; 21 | } 22 | return ( 23 | 24 | 25 | {getFieldDecorator('accessToken', { 26 | initialValue: initAccessToken, 27 | rules: [ 28 | { 29 | required: true, 30 | message: ( 31 | 35 | ), 36 | }, 37 | ], 38 | })( 39 | 43 | 44 | 45 | } 46 | /> 47 | )} 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default ConfigForm; 54 | -------------------------------------------------------------------------------- /src/common/backend/services/server_chan/index.ts: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | import Form from './form'; 3 | import localeService from '@/common/locales'; 4 | 5 | export default () => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.services.server_chan.name', 9 | }), 10 | icon: 'wechat', 11 | type: 'server_chan', 12 | service: Service, 13 | form: Form, 14 | homePage: 'https://sc.ftqq.com/', 15 | permission: { 16 | origins: ['https://sc.ftqq.com/*'], 17 | }, 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /src/common/backend/services/server_chan/service.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { CompleteStatus } from 'common/backend/interface'; 3 | import { DocumentService, CreateDocumentRequest } from '../../index'; 4 | import request from 'umi-request'; 5 | 6 | export default class GithubDocumentService implements DocumentService { 7 | private config: { accessToken: string }; 8 | 9 | constructor(config: { accessToken: string }) { 10 | this.config = config; 11 | } 12 | 13 | getId = () => { 14 | return this.config.accessToken; 15 | }; 16 | 17 | getUserInfo = async () => { 18 | return { 19 | name: localeService.format({ 20 | id: 'backend.services.server_chan.name', 21 | }), 22 | avatar: '', 23 | homePage: 'https://sc.ftqq.com/', 24 | description: localeService.format({ 25 | id: 'backend.services.server_chan.name', 26 | }), 27 | }; 28 | }; 29 | 30 | getRepositories = async () => { 31 | return [ 32 | { 33 | id: 'server_chan', 34 | name: localeService.format({ 35 | id: 'backend.services.server_chan.name', 36 | }), 37 | groupId: 'server_chan', 38 | groupName: localeService.format({ 39 | id: 'backend.services.server_chan.name', 40 | }), 41 | }, 42 | ]; 43 | }; 44 | 45 | createDocument = async (info: CreateDocumentRequest): Promise => { 46 | const res = await request.post<{ errmsg: string; errno: number }>( 47 | `https://sc.ftqq.com/${this.config.accessToken}.send`, 48 | { 49 | requestType: 'form', 50 | data: { 51 | text: info.title, 52 | desp: info.content, 53 | }, 54 | } 55 | ); 56 | if (res.errno !== 0) { 57 | throw new Error(res.errmsg); 58 | } 59 | return { 60 | href: `http://sc.ftqq.com/`, 61 | }; 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/common/backend/services/siyuan/form.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Input } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import localeService from '@/common/locales'; 7 | 8 | interface SiyuanFormProps { 9 | info?: SiyuanBackendServiceConfig; 10 | } 11 | 12 | interface SiyuanBackendServiceConfig { 13 | accessToken?: string; 14 | } 15 | 16 | const form: React.FC = props => { 17 | const { 18 | form: { getFieldDecorator }, 19 | info, 20 | } = props; 21 | 22 | let initData: Partial = {}; 23 | if (info) { 24 | initData = info; 25 | } 26 | let editMode = info ? true : false; 27 | return ( 28 | 29 | 34 | {getFieldDecorator('accessToken', { 35 | initialValue: initData.accessToken, 36 | })()} 37 | 38 | 39 | ); 40 | }; 41 | 42 | export default form; 43 | -------------------------------------------------------------------------------- /src/common/backend/services/siyuan/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ServiceMeta } from './../interface'; 3 | import Service from './service'; 4 | import form from './form'; 5 | 6 | /** 7 | * @see https://github.com/siyuan-note/siyuan/issues/1266 8 | */ 9 | export default () => { 10 | return { 11 | name: localeService.format({ 12 | id: 'backend.services.siyuan.name', 13 | }), 14 | icon: 'siyuan', 15 | form, 16 | type: 'siyuan', 17 | service: Service, 18 | homePage: 'https://b3log.org/siyuan/', 19 | permission: { 20 | origins: ['http://localhost:6806/*', 'http://127.0.0.1:6806/*'], 21 | }, 22 | } as ServiceMeta; 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/backend/services/siyuan/service.ts: -------------------------------------------------------------------------------- 1 | import { CompleteStatus } from 'common/backend/interface'; 2 | import { Repository, CreateDocumentRequest } from './../interface'; 3 | import { DocumentService } from '../../index'; 4 | import { IBasicRequestService } from '@/service/common/request'; 5 | import { Container } from 'typedi'; 6 | import { SiYuanClient } from '../../clients/siyuan/client'; 7 | import localeService from '@/common/locales'; 8 | 9 | /** 10 | * 11 | * Document service for self hosted leanote or leanote.com 12 | */ 13 | export default class SiYuanDocumentService implements DocumentService { 14 | private client: SiYuanClient; 15 | constructor(config: { accessToken?: string }) { 16 | this.client = new SiYuanClient({ 17 | request: Container.get(IBasicRequestService), 18 | accessToken: config.accessToken, 19 | }); 20 | } 21 | 22 | /** Unique account identification */ 23 | getId = () => { 24 | return 'siyuan'; 25 | }; 26 | 27 | getUserInfo = async () => { 28 | return { 29 | name: 'siyuan', 30 | avatar: '', 31 | homePage: '', 32 | description: ``, 33 | }; 34 | }; 35 | 36 | getRepositories = async () => { 37 | let response = await this.client.listNotebooks(); 38 | return response.map( 39 | ({ name, id }): Repository => { 40 | return { 41 | groupId: 'siyuan', 42 | groupName: localeService.format({ 43 | id: 'backend.services.siyuan.notes', 44 | }), 45 | id, 46 | name, 47 | }; 48 | } 49 | ); 50 | }; 51 | 52 | createDocument = async (data: CreateDocumentRequest): Promise => { 53 | const id = await this.client.createNote(data); 54 | return { 55 | href: `siyuan://blocks/${id}`, 56 | }; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/common/backend/services/ticktick/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Select } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import backend from '../..'; 7 | import Dida365DocumentService from './service'; 8 | import locale from '@/common/locales'; 9 | import { useFetch } from '@shihengtech/hooks'; 10 | 11 | const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { 12 | const service = backend.getDocumentService() as Dida365DocumentService; 13 | const tagsResponse = useFetch(async () => service.getTags(), [service], { 14 | initialState: { 15 | data: [], 16 | }, 17 | }); 18 | 19 | return ( 20 | 21 | 22 | {getFieldDecorator('tags', { 23 | initialValue: [], 24 | })( 25 | 41 | )} 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default HeaderForm; 48 | -------------------------------------------------------------------------------- /src/common/backend/services/ticktick/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import { ServiceMeta } from '@/common/backend'; 3 | import Service from './service'; 4 | import headerForm from './headerForm'; 5 | 6 | export default (): ServiceMeta => { 7 | return { 8 | name: localeService.format({ 9 | id: 'backend.services.ticktick.name', 10 | defaultMessage: 'TickTick', 11 | }), 12 | icon: 'dida365', 13 | type: 'ticktick', 14 | headerForm, 15 | service: Service, 16 | permission: { 17 | origins: ['https://api.ticktick.com/*'], 18 | permissions: [], 19 | }, 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/common/backend/services/ulysses/form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | export default () => ( 5 |
6 | 10 |
11 | ); 12 | -------------------------------------------------------------------------------- /src/common/backend/services/ulysses/index.ts: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | import Form from './form'; 3 | 4 | export default () => { 5 | return { 6 | name: 'Ulysses', 7 | icon: 'ulysses', 8 | type: 'ulysses', 9 | service: Service, 10 | form: Form, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/common/backend/services/ulysses/service.ts: -------------------------------------------------------------------------------- 1 | import { ITabService } from './../../../../service/common/tab'; 2 | import { CompleteStatus } from 'common/backend/interface'; 3 | import { DocumentService, CreateDocumentRequest } from '../../index'; 4 | import Container from 'typedi'; 5 | 6 | export default class GithubDocumentService implements DocumentService { 7 | getId = () => { 8 | return 'ulysses'; 9 | }; 10 | 11 | getUserInfo = async () => { 12 | return { 13 | name: 'Ulysses', 14 | avatar: '', 15 | description: 'Ulysses app', 16 | }; 17 | }; 18 | 19 | getRepositories = async () => { 20 | return [ 21 | { 22 | id: 'ulysses', 23 | name: 'Ulysses', 24 | groupId: 'ulysses', 25 | groupName: 'Ulysses', 26 | }, 27 | ]; 28 | }; 29 | 30 | createDocument = async (info: CreateDocumentRequest): Promise => { 31 | const text = `# ${info.title}\n\n${info.content}`; 32 | const url = `ulysses://x-callback-url/new-sheet?text=${encodeURIComponent(text)}`; 33 | Container.get(ITabService).create({ url }); 34 | return { 35 | href: `ulysses://x-callback-url/open-recent`, 36 | }; 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/common/backend/services/webdav/index.ts: -------------------------------------------------------------------------------- 1 | import Service from './service'; 2 | import Form from './form'; 3 | 4 | export default () => { 5 | return { 6 | name: 'WebDAV', 7 | icon: 'webdav', 8 | type: 'webdav', 9 | service: Service, 10 | form: Form, 11 | homePage: '', 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/common/backend/services/webdav/interface.ts: -------------------------------------------------------------------------------- 1 | export interface WebDAVServiceConfig { 2 | origin: string; 3 | username: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/backend/services/wiznote/form.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Form } from '@ant-design/compatible'; 3 | import '@ant-design/compatible/assets/index.less'; 4 | import { Input } from 'antd'; 5 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 6 | import { WizNoteConfig } from '@/common/backend/services/wiznote/interface'; 7 | import { FormattedMessage } from 'react-intl'; 8 | import useOriginForm from '@/hooks/useOriginForm'; 9 | 10 | interface WizNoteFormProps extends FormComponentProps { 11 | info?: WizNoteConfig; 12 | } 13 | 14 | const WizNoteForm: React.FC = ({ form, info }) => { 15 | const { verified, handleAuthentication, formRules } = useOriginForm({ form, initStatus: !!info }); 16 | return ( 17 | 18 | 21 | } 22 | > 23 | {form.getFieldDecorator('origin', { 24 | initialValue: info?.origin ?? 'https://note.wiz.cn', 25 | rules: formRules, 26 | })( 27 | 33 | } 34 | onSearch={handleAuthentication} 35 | disabled={verified} 36 | /> 37 | )} 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default WizNoteForm; 44 | -------------------------------------------------------------------------------- /src/common/backend/services/wiznote/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Select } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import backend from '../..'; 7 | import { useFetch } from '@shihengtech/hooks'; 8 | import WizNoteDocumentService from './service'; 9 | import locale from '@/common/locales'; 10 | 11 | const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { 12 | const service = backend.getDocumentService() as WizNoteDocumentService; 13 | 14 | const tagResponse = useFetch(async () => service.getTags(), [service], { 15 | initialState: { 16 | data: [], 17 | }, 18 | }); 19 | 20 | return ( 21 | 22 | 23 | {getFieldDecorator('tags', { 24 | initialValue: [], 25 | })( 26 | 42 | )} 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default HeaderForm; 49 | -------------------------------------------------------------------------------- /src/common/backend/services/wiznote/index.ts: -------------------------------------------------------------------------------- 1 | import localeService from '@/common/locales'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | import headerForm from './headerForm'; 5 | 6 | export default () => { 7 | return { 8 | name: localeService.format({ 9 | id: 'backend.services.wiznote.name', 10 | }), 11 | icon: 'wiznote', 12 | type: 'WizNote', 13 | headerForm, 14 | service: Service, 15 | form: Form, 16 | permission: { 17 | permissions: ['cookies'], 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/backend/services/wiznote/interface.ts: -------------------------------------------------------------------------------- 1 | import { CreateDocumentRequest } from '../interface'; 2 | 3 | export interface WizNoteConfig { 4 | origin: string; 5 | spaceId: number; 6 | } 7 | 8 | export interface WizNoteUserInfo { 9 | result: { 10 | email: string; 11 | userGuid: string; 12 | displayName: string; 13 | token: string; 14 | kbGuid: string; 15 | }; 16 | } 17 | 18 | export interface WizNoteCreateDocumentRequest extends CreateDocumentRequest { 19 | tags: string[]; 20 | } 21 | 22 | export interface WizNoteCreateTagResponse { 23 | result: { 24 | tagGuid: string; 25 | }; 26 | } 27 | 28 | export interface WizNoteGetTagsResponse { 29 | result: { 30 | id: string; 31 | name: string; 32 | tagGuid: string; 33 | }[]; 34 | } 35 | 36 | export interface WizNoteGetRepositoriesResponse { 37 | result: string[]; 38 | } 39 | -------------------------------------------------------------------------------- /src/common/backend/services/wolai/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '@/common/backend'; 2 | import Service from './service'; 3 | 4 | export default (): ServiceMeta => { 5 | return { 6 | name: '我来', 7 | icon: 'https://static2.wolai.com/dist/favicon.ico', 8 | type: 'wolai', 9 | homePage: 'https://www.wolai.com/', 10 | service: Service, 11 | permission: { 12 | origins: ['https://api.wolai.com/*'], 13 | permissions: ['cookies'], 14 | }, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/backend/services/youdao/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from '@/common/backend'; 2 | import localeService from '@/common/locales'; 3 | import Service from './service'; 4 | 5 | export default (): ServiceMeta => { 6 | return { 7 | name: localeService.format({ 8 | id: 'backend.services.youdao.name', 9 | defaultMessage: 'Youdao', 10 | }), 11 | icon: 'https://note.youdao.com/web/favicon.ico', 12 | type: 'youdao', 13 | homePage: 'https://note.youdao.com/web/', 14 | service: Service, 15 | permission: { 16 | origins: ['https://note.youdao.com/*'], 17 | permissions: ['cookies'], 18 | }, 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/backend/services/yuque/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Input } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import locales from '@/common/locales'; 7 | 8 | const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { 9 | return ( 10 | 11 | 12 | {getFieldDecorator('slug', { 13 | rules: [ 14 | { 15 | pattern: /^[\w-.]{2,190}$/, 16 | message: locales.format({ 17 | id: 'backend.services.yuque.headerForm.slug_error', 18 | }), 19 | }, 20 | ], 21 | })( 22 | 28 | )} 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default HeaderForm; 35 | -------------------------------------------------------------------------------- /src/common/backend/services/yuque/index.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta } from './../interface'; 2 | import Service from './service'; 3 | import Form from './form'; 4 | import localeService from '@/common/locales'; 5 | import headerForm from './headerForm'; 6 | 7 | export default (): ServiceMeta => { 8 | return { 9 | name: localeService.format({ 10 | id: 'backend.services.yuque.name', 11 | defaultMessage: 'Yuque', 12 | }), 13 | icon: 'yuque', 14 | type: 'yuque', 15 | service: Service, 16 | headerForm: headerForm, 17 | form: Form, 18 | homePage: 'https://www.yuque.com', 19 | permission: { 20 | origins: ['https://www.yuque.com/*'], 21 | }, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/common/backend/services/yuque/interface.ts: -------------------------------------------------------------------------------- 1 | import { CompleteStatus, CreateDocumentRequest, UpdateTOCRequest } from './../interface'; 2 | import { Repository } from '../interface'; 3 | 4 | export enum RepositoryType { 5 | all = 'all', 6 | self = 'self', 7 | group = 'group', 8 | } 9 | 10 | export interface YuqueBackendServiceConfig { 11 | accessToken: string; 12 | repositoryType: RepositoryType; 13 | } 14 | 15 | export interface YuqueUserInfoResponse { 16 | id: number; 17 | avatar_url: string; 18 | name: string; 19 | login: string; 20 | description: string; 21 | } 22 | 23 | export interface YuqueGroupResponse { 24 | id: number; 25 | name: string; 26 | login: string; 27 | } 28 | 29 | export interface YuqueRepository extends Repository { 30 | namespace: string; 31 | } 32 | 33 | export interface YuqueRepositoryResponse { 34 | id: number; 35 | name: string; 36 | namespace: string; 37 | } 38 | 39 | export interface YuqueCreateDocumentResponse { 40 | id: number; 41 | slug: string; 42 | title: string; 43 | created_at: string; 44 | updated_at: string; 45 | } 46 | 47 | export interface YuqueCompleteStatus extends CompleteStatus { 48 | documentId: string; 49 | repositoryId: string; 50 | accessToken: string; 51 | } 52 | 53 | export interface YuqueCreateDocumentRequest extends CreateDocumentRequest { 54 | slug?: string; 55 | } 56 | 57 | export interface YuqueUpdateTOCRequest extends UpdateTOCRequest{ 58 | repositoryId: string; 59 | documentId: number[]; 60 | } 61 | -------------------------------------------------------------------------------- /src/common/backend/services/yuque_oauth/headerForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from '@ant-design/compatible'; 2 | import '@ant-design/compatible/assets/index.less'; 3 | import { Input } from 'antd'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | import React, { Fragment } from 'react'; 6 | import locales from '@/common/locales'; 7 | 8 | const HeaderForm: React.FC = ({ form: { getFieldDecorator } }) => { 9 | return ( 10 | 11 | 12 | {getFieldDecorator('slug', { 13 | rules: [ 14 | { 15 | pattern: /^[\w-.]{2,190}$/, 16 | message: locales.format({ 17 | id: 'backend.services.yuque.headerForm.slug_error', 18 | }), 19 | }, 20 | ], 21 | })( 22 | 28 | )} 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default HeaderForm; 35 | -------------------------------------------------------------------------------- /src/common/backend/services/yuque_oauth/index.ts: -------------------------------------------------------------------------------- 1 | import config from '@/config'; 2 | import { ServiceMeta } from './../interface'; 3 | import Service from './service'; 4 | import localeService from '@/common/locales'; 5 | import { stringify } from 'qs'; 6 | import form from './form'; 7 | import headerForm from './headerForm'; 8 | import { IConfigService } from '@/service/common/config'; 9 | import { Container } from 'typedi'; 10 | 11 | const oauthUrl = `https://www.yuque.com/oauth2/authorize?${stringify({ 12 | client_id: config.yuqueClientId, 13 | scope: config.yuqueScope, 14 | redirect_uri: config.yuqueCallback, 15 | state: Container.get(IConfigService).id, 16 | response_type: 'code', 17 | })}`; 18 | 19 | export default (): ServiceMeta => { 20 | return { 21 | name: localeService.format({ 22 | id: 'backend.services.yuque_oauth.name', 23 | }), 24 | icon: 'yuque', 25 | type: 'yuque_oauth', 26 | headerForm: headerForm, 27 | service: Service, 28 | oauthUrl, 29 | form: form, 30 | homePage: 'https://www.yuque.com', 31 | permission: { 32 | origins: ['https://www.yuque.com/*'], 33 | }, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/common/backend/services/yuque_oauth/interface.ts: -------------------------------------------------------------------------------- 1 | import { CompleteStatus, CreateDocumentRequest } from './../interface'; 2 | import { Repository } from '../interface'; 3 | 4 | export enum RepositoryType { 5 | all = 'all', 6 | self = 'self', 7 | group = 'group', 8 | } 9 | 10 | export interface YuqueBackendServiceConfig { 11 | access_token: string; 12 | repositoryType: RepositoryType; 13 | } 14 | 15 | export interface YuqueUserInfoResponse { 16 | id: number; 17 | avatar_url: string; 18 | name: string; 19 | login: string; 20 | description: string; 21 | } 22 | 23 | export interface YuqueGroupResponse { 24 | id: number; 25 | name: string; 26 | login: string; 27 | } 28 | 29 | export interface YuqueRepository extends Repository { 30 | namespace: string; 31 | } 32 | 33 | export interface YuqueRepositoryResponse { 34 | id: number; 35 | name: string; 36 | namespace: string; 37 | } 38 | 39 | export interface YuqueCreateDocumentResponse { 40 | id: number; 41 | slug: string; 42 | title: string; 43 | created_at: string; 44 | updated_at: string; 45 | } 46 | 47 | export interface YuqueCompleteStatus extends CompleteStatus { 48 | documentId: string; 49 | repositoryId: string; 50 | } 51 | 52 | export interface YuqueCreateDocumentRequest extends CreateDocumentRequest { 53 | slug?: string; 54 | } 55 | -------------------------------------------------------------------------------- /src/common/blob.ts: -------------------------------------------------------------------------------- 1 | const Base64ImageToBlob = (image: string): Blob => { 2 | const arr = image.split(','); 3 | const mime = arr[0].match(/:(.*?);/)![1] || 'image/png'; 4 | const bytes = window.atob(arr[1]); 5 | let ab = new ArrayBuffer(bytes.length); 6 | let ia = new Uint8Array(ab); 7 | for (let i = 0; i < bytes.length; i++) { 8 | ia[i] = bytes.charCodeAt(i); 9 | } 10 | const blob = new Blob([ab], { 11 | type: mime, 12 | }); 13 | return blob; 14 | }; 15 | 16 | const BlobToBase64 = (blob: Blob): Promise => { 17 | const reader = new FileReader(); 18 | reader.readAsDataURL(blob); 19 | return new Promise(resolve => { 20 | reader.onloadend = () => { 21 | resolve(reader.result as string); 22 | }; 23 | }); 24 | }; 25 | 26 | function loadImage(date: string): Promise { 27 | return new Promise((resolve, reject) => { 28 | let img = new Image(); 29 | img.onload = () => resolve(img); 30 | img.onerror = reject; 31 | img.src = date; 32 | }); 33 | } 34 | 35 | export { Base64ImageToBlob, loadImage, BlobToBase64 }; 36 | -------------------------------------------------------------------------------- /src/common/chrome/storage.ts: -------------------------------------------------------------------------------- 1 | import { AbstractStorageService } from '@web-clipper/shared/lib/storage'; 2 | import * as browser from '@web-clipper/chrome-promise'; 3 | 4 | class LocalStorageService extends AbstractStorageService { 5 | constructor() { 6 | super(browser.storage.local, browser.storage.onChanged, 'local'); 7 | } 8 | } 9 | 10 | class SyncStorageService extends AbstractStorageService { 11 | constructor() { 12 | super(browser.storage.sync, browser.storage.onChanged, 'sync'); 13 | } 14 | } 15 | 16 | const localStorageService = new LocalStorageService(); 17 | const syncStorageService = new SyncStorageService(); 18 | 19 | export { localStorageService, syncStorageService }; 20 | -------------------------------------------------------------------------------- /src/common/error.ts: -------------------------------------------------------------------------------- 1 | export interface SerializedError { 2 | readonly $isError: true; 3 | readonly name: string; 4 | readonly message: string; 5 | readonly stack: string; 6 | } 7 | 8 | export function transformErrorForSerialization(error: Error): SerializedError; 9 | export function transformErrorForSerialization(error: any): any; 10 | export function transformErrorForSerialization(error: any): any { 11 | if (error instanceof Error) { 12 | let { name, message } = error; 13 | const stack: string = (error).stacktrace || (error).stack; 14 | return { 15 | $isError: true, 16 | name, 17 | message, 18 | stack, 19 | }; 20 | } 21 | 22 | // return as is 23 | return error; 24 | } 25 | -------------------------------------------------------------------------------- /src/common/getResource.ts: -------------------------------------------------------------------------------- 1 | export function getResourcePath(name: string) { 2 | let isFirefox = chrome.runtime.getURL(name).startsWith('moz-extension'); 3 | if (isFirefox) { 4 | return `chrome/${name}`; 5 | } 6 | return name; 7 | } 8 | -------------------------------------------------------------------------------- /src/common/hooks/useFilterExtensions.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { ExtensionType, SerializedExtensionInfo } from '@/extensions/common'; 3 | 4 | const useFilterExtensions = (extensions: T[]) => { 5 | return useMemo(() => { 6 | const toolExtensions: T[] = []; 7 | const clipExtensions: T[] = []; 8 | extensions.forEach(o => { 9 | if (o.type === ExtensionType.Tool) { 10 | toolExtensions.push(o); 11 | return; 12 | } 13 | clipExtensions.push(o); 14 | }); 15 | return [toolExtensions, clipExtensions]; 16 | }, [extensions]); 17 | }; 18 | 19 | export default useFilterExtensions; 20 | -------------------------------------------------------------------------------- /src/common/hooks/useFilterImageHostingServices.ts: -------------------------------------------------------------------------------- 1 | import { ImageHostingServiceMeta } from '../backend'; 2 | import { ImageHosting } from '../modelTypes/userPreference'; 3 | 4 | interface Props { 5 | backendServiceType: string; 6 | imageHostingServices: ImageHosting[]; 7 | imageHostingServicesMap: { 8 | [type: string]: ImageHostingServiceMeta; 9 | }; 10 | } 11 | 12 | export type ImageHostingWithMeta = { 13 | imageHostingServices: ImageHosting; 14 | meta: ImageHostingServiceMeta; 15 | }; 16 | 17 | const useFilterImageHostingServices = ({ 18 | backendServiceType, 19 | imageHostingServices, 20 | imageHostingServicesMap, 21 | }: Props) => { 22 | return imageHostingServices 23 | .map(o => { 24 | const meta = imageHostingServicesMap[o.type]; 25 | if (!meta) { 26 | return null; 27 | } 28 | if (meta.builtIn && meta.type !== backendServiceType) { 29 | return null; 30 | } 31 | if (meta.support && !meta.support(backendServiceType)) { 32 | return null; 33 | } 34 | return { imageHostingServices: o, meta }; 35 | }) 36 | .filter((o): o is ImageHostingWithMeta => !!o); 37 | }; 38 | 39 | export default useFilterImageHostingServices; 40 | -------------------------------------------------------------------------------- /src/common/hooks/useOriginPermission.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionsService } from '@/service/common/permissions'; 2 | import { useState } from 'react'; 3 | import Container from 'typedi'; 4 | 5 | const useOriginPermission = (initData: boolean) => { 6 | const [verified, setVerified] = useState(initData); 7 | const permissionsService = Container.get(IPermissionsService); 8 | const requestOriginPermission = async (origin: string) => { 9 | const result = await permissionsService.request({ 10 | origins: [`${origin}/*`], 11 | }); 12 | setVerified(result); 13 | }; 14 | return [verified, requestOriginPermission] as const; 15 | }; 16 | 17 | export default useOriginPermission; 18 | -------------------------------------------------------------------------------- /src/common/locales/antd.ts: -------------------------------------------------------------------------------- 1 | import en_US from 'antd/lib/locale-provider/en_US'; 2 | import ja_JP from 'antd/lib/locale-provider/ja_JP'; 3 | import ru_RU from 'antd/lib/locale-provider/ru_RU'; 4 | import zh_CN from 'antd/lib/locale-provider/zh_CN'; 5 | import zh_TW from 'antd/lib/locale-provider/zh_TW'; 6 | 7 | const localeProvider = { 8 | 'en-US': en_US, 9 | 'ja-JP': ja_JP, 10 | 'ru-RU': ru_RU, 11 | 'zh-CN': zh_CN, 12 | 'zh-TW': zh_TW, 13 | }; 14 | 15 | export { localeProvider }; 16 | -------------------------------------------------------------------------------- /src/common/locales/data/de-DE.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './de-DE.json'; 3 | 4 | const model: LocaleModel = { 5 | name: 'Deutsch', 6 | locale: 'de-DE', 7 | messages, 8 | alias: ['de'], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/data/en-US.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './en-US.json'; 3 | 4 | const model: LocaleModel = { 5 | name: 'English', 6 | locale: 'en-US', 7 | messages, 8 | alias: ['en'], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/data/ja-JP.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './ja-JP.json'; 3 | 4 | const model: LocaleModel = { 5 | name: '日本語', 6 | locale: 'ja-JP', 7 | messages, 8 | alias: ['jp'], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/data/ko-KR.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './ko-KR.json'; 3 | 4 | const model: LocaleModel = { 5 | name: '한국어', 6 | locale: 'ko-KR', 7 | messages, 8 | alias: [], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/data/ru-RU.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './ru-RU.json'; 3 | 4 | const model: LocaleModel = { 5 | name: 'русский', 6 | locale: 'ru-RU', 7 | messages, 8 | alias: [], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/data/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './zh-CN.json'; 3 | 4 | const model: LocaleModel = { 5 | name: '简体中文', 6 | locale: 'zh-CN', 7 | messages, 8 | alias: ['zh'], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/data/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import { LocaleModel } from '@/common/locales/interface'; 2 | import messages from './zh-TW.json'; 3 | 4 | const model: LocaleModel = { 5 | name: '繁體中文', 6 | locale: 'zh-TW', 7 | messages, 8 | alias: ['tw'], 9 | }; 10 | 11 | export default model; 12 | -------------------------------------------------------------------------------- /src/common/locales/index.test.ts: -------------------------------------------------------------------------------- 1 | import { removeEmptyKeys } from './interface'; 2 | 3 | it('test remove PR_IS_WELCOME', () => { 4 | const messages = { 5 | a: '1', 6 | b: '', 7 | c: '', 8 | }; 9 | 10 | expect(removeEmptyKeys(messages, { b: '2' })).toEqual({ 11 | a: '1', 12 | b: '2', 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/common/locales/interface.ts: -------------------------------------------------------------------------------- 1 | export interface LocaleModel { 2 | name: string; 3 | locale: string; 4 | alias: string[]; 5 | messages: { 6 | [key: string]: string; 7 | }; 8 | } 9 | 10 | export function removeEmptyKeys( 11 | params: LocaleModel['messages'], 12 | defaultMessage: LocaleModel['messages'] 13 | ): LocaleModel['messages'] { 14 | const result: LocaleModel['messages'] = {}; 15 | Object.keys(params).forEach(key => { 16 | if (params[key] !== '') { 17 | result[key] = params[key]; 18 | } else { 19 | if (defaultMessage[key] !== '') { 20 | result[key] = defaultMessage[key]; 21 | } 22 | } 23 | }); 24 | return result; 25 | } 26 | -------------------------------------------------------------------------------- /src/common/matchUrl.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-loop-func */ 2 | import matchUrl from './matchUrl'; 3 | 4 | describe('test matchUrl', () => { 5 | describe('should match all', () => { 6 | const cases: { rule: string; true?: string[]; false?: string[] }[] = [ 7 | { 8 | rule: '*://*/*', 9 | true: ['http://www.google.com/', 'https://www.google.com/'], 10 | }, 11 | { 12 | rule: '*://docs.google.com/', 13 | true: ['https://docs.google.com/'], 14 | false: ['https://docs.google.com.cn/', 'https://sub.docs.google.com/'], 15 | }, 16 | { 17 | rule: '*://*.google.com/', 18 | true: ['https://www.google.com/', 'https://a.b.google.com/', 'https://google.com/'], 19 | false: ['https://www.google.com.hk/'], 20 | }, 21 | { 22 | rule: '*://www.google.tld/', 23 | true: ['https://www.google.com/', 'https://www.google.com.cn/', 'https://www.google.jp/'], 24 | false: ['https://www.google.example.com/'], 25 | }, 26 | { 27 | rule: 'https://www.google.com/a', 28 | true: [ 29 | 'https://www.google.com/a', 30 | 'https://www.google.com/a#hash', 31 | 'https://www.google.com/a?query', 32 | 'https://www.google.com/a?query#hash', 33 | ], 34 | }, 35 | ]; 36 | for (const iterator of cases) { 37 | it(`test rune ${iterator.rule}`, () => { 38 | if (Array.isArray(iterator.true)) { 39 | for (const url of iterator.true) { 40 | expect(matchUrl(iterator.rule, url)).toBeTruthy(); 41 | } 42 | } 43 | if (Array.isArray(iterator.false)) { 44 | for (const url of iterator.false) { 45 | expect(matchUrl(iterator.rule, url)).toBeFalsy(); 46 | } 47 | } 48 | }); 49 | } 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/common/modelTypes/account.ts: -------------------------------------------------------------------------------- 1 | export interface AccountPreference { 2 | [key: string]: string | undefined; 3 | id: string; 4 | type: string; 5 | name: string; 6 | avatar: string; 7 | homePage: string; 8 | description?: string; 9 | defaultRepositoryId?: string; 10 | imageHosting?: string; 11 | } 12 | 13 | export interface AccountStore { 14 | currentAccountId?: string; 15 | defaultAccountId?: string; 16 | accounts: AccountPreference[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/modelTypes/clipper.ts: -------------------------------------------------------------------------------- 1 | import { ClipperDataType } from '@/common/modelTypes/userPreference'; 2 | import { 3 | Repository, 4 | CompleteStatus, 5 | CreateDocumentRequest, 6 | } from '@/common/backend/services/interface'; 7 | 8 | export interface ClipperHeaderForm { 9 | [key: string]: string | number; 10 | title: string; 11 | } 12 | 13 | export interface ClipperStore { 14 | clipperHeaderForm: ClipperHeaderForm; 15 | url?: string; 16 | currentAccountId: string; 17 | repositories: Repository[]; 18 | currentImageHostingService?: { type: string }; 19 | currentRepository?: Repository; 20 | clipperData: { 21 | [key: string]: ClipperDataType; 22 | }; 23 | completeStatus?: CompleteStatus; 24 | createDocumentRequest?: CreateDocumentRequest; 25 | } 26 | -------------------------------------------------------------------------------- /src/common/modelTypes/extensions.ts: -------------------------------------------------------------------------------- 1 | import { IExtensionWithId } from '@/extensions/common'; 2 | 3 | export interface ExtensionStore { 4 | extensions: IExtensionWithId[]; 5 | defaultExtensionId?: string | null; 6 | } 7 | 8 | export const LOCAL_EXTENSIONS_DISABLED_EXTENSIONS_KEY = 'local.extensions.disabled.extensions'; 9 | 10 | export const LOCAL_EXTENSIONS_ENABLE_AUTOMATIC_EXTENSIONS_KEY = 11 | 'local.extensions.enable.automatic.extensions'; 12 | -------------------------------------------------------------------------------- /src/common/modelTypes/userPreference.ts: -------------------------------------------------------------------------------- 1 | import { ServiceMeta, ImageHostingServiceMeta } from '@/common/backend'; 2 | 3 | export interface UserPreferenceStore { 4 | locale: string; 5 | imageHosting: ImageHosting[]; 6 | liveRendering: boolean; 7 | iconColor: 'dark' | 'light' | 'auto'; 8 | servicesMeta: { 9 | [type: string]: ServiceMeta; 10 | }; 11 | imageHostingServicesMeta: { 12 | [type: string]: ImageHostingServiceMeta; 13 | }; 14 | } 15 | 16 | /** 17 | * 图床配置的数据结构 18 | */ 19 | export interface ImageHosting { 20 | id: string; 21 | type: string; 22 | remark?: string; 23 | info?: { 24 | [key: string]: string; 25 | }; 26 | } 27 | 28 | export interface ImageClipperData { 29 | dataUrl: string; 30 | width: number; 31 | height: number; 32 | } 33 | 34 | export type ClipperDataType = string | ImageClipperData; 35 | 36 | export const LOCAL_USER_PREFERENCE_LOCALE_KEY = 'local.userPreference.locale'; 37 | 38 | /** 39 | * user Access Tiken 40 | */ 41 | export const LOCAL_ACCESS_TOKEN_LOCALE_KEY = 'local.access.token.locale'; 42 | -------------------------------------------------------------------------------- /src/common/object.ts: -------------------------------------------------------------------------------- 1 | export function isUndefined(data: any) { 2 | // eslint-disable-next-line no-undefined 3 | return data === undefined; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/storage/index.ts: -------------------------------------------------------------------------------- 1 | import { TypedCommonStorageInterface, CommonStorage } from './interface'; 2 | export * from './interface'; 3 | import * as browser from '@web-clipper/chrome-promise'; 4 | import { TypedCommonStorage } from './typedCommonStorage'; 5 | 6 | export class ChromeSyncStorageImpl implements CommonStorage { 7 | public async set(key: string, item: Object): Promise { 8 | let tempObject: any = {}; 9 | tempObject[key] = item; 10 | return browser.storage.sync.set(tempObject); 11 | } 12 | 13 | public async get(key: string): Promise { 14 | const items = await browser.storage.sync.get(key); 15 | return items[key]; 16 | } 17 | } 18 | 19 | export default new TypedCommonStorage(new ChromeSyncStorageImpl()) as TypedCommonStorageInterface; 20 | -------------------------------------------------------------------------------- /src/common/storage/interface.ts: -------------------------------------------------------------------------------- 1 | import { ImageHosting } from 'common/types'; 2 | export interface PreferenceStorage { 3 | imageHosting: ImageHosting[]; 4 | defaultPluginId?: string | null; 5 | showLineNumber: boolean; 6 | liveRendering: boolean; 7 | iconColor: 'dark' | 'light' | 'auto'; 8 | } 9 | 10 | export interface CommonStorage { 11 | set(key: string, value: any): void | Promise; 12 | get(key: string): Promise; 13 | } 14 | 15 | export interface TypedCommonStorageInterface { 16 | getPreference(): Promise; 17 | 18 | /** --------默认插件--------- */ 19 | 20 | setDefaultPluginId(id: string | null): Promise; 21 | 22 | getDefaultPluginId(): Promise; 23 | 24 | /** --------编辑器显示行号--------- */ 25 | 26 | setShowLineNumber(value: boolean): Promise; 27 | 28 | getShowLineNumber(): Promise; 29 | 30 | /** --------实时渲染--------- */ 31 | setLiveRendering(value: boolean): Promise; 32 | 33 | getLiveRendering(): Promise; 34 | 35 | setIconColor(value: string): Promise; 36 | 37 | getIconColor(): Promise; 38 | 39 | /** --------图床--------- */ 40 | 41 | addImageHosting(imageHosting: ImageHosting): Promise; 42 | 43 | getImageHosting(): Promise; 44 | 45 | deleteImageHostingById(id: string): Promise; 46 | 47 | editImageHostingById(id: string, value: ImageHosting): Promise; 48 | } 49 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { AccountStore } from './modelTypes/account'; 2 | import { RouteComponentProps } from 'react-router'; 3 | import { Dispatch } from 'react'; 4 | import { UserPreferenceStore } from '@/common/modelTypes/userPreference'; 5 | import { ClipperStore } from '@/common/modelTypes/clipper'; 6 | import { DvaLoadingState } from 'dva-loading'; 7 | import { ExtensionStore } from './modelTypes/extensions'; 8 | 9 | export * from '@/common/modelTypes/userPreference'; 10 | export * from '@/common/modelTypes/clipper'; 11 | export * from '@/common/modelTypes/account'; 12 | 13 | export type DvaRouterProps = { 14 | dispatch: Dispatch; 15 | } & RouteComponentProps; 16 | 17 | interface DvaLoadingState { 18 | global: boolean; 19 | models: { [type: string]: boolean | undefined }; 20 | effects: { [type: string]: boolean | undefined }; 21 | } 22 | 23 | export interface GlobalStore { 24 | account: AccountStore; 25 | clipper: ClipperStore; 26 | userPreference: UserPreferenceStore; 27 | loading: DvaLoadingState; 28 | extension: ExtensionStore; 29 | router: { 30 | location: { 31 | search: string; 32 | pathname: string; 33 | }; 34 | }; 35 | } 36 | 37 | export interface IResponse { 38 | result: T; 39 | message: string; 40 | } 41 | -------------------------------------------------------------------------------- /src/common/version/index.test.ts: -------------------------------------------------------------------------------- 1 | import { hasUpdate } from './index'; 2 | 3 | describe('test version', function() { 4 | it('test hasUpdate', function() { 5 | expect(hasUpdate('3.0.1', '3.0.0')).toBe(true); 6 | expect(hasUpdate('3.1.1', '3.0.1')).toBe(true); 7 | expect(hasUpdate('3.0.0', '2.0.0')).toBe(true); 8 | expect(hasUpdate('3.0.0', '3.0.0')).toBe(false); 9 | expect(hasUpdate('3.0.0', '4.0.0')).toBe(false); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/common/version/index.ts: -------------------------------------------------------------------------------- 1 | export function hasUpdate(remote: string, local: string): boolean { 2 | if (!remote) { 3 | return false; 4 | } 5 | const remoteVersion = remote.split('.').map(version => parseInt(version, 10)); 6 | const localVersion = local.split('.').map(version => parseInt(version, 10)); 7 | for (let i = 0; i < remoteVersion.length; i++) { 8 | if (remoteVersion[i] > localVersion[i]) { 9 | return true; 10 | } 11 | if (remoteVersion[i] < localVersion[i]) { 12 | return false; 13 | } 14 | } 15 | return false; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ExtensionCard/index.less: -------------------------------------------------------------------------------- 1 | :global { 2 | .ant-form-item { 3 | margin-bottom: 8px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/IconFont.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon as LegacyIcon } from '@ant-design/compatible'; 3 | import { IconProps } from '@ant-design/compatible/es/icon'; 4 | import { createFromIconfontCN } from '@ant-design/icons'; 5 | import Container from 'typedi'; 6 | import { IConfigService } from '@/service/common/config'; 7 | import { Observer, useObserver } from 'mobx-react'; 8 | 9 | const IconFont: React.FC = (props) => { 10 | const configService = Container.get(IConfigService); 11 | const IconFont = useObserver(() => { 12 | return createFromIconfontCN({ scriptUrl: './icon.js' }); 13 | }); 14 | return ( 15 | 16 | {() => { 17 | if (!configService.remoteIconSet.has(props.type)) { 18 | return ; 19 | } 20 | if (!props.type) { 21 | throw new Error('Type is required'); 22 | } 23 | return ; 24 | }} 25 | 26 | ); 27 | }; 28 | 29 | export default IconFont; 30 | -------------------------------------------------------------------------------- /src/components/ImageHostingSelect.less: -------------------------------------------------------------------------------- 1 | .imageHostingSelect { 2 | :global { 3 | .ant-select-selector { 4 | height: 72px !important; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/ImageHostingSelect.tsx: -------------------------------------------------------------------------------- 1 | import { ImageHostingWithMeta } from '@/common/hooks/useFilterImageHostingServices'; 2 | import Select, { SelectProps } from 'antd/lib/select'; 3 | import React, { forwardRef } from 'react'; 4 | import ImageHostingSelectOption from '@/components/imageHostingSelectOption'; 5 | import styles from './ImageHostingSelect.less'; 6 | 7 | interface ImageHostingSelectProps extends SelectProps { 8 | supportedImageHostingServices: ImageHostingWithMeta[]; 9 | } 10 | 11 | /** 12 | * TODO 13 | * fix any 14 | */ 15 | export const ImageHostingSelect: React.ForwardRefRenderFunction = ( 16 | { supportedImageHostingServices, ...props }, 17 | ref 18 | ) => ( 19 | 28 | ); 29 | 30 | /** 31 | * TODO 32 | * fix any 33 | */ 34 | export default forwardRef(ImageHostingSelect); 35 | -------------------------------------------------------------------------------- /src/components/LinkRender/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LinkRenderProps { 4 | href: string; 5 | } 6 | 7 | const LinkRender: React.FC = props => { 8 | return ( 9 | 10 | {props.children} 11 | 12 | ); 13 | }; 14 | 15 | export default LinkRender; 16 | -------------------------------------------------------------------------------- /src/components/accountItem/index.less: -------------------------------------------------------------------------------- 1 | .card { 2 | padding: 10px; 3 | border-radius: 10px; 4 | text-align: center; 5 | line-height: 1.5; 6 | font-size: 14px; 7 | border: 1px solid #e8e8e8; 8 | height: 300px; 9 | position: relative; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .star { 15 | position: absolute; 16 | top: 10px; 17 | right: 10px; 18 | font-size: 20px; 19 | } 20 | 21 | .userInfo { 22 | flex: 1; 23 | .name { 24 | margin-bottom: 4px; 25 | text-overflow: ellipsis; 26 | white-space: nowrap; 27 | overflow: hidden; 28 | font-size: 20px; 29 | color: #262626; 30 | } 31 | .description { 32 | color: #8c8c8c; 33 | margin-bottom: 8px; 34 | height: 63px; 35 | overflow: hidden; 36 | word-wrap: break-word; 37 | } 38 | } 39 | 40 | .operation { 41 | display: flex; 42 | .editButton { 43 | flex: 1; 44 | margin-right: 10px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/avatar/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Avatar } from 'antd'; 3 | import IconFont from '@/components/IconFont'; 4 | 5 | interface IconAvatarProps { 6 | avatar?: string; 7 | icon: string; 8 | size: 'large' | 'small' | number; 9 | } 10 | 11 | const IconAvatar: React.FC = ({ avatar, size, icon: _icon }) => { 12 | const icon = avatar || _icon; 13 | let fontSize; 14 | if (typeof size === 'string') { 15 | fontSize = { 16 | small: 24, 17 | large: 40, 18 | }[size]; 19 | } else { 20 | fontSize = size; 21 | } 22 | if (icon.startsWith('http') || icon.indexOf('/') != -1) { 23 | return ; 24 | } 25 | return ; 26 | }; 27 | 28 | export default IconAvatar; 29 | -------------------------------------------------------------------------------- /src/components/container/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es//style/themes/default.less'; 2 | 3 | .mainContainer { 4 | position: fixed; 5 | right: 10px; 6 | top: 10px; 7 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px; 8 | } 9 | 10 | .toolContainer { 11 | width: 324px; 12 | height: auto; 13 | background: #fff; 14 | padding: 10px; 15 | } 16 | .closeButton { 17 | position: absolute; 18 | right: 10px; 19 | top: 10px; 20 | cursor: pointer; 21 | } 22 | 23 | .centerContainer { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | width: 100%; 28 | height: 100%; 29 | } 30 | .editorContainer { 31 | position: absolute; 32 | right: 350px; 33 | top: 0; 34 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px; 35 | border: 2px solid #dddddd; 36 | } 37 | 38 | .mask { 39 | position: fixed; 40 | right: 0px; 41 | top: 0px; 42 | bottom: 0px; 43 | left: 0px; 44 | background: @modal-mask-bg; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/container/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.less'; 3 | import { CloseOutlined } from '@ant-design/icons'; 4 | 5 | const Container: React.FC = ({ children }) => { 6 | return
{children}
; 7 | }; 8 | 9 | export interface ToolContainerProps { 10 | onClickCloseButton?: () => void; 11 | onClickMask?: () => void; 12 | } 13 | 14 | export class ToolContainer extends React.Component { 15 | onClickCloseButton = () => { 16 | if (this.props.onClickCloseButton) { 17 | this.props.onClickCloseButton(); 18 | } 19 | }; 20 | 21 | handleClickMask = () => { 22 | if (this.props.onClickMask) { 23 | this.props.onClickMask(); 24 | } 25 | }; 26 | 27 | public render() { 28 | return ( 29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 | {
{this.props.children}
} 37 |
38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | export const CenterContainer: React.FC = ({ children }) => { 45 | return
{children}
; 46 | }; 47 | 48 | export const EditorContainer: React.FC = ({ children }) => { 49 | return
{children}
; 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/imageHostingSelectOption/index.less: -------------------------------------------------------------------------------- 1 | .avatar { 2 | img { 3 | max-width: 32px; 4 | max-height: 32px; 5 | height: auto; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/imageHostingSelectOption/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { List, Avatar } from 'antd'; 3 | import styles from './index.less'; 4 | import { FormattedMessage } from 'react-intl'; 5 | import IconFont from '@/components/IconFont'; 6 | 7 | interface PageProps { 8 | icon: string; 9 | name: string; 10 | remark?: string; 11 | id: string; 12 | } 13 | 14 | export default class Page extends React.Component { 15 | render() { 16 | const { 17 | name, 18 | remark = ( 19 | 23 | ), 24 | icon, 25 | } = this.props; 26 | let avatar; 27 | 28 | if (icon.startsWith('http') || icon.indexOf('/') != -1) { 29 | avatar = ; 30 | } else { 31 | avatar = ; 32 | } 33 | 34 | return ( 35 | 36 | {name}} description={remark} /> 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/imagehostingListItem/index.less: -------------------------------------------------------------------------------- 1 | .avatar { 2 | img { 3 | max-width: 32px; 4 | max-height: 32px; 5 | height: auto; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/imagehostingListItem/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { List, Avatar } from 'antd'; 3 | import styles from './index.less'; 4 | import { FormattedMessage } from 'react-intl'; 5 | import IconFont from '@/components/IconFont'; 6 | 7 | interface PageProps { 8 | icon: string; 9 | name: string; 10 | remark?: string; 11 | id: string; 12 | onEditAccount: (id: string) => void; 13 | onDeleteAccount: (id: string) => void; 14 | } 15 | 16 | export default class Page extends React.Component { 17 | handleEditAccount = () => { 18 | const { onEditAccount, id } = this.props; 19 | if (onEditAccount) { 20 | onEditAccount(id); 21 | } 22 | }; 23 | 24 | handleDeleteAccount = () => { 25 | const { onDeleteAccount, id } = this.props; 26 | if (onDeleteAccount) { 27 | onDeleteAccount(id); 28 | } 29 | }; 30 | 31 | render() { 32 | const { 33 | name, 34 | remark = ( 35 | 36 | ), 37 | icon, 38 | } = this.props; 39 | let avatar; 40 | 41 | if (icon.startsWith('http') || icon.indexOf('/') != -1) { 42 | avatar = ; 43 | } else { 44 | avatar = ; 45 | } 46 | 47 | const actions = [ 48 | 49 | 50 | , 51 | 52 | 53 | , 54 | ]; 55 | 56 | return ( 57 | 58 | {name}} description={remark} /> 59 | 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/section/index.less: -------------------------------------------------------------------------------- 1 | .sectionTitle { 2 | color: rgba(0, 0, 0, 0.45); 3 | line-height: 1.5; 4 | font-size: 14px; 5 | margin: 0 0 8px 0; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/section/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.less'; 3 | 4 | interface Props { 5 | title?: string | React.ReactNode; 6 | className?: string; 7 | } 8 | 9 | const Section: React.FC = ({ title, children, className }) => { 10 | return ( 11 |
12 | {title &&

{title}

} 13 | {children} 14 |
15 | ); 16 | }; 17 | export default Section; 18 | -------------------------------------------------------------------------------- /src/components/share/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IconFont from '@/components/IconFont'; 3 | interface ShareProps { 4 | content: string; 5 | } 6 | const Share: React.FC = ({ content: originContent }) => { 7 | const content = encodeURIComponent(originContent.slice(0, 200)); 8 | const url = encodeURIComponent('https://clipper.website'); 9 | 10 | const twitterHref = `https://twitter.com/intent/tweet?via=yuanfandi&text=${content}&url=${url}`; 11 | const weiboHref = `https://service.weibo.com/share/share.php?url=${url}&title=${content}&display=0`; 12 | const doubanHref = `https://shuo.douban.com/!service/share?href=${url}&text=${content}`; 13 | 14 | return ( 15 | 26 | ); 27 | }; 28 | export default Share; 29 | -------------------------------------------------------------------------------- /src/components/userItem/index.less: -------------------------------------------------------------------------------- 1 | .userItem { 2 | display: flex; 3 | align-items: center; 4 | color: #262626; 5 | .userItemInfo { 6 | margin-left: 8px; 7 | align-self: flex-start; 8 | color: #595959; 9 | } 10 | .description { 11 | max-width: 120px; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | white-space: nowrap; 15 | color: #8c8c8c; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/userItem/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.less'; 3 | import IconAvatar from '@/components/avatar'; 4 | 5 | interface UserItemProps { 6 | avatar: string; 7 | icon: string; 8 | name: string; 9 | description?: string; 10 | } 11 | 12 | const UserItem: React.FC = props => ( 13 |
14 | 15 |
16 |
{props.name}
17 |
{props.description}
18 |
19 |
20 | ); 21 | 22 | export default UserItem; 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | interface WebClipperConfig { 2 | icon: string; 3 | iconDark: string; 4 | yuqueClientId: string; 5 | yuqueCallback: string; 6 | yuqueScope: string; 7 | oneNoteCallBack: string; 8 | oneNoteClientId: string; 9 | } 10 | 11 | export interface RemoteConfig { 12 | iconfont: string; 13 | chromeWebStoreVersion: string; 14 | } 15 | 16 | let config: WebClipperConfig = { 17 | icon: 'icons/icon.png', 18 | iconDark: 'icons/icon-dark.png', 19 | yuqueClientId: 'D1AwzCeDPLFWGfcGv7ze', 20 | yuqueCallback: 'http://webclipper-oauth.yfd.im/yuque_oauth', 21 | yuqueScope: 'doc,group,repo,attach_upload', 22 | oneNoteClientId: '563571ad-cfcd-442a-aa34-046bad24b1b6', 23 | oneNoteCallBack: 'https://webclipper-oauth.yfd.im/onenote_oauth', 24 | }; 25 | 26 | if (process.env.NODE_ENV === 'development') { 27 | config = Object.assign({}, config, { 28 | icon: 'icons/icon-dev.png', 29 | }); 30 | } 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /src/extensions/contextMenus.ts: -------------------------------------------------------------------------------- 1 | import { IContentScriptService } from '@/service/common/contentScript'; 2 | import { IExtensionManifest } from './common'; 3 | 4 | export interface IContextMenuProperties { 5 | id: string; 6 | title: string; 7 | contexts: string[]; 8 | } 9 | 10 | interface IContextMenuExtensionManifest extends IExtensionManifest { 11 | contexts?: string[]; 12 | } 13 | 14 | export interface IContextMenuExtension { 15 | readonly manifest: IContextMenuExtensionManifest; 16 | run(id: chrome.tabs.Tab, context: IContextMenuContext): Promise; 17 | } 18 | 19 | export interface IContextMenuContext { 20 | config: unknown; 21 | contentScriptService: IContentScriptService; 22 | // initContentScriptService(id: number): Promise; 23 | } 24 | 25 | export abstract class ContextMenuExtension implements IContextMenuExtension { 26 | constructor(public manifest: IContextMenuExtensionManifest) {} 27 | 28 | abstract run(id: chrome.tabs.Tab, context: IContextMenuContext): Promise; 29 | } 30 | 31 | export interface IContextMenuExtensionFactory { 32 | id: string; 33 | new (): IContextMenuExtension; 34 | } 35 | 36 | export interface IContextMenusWithId { 37 | id: string; 38 | contextMenu: IContextMenuExtensionFactory; 39 | } 40 | -------------------------------------------------------------------------------- /src/extensions/extensions/bookmark.ts: -------------------------------------------------------------------------------- 1 | import { TextExtension } from '@/extensions/common'; 2 | 3 | export default new TextExtension( 4 | { 5 | name: 'Bookmark', 6 | version: '0.0.1', 7 | description: 'Add bookmark.', 8 | icon: 'link', 9 | i18nManifest: { 10 | 'de-DE': { name: 'Lesezeichen', description: 'Lesezeichen hinzufügen.' }, 11 | 'en-US': { name: 'Bookmark', description: 'Add bookmark.' }, 12 | 'ja-JP': { name: 'ブックマーク', description: 'ブックマークを追加します。' }, 13 | 'ko-KR': { name: '북마크', description: '북마크 추가.' }, 14 | 'ru-RU': { name: 'Закладка', description: 'Добавить закладку.' }, 15 | 'zh-CN': { name: '书签', description: '添加书签' }, 16 | }, 17 | }, 18 | { 19 | run: async (context) => { 20 | const { document, locale } = context; 21 | switch (locale) { 22 | case 'zh-CN': { 23 | return `## 链接 \n [${document.URL}](${document.URL}) \n ## 备注:`; 24 | } 25 | default: 26 | return `## Link \n [${document.URL}](${document.URL}) \n ## Comment:`; 27 | } 28 | }, 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /src/extensions/extensions/extensions/remove.ts: -------------------------------------------------------------------------------- 1 | import { ToolExtension } from '@/extensions/common'; 2 | 3 | export default new ToolExtension( 4 | { 5 | name: 'Delete Element', 6 | icon: 'delete', 7 | version: '0.0.1', 8 | description: 'Delete selected page elements.', 9 | i18nManifest: { 10 | 'de-DE': { name: 'Element löschen', description: 'Ausgewählte Seitenelemente löschen.' }, 11 | 'en-US': { name: 'Delete Element', description: 'Delete selected page elements.' }, 12 | 'ja-JP': { name: '要素を削除', description: '選択したページ要素を削除します。' }, 13 | 'ko-KR': { name: '요소 삭제', description: '선택한 페이지 요소를 삭제합니다.' }, 14 | 'ru-RU': { name: 'Удалить элемент', description: 'Удалить выбранные элементы страницы.' }, 15 | 'zh-CN': { name: '删除元素', description: '删除选择的页面元素。' }, 16 | 'zh-TW': { name: '刪除元素', description: '刪除選擇的頁面元素。' }, 17 | }, 18 | }, 19 | { 20 | run: async context => { 21 | const { $, Highlighter, toggleClipper } = context; 22 | toggleClipper(); 23 | const data = await new Highlighter().start(); 24 | $(data).remove(); 25 | toggleClipper(); 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /src/extensions/extensions/extensions/selectTool.ts: -------------------------------------------------------------------------------- 1 | import { ToolExtension } from '@/extensions/common'; 2 | 3 | export default new ToolExtension( 4 | { 5 | name: 'Manual selection', 6 | icon: 'select', 7 | version: '0.0.1', 8 | description: 'Manual selection page element.', 9 | i18nManifest: { 10 | 'de-DE': { name: 'Manuelle Auswahl', description: 'Manuelle Auswahl von Seitenelementen.' }, 11 | 'en-US': { name: 'Manual selection', description: 'Manual selection of page elements.' }, 12 | 'ja-JP': { name: '手動選択', description: 'ページ要素を手動で選択します。' }, 13 | 'ko-KR': { name: '수동 선택', description: '페이지 요소를 수동으로 선택합니다.' }, 14 | 'ru-RU': { name: 'Ручной выбор', description: 'Ручной выбор элементов страницы.' }, 15 | 'zh-CN': { name: '手动选取', description: '手动选取页面中的元素' }, 16 | }, 17 | }, 18 | { 19 | init: ({ pathname }) => { 20 | if (pathname === '/') { 21 | return false; 22 | } 23 | return true; 24 | }, 25 | run: async context => { 26 | const { turndown, Highlighter, toggleClipper } = context; 27 | toggleClipper(); 28 | try { 29 | const data = await new Highlighter().start(); 30 | return turndown.turndown(data); 31 | } catch (error) { 32 | throw error; 33 | } finally { 34 | toggleClipper(); 35 | } 36 | }, 37 | afterRun: context => { 38 | const { result, data } = context; 39 | return `${data}\n${result}`; 40 | }, 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /src/extensions/extensions/fullPage.ts: -------------------------------------------------------------------------------- 1 | import { TextExtension } from '@/extensions/common'; 2 | 3 | export default new TextExtension( 4 | { 5 | name: 'Full Page', 6 | version: '0.0.1', 7 | description: 'Save Full Page and turn ro Markdown.', 8 | icon: 'copy', 9 | i18nManifest: { 10 | 'de-DE': { name: 'Vollständige Seite', description: 'Speichern Sie die gesamte Seite und konvertieren Sie sie in Markdown.' }, 11 | 'en-US': { name: 'Full Page', description: 'Save Full Page and turn to Markdown.' }, 12 | 'ja-JP': { name: '全ページ', description: '全ページを保存し、Markdownに変換します。' }, 13 | 'ko-KR': { name: '전체 페이지', description: '전체 페이지를 저장하고 Markdown으로 변환합니다.' }, 14 | 'ru-RU': { name: 'Полная страница', description: 'Сохранить полную страницу и преобразовать в Markdown.' }, 15 | 'zh-CN': { name: '整个页面', description: '把整个页面元素转换为 Markdown' }, 16 | } 17 | }, 18 | { 19 | run: async context => { 20 | const { turndown, $ } = context; 21 | const $body = $('html').clone(); 22 | $body.find('script').remove(); 23 | $body.find('style').remove(); 24 | $body.removeClass(); 25 | return turndown.turndown($body.html()); 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /src/extensions/extensions/qrcode.ts: -------------------------------------------------------------------------------- 1 | import { TextExtension } from '@/extensions/common'; 2 | 3 | export default new TextExtension( 4 | { 5 | name: 'QR code', 6 | icon: 'qrcode', 7 | version: '0.0.1', 8 | description: 'Convert the URL of the current page to a QR code.', 9 | i18nManifest: { 10 | 'de-DE': { name: 'QR-Code', description: 'Konvertieren Sie die URL der aktuellen Seite in einen QR-Code.' }, 11 | 'en-US': { name: 'QR code', description: 'Convert the URL of the current page to a QR code.' }, 12 | 'ja-JP': { name: 'QRコード', description: '現在のページのURLをQRコードに変換します。' }, 13 | 'ko-KR': { name: 'QR 코드', description: '현재 페이지의 URL을 QR 코드로 변환합니다.' }, 14 | 'ru-RU': { name: 'QR код', description: 'Преобразовать URL текущей страницы в QR-код.' }, 15 | 'zh-CN': { name: '二维码', description: '显示当前链接为二维码' }, 16 | } 17 | }, 18 | { 19 | init: ({ currentImageHostingService }) => !!currentImageHostingService, 20 | run: async context => { 21 | const { QRCode, document } = context; 22 | const dataUrl = await QRCode.toDataURL(document.URL); 23 | return dataUrl; 24 | }, 25 | afterRun: async context => { 26 | const { result: dataUrl } = context; 27 | return `![](${dataUrl})\n`; 28 | }, 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /src/extensions/extensions/readability.ts: -------------------------------------------------------------------------------- 1 | import { TextExtension } from '@/extensions/common'; 2 | 3 | export default new TextExtension( 4 | { 5 | name: 'Readability', 6 | icon: 'copy', 7 | version: '0.0.1', 8 | description: 'Intelligent extraction of webpage main content.', 9 | i18nManifest: { 10 | 'de-DE': { name: 'Lesbarkeit', description: 'Intelligente Extraktion des Hauptinhalts der Webseite.' }, 11 | 'en-US': { name: 'Readability', description: 'Intelligent extraction of webpage main content.' }, 12 | 'ja-JP': { name: '読みやすさ', description: 'ウェブページの主要な内容をインテリジェントに抽出します。' }, 13 | 'ko-KR': { name: '가독성', description: '웹 페이지의 주요 내용을 지능적으로 추출합니다.' }, 14 | 'ru-RU': { name: 'Читаемость', description: 'Интеллектуальная извлечение основного содержимого веб-страницы.' }, 15 | 'zh-CN': { name: '智能提取', description: '智能提取当前页面元素' }, 16 | } 17 | }, 18 | { 19 | run: async context => { 20 | const { turndown, document, Readability, $ } = context; 21 | let documentClone = document.cloneNode(true); 22 | $(documentClone) 23 | .find('#skPlayer') 24 | .remove(); 25 | let article = new Readability(documentClone, { 26 | keepClasses: true, 27 | }).parse(); 28 | return turndown.turndown(article.content); 29 | }, 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /src/extensions/extensions/select.ts: -------------------------------------------------------------------------------- 1 | import { TextExtension } from '@/extensions/common'; 2 | 3 | export default new TextExtension( 4 | { 5 | name: 'Manual selection', 6 | icon: 'select', 7 | version: '0.0.1', 8 | description: 'Manual selection page element.', 9 | i18nManifest: { 10 | 'de-DE': { name: 'Manuelle Auswahl', description: 'Manuelle Auswahl von Seitenelementen.' }, 11 | 'en-US': { name: 'Manual selection', description: 'Manual selection of page elements.' }, 12 | 'ja-JP': { name: '手動選択', description: 'ページ要素を手動で選択します。' }, 13 | 'ko-KR': { name: '수동 선택', description: '페이지 요소를 수동으로 선택합니다.' }, 14 | 'ru-RU': { name: 'Ручной выбор', description: 'Ручной выбор элементов страницы.' }, 15 | 'zh-CN': { name: '手动选取', description: '手动选取页面元素' }, 16 | } 17 | }, 18 | { 19 | run: async context => { 20 | const { turndown, Highlighter, toggleClipper, $ } = context; 21 | toggleClipper(); 22 | try { 23 | const data = await new Highlighter().start(); 24 | let container = document.createElement('div'); 25 | container.appendChild( 26 | $(data) 27 | .clone() 28 | .get(0) 29 | ); 30 | return turndown.turndown(container); 31 | } catch (error) { 32 | throw error; 33 | } finally { 34 | toggleClipper(); 35 | } 36 | }, 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /src/extensions/extensions/web-clipper/clear.ts: -------------------------------------------------------------------------------- 1 | import { ToolExtension } from '@/extensions/common'; 2 | 3 | export default new ToolExtension( 4 | { 5 | name: 'Clear', 6 | icon: 'close-circle', 7 | version: '0.0.1', 8 | description: 'Clear Content', 9 | apiVersion: '1.12.0', 10 | i18nManifest: { 11 | 'de-DE': { name: 'Löschen', description: 'Inhalt löschen.' }, 12 | 'en-US': { name: 'Clear', description: 'Clear Content' }, 13 | 'ja-JP': { name: 'クリア', description: '内容をクリアします。' }, 14 | 'ko-KR': { name: '지우기', description: '내용 지우기' }, 15 | 'ru-RU': { name: 'Очистить', description: 'Очистить содержимое' }, 16 | 'zh-CN': { name: '清空', description: '清空内容' }, 17 | } 18 | }, 19 | { 20 | init: ({ pathname }) => { 21 | return pathname.startsWith('/plugin'); 22 | }, 23 | afterRun: () => { 24 | return ''; 25 | }, 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /src/extensions/extensions/web-clipper/copyToClipboard.ts: -------------------------------------------------------------------------------- 1 | import { ToolExtension } from '@/extensions/common'; 2 | 3 | export default new ToolExtension( 4 | { 5 | name: 'Copy To Clipboard', 6 | icon: 'copy', 7 | version: '0.0.1', 8 | description: 'Copy To Clipboard', 9 | i18nManifest: { 10 | 'de-DE': { name: 'In die Zwischenablage kopieren', description: 'In die Zwischenablage kopieren.' }, 11 | 'en-US': { name: 'Copy To Clipboard', description: 'Copy To Clipboard' }, 12 | 'ja-JP': { name: 'クリップボードにコピー', description: 'クリップボードにコピーします。' }, 13 | 'ko-KR': { name: '클립보드에 복사', description: '클립보드에 복사합니다.' }, 14 | 'ru-RU': { name: 'Копировать в буфер обмена', description: 'Копировать в буфер обмена' }, 15 | 'zh-CN': { name: '复制', description: '复制到剪贴板' }, 16 | } 17 | }, 18 | { 19 | afterRun: ({ copyToClipboard, data }) => { 20 | copyToClipboard(data); 21 | return data; 22 | }, 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /src/extensions/extensions/web-clipper/download.ts: -------------------------------------------------------------------------------- 1 | import { ToolExtension } from '@/extensions/common'; 2 | 3 | export default new ToolExtension( 4 | { 5 | name: 'Save as Markdown', 6 | icon: 'file-markdown', 7 | version: '0.0.2', 8 | description: 'Save as Markdown and Download.', 9 | apiVersion: '1.12.0', 10 | i18nManifest: { 11 | 'de-DE': { name: 'Als Markdown speichern', description: 'Als Markdown speichern und herunterladen.' }, 12 | 'en-US': { name: 'Save as Markdown', description: 'Save as Markdown and Download.' }, 13 | 'ja-JP': { name: 'Markdownとして保存', description: 'Markdownとして保存し、ダウンロードします。' }, 14 | 'ko-KR': { name: 'Markdown으로 저장', description: 'Markdown으로 저장하고 다운로드합니다.' }, 15 | 'ru-RU': { name: 'Сохранить как Markdown', description: 'Сохранить как Markdown и скачать.' }, 16 | 'zh-CN': { name: '保存为 Markdown', description: '保存为 Markdown 并下载' }, 17 | }, 18 | }, 19 | { 20 | init: ({ pathname }) => { 21 | return pathname.startsWith('/plugin'); 22 | }, 23 | run: ({ document }) => { 24 | return document.title; 25 | }, 26 | afterRun: ({ createAndDownloadFile, data, result }) => { 27 | createAndDownloadFile(`${result || 'content'}.md`, data); 28 | return data; 29 | }, 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /src/extensions/extensions/web-clipper/pangu.ts: -------------------------------------------------------------------------------- 1 | import { ToolExtension } from '@/extensions/common'; 2 | import { SelectAreaPosition } from '@web-clipper/area-selector'; 3 | 4 | export default new ToolExtension( 5 | { 6 | name: 'Pangu', 7 | icon: 'pangu', 8 | version: '0.0.2', 9 | automatic: true, 10 | apiVersion: '1.13.0', 11 | description: 'Paranoid text spacing in JavaScript', 12 | powerpack: false, 13 | i18nManifest: { 14 | 'de-DE': { name: 'Pangu', description: 'Fügen Sie Leerzeichen zwischen chinesischen und englischen Zeichen ein.' }, 15 | 'en-US': { name: 'Pangu', description: 'Paranoid text spacing in JavaScript' }, 16 | 'ja-JP': { name: 'Pangu', description: 'すべての中国語と半角英数字、記号の間に空白を挿入します。' }, 17 | 'ko-KR': { name: 'Pangu', description: '모든 한자와 반각 영어, 숫자, 기호 사이에 공백을 삽입합니다.' }, 18 | 'ru-RU': { name: 'Pangu', description: 'Вставка пробелов между китайскими и английскими символами.' }, 19 | 'zh-CN': { name: 'Pangu', description: '所有的中文字和半形的英文、数字、符号之间插入空白。' }, 20 | }, 21 | }, 22 | { 23 | afterRun: async context => { 24 | const { pangu, data } = context; 25 | return pangu(data); 26 | }, 27 | } 28 | ); 29 | -------------------------------------------------------------------------------- /src/extensions/index.ts: -------------------------------------------------------------------------------- 1 | import { IExtensionWithId, ToolExtension, TextExtension } from './common'; 2 | import { IContextMenuExtensionFactory } from './contextMenus'; 3 | 4 | const context = require.context('./extensions', true, /\.(ts|tsx)$/); 5 | 6 | const contextMenusContext = require.context('./contextMenus', true, /\.(ts|tsx)$/); 7 | 8 | export const contextMenus = contextMenusContext.keys().map(key => { 9 | const ContextMenuExtensionFactory: IContextMenuExtensionFactory = contextMenusContext(key) 10 | .default; 11 | return { 12 | id: ContextMenuExtensionFactory.id, 13 | contextMenu: ContextMenuExtensionFactory, 14 | }; 15 | }); 16 | 17 | export const extensions: IExtensionWithId[] = context.keys().map(key => { 18 | const id = key.slice(2, key.length - 3); 19 | const extension = context(key).default; 20 | if (extension instanceof ToolExtension || extension instanceof TextExtension) { 21 | return { 22 | ...context(key).default, 23 | id, 24 | router: `/plugins/${id}`, 25 | }; 26 | } 27 | return { 28 | factory: extension, 29 | id, 30 | router: `/plugins/${id}`, 31 | }; 32 | }); 33 | -------------------------------------------------------------------------------- /src/hooks/useOriginForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import useOriginPermission from '@/common/hooks/useOriginPermission'; 4 | import { FormComponentProps } from '@ant-design/compatible/lib/form'; 5 | 6 | interface UseOriginFormProps extends FormComponentProps { 7 | initStatus: boolean; 8 | originKey?: string; 9 | } 10 | 11 | const useOriginForm = ({ initStatus, form, originKey }: UseOriginFormProps) => { 12 | const key = originKey || 'origin'; 13 | const [verified, requestOriginPermission] = useOriginPermission(initStatus); 14 | const handleAuthentication = () => { 15 | form.validateFields([key], async (err, value) => { 16 | if (err) { 17 | return; 18 | } 19 | requestOriginPermission(value[key]); 20 | }); 21 | }; 22 | const formRules = [ 23 | { 24 | required: true, 25 | message: ( 26 | 30 | ), 31 | }, 32 | { 33 | validator(_r: any, value: string, callback: Function) { 34 | if (!value) { 35 | return callback(); 36 | } 37 | try { 38 | const _url = new URL(value); 39 | if (_url.origin !== value) { 40 | form.setFieldsValue({ 41 | [key]: _url.toString(), 42 | }); 43 | callback(); 44 | } 45 | callback(); 46 | } catch (_error) { 47 | return callback( 48 | 52 | ); 53 | } 54 | }, 55 | }, 56 | ]; 57 | return { verified, handleAuthentication, formRules }; 58 | }; 59 | 60 | export default useOriginForm; 61 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | webpack App 6 | 58 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/pages/app.less: -------------------------------------------------------------------------------- 1 | body { 2 | background: none; 3 | font-family: Chinese Quote, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, PingFang SC, 4 | Hiragino Sans GB, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/complete/complete.less: -------------------------------------------------------------------------------- 1 | .jump { 2 | margin-top: 16px; 3 | } 4 | .icons { 5 | font-size: 100px; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/locale.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IntlProvider } from 'react-intl'; 3 | import { ConfigProvider } from 'antd'; 4 | import { connect } from 'dva'; 5 | import { localesMap } from '@/common/locales'; 6 | import { localeProvider } from '@/common/locales/antd'; 7 | import { GlobalStore } from '@/common/types'; 8 | 9 | const mapStateToProps = ({ userPreference: { locale } }: GlobalStore) => { 10 | return { 11 | locale, 12 | }; 13 | }; 14 | type PageStateProps = ReturnType; 15 | 16 | const LocalWrapper: React.FC = ({ children, locale }) => { 17 | const language = locale; 18 | const model = (localesMap.get(language) || localesMap.get('en-US'))!; 19 | return ( 20 | 21 | { 24 | if (!e || !e.parentNode) { 25 | return document.body; 26 | } 27 | return e.parentNode as HTMLElement; 28 | }} 29 | > 30 | {children} 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default connect(mapStateToProps)(LocalWrapper); 37 | -------------------------------------------------------------------------------- /src/pages/plugin/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect, router } from 'dva'; 3 | import { ExtensionType } from '@/extensions/common'; 4 | import TextEditor from './TextEditor'; 5 | import { DvaRouterProps } from '@/common/types'; 6 | import { useObserver } from 'mobx-react'; 7 | import Container from 'typedi'; 8 | import { IExtensionContainer } from '@/service/common/extension'; 9 | 10 | const { Redirect } = router; 11 | 12 | const ClipperPluginPage: React.FC = props => { 13 | const { 14 | history: { 15 | location: { pathname, search }, 16 | }, 17 | } = props; 18 | const extensions = useObserver(() => Container.get(IExtensionContainer).extensions); 19 | if (pathname === '/editor') { 20 | return ; 21 | } 22 | const extension = extensions.find(o => o.router === pathname); 23 | if (!extension) { 24 | return ; 25 | } 26 | if (extension.type === ExtensionType.Text) { 27 | return ; 28 | } 29 | return ; 30 | }; 31 | 32 | export default connect()(ClipperPluginPage); 33 | -------------------------------------------------------------------------------- /src/pages/plugin/index.less: -------------------------------------------------------------------------------- 1 | .mainContent { 2 | width: 960px; 3 | height: 600px; 4 | padding: 10px; 5 | background: white; 6 | position: relative; 7 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px; 8 | border: 2px solid #dddddd; 9 | } 10 | 11 | .closeIcon { 12 | position: absolute; 13 | padding: 10px; 14 | right: 0; 15 | z-index: 100000; 16 | top: 0; 17 | } 18 | .imageContent { 19 | max-width: 960px; 20 | max-height: 600px; 21 | } 22 | 23 | :global { 24 | .CodeMirror-gutters { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/preference/account/index.less: -------------------------------------------------------------------------------- 1 | .accountPanel { 2 | display: flex; 3 | flex-wrap: wrap; 4 | width: 100%; 5 | } 6 | .createButton { 7 | height: 300px; 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/preference/account/modal/index.less: -------------------------------------------------------------------------------- 1 | .modalTitle { 2 | a { 3 | margin-left: 10px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/preference/changelog/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from 'antd'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import Container from 'typedi'; 5 | import { useFetch } from '@shihengtech/hooks'; 6 | import LinkRender from '@/components/LinkRender'; 7 | import { IEnvironmentService } from '@/services/environment/common/environment'; 8 | 9 | const Changelog: React.FC = () => { 10 | const environmentService = Container.get(IEnvironmentService); 11 | const { loading, data: changelog } = useFetch(async () => { 12 | return environmentService.changelog(); 13 | }, []); 14 | 15 | if (loading || !changelog) { 16 | return ; 17 | } 18 | return {changelog}; 19 | }; 20 | 21 | export default Changelog; 22 | -------------------------------------------------------------------------------- /src/pages/preference/extensions/index.less: -------------------------------------------------------------------------------- 1 | .extensionCard { 2 | margin-bottom: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/preference/imageHosting/index.less: -------------------------------------------------------------------------------- 1 | .listItem { 2 | padding-top: 16px; 3 | padding-bottom: 16px; 4 | display: flex; 5 | align-items: center; 6 | .icon { 7 | width: 48px; 8 | height: 48px; 9 | padding: 10; 10 | font-size: 48px; 11 | } 12 | .actionSplit { 13 | position: absolute; 14 | top: 50%; 15 | right: 0; 16 | width: 1px; 17 | height: 14px; 18 | margin-top: -7px; 19 | background-color: #e8e8e8; 20 | } 21 | } 22 | .box { 23 | padding: 10px; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/preference/index.less: -------------------------------------------------------------------------------- 1 | .mainContent { 2 | width: 960px; 3 | height: 600px; 4 | padding: 8px; 5 | background: white; 6 | position: relative; 7 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px; 8 | border: 2px solid #dddddd; 9 | :global { 10 | .ant-tabs-content { 11 | height: 100%; 12 | } 13 | } 14 | } 15 | 16 | .closeIcon { 17 | position: absolute; 18 | padding: 10px; 19 | right: 0; 20 | top: 0; 21 | z-index: 10000; 22 | cursor: pointer; 23 | } 24 | 25 | .tabPane { 26 | padding: 20px; 27 | height: 100%; 28 | overflow-y: scroll; 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/preference/privacy/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton } from 'antd'; 3 | import ReactMarkdown from 'react-markdown'; 4 | import LinkRender from '@/components/LinkRender'; 5 | import Container from 'typedi'; 6 | import { useFetch } from '@shihengtech/hooks'; 7 | import { IEnvironmentService } from '@/services/environment/common/environment'; 8 | 9 | const Privacy: React.FC = () => { 10 | const environmentService = Container.get(IEnvironmentService); 11 | const { loading, data: privacy } = useFetch(async () => { 12 | return environmentService.privacy(); 13 | }, []); 14 | 15 | if (loading || !privacy) { 16 | return ; 17 | } 18 | return {privacy}; 19 | }; 20 | 21 | export default Privacy; 22 | -------------------------------------------------------------------------------- /src/pages/tool/index.less: -------------------------------------------------------------------------------- 1 | .menuButton { 2 | text-align: left; 3 | border: none; 4 | box-shadow: none; 5 | span { 6 | font-size: 16px; 7 | } 8 | } 9 | 10 | .save-button { 11 | span { 12 | font-size: 16px; 13 | } 14 | } 15 | .toolbar { 16 | font-size: 18px; 17 | display: flex; 18 | justify-content: space-between; 19 | } 20 | .toolbarButton { 21 | border: none; 22 | columns: #e8e8e8; 23 | font-size: 18px; 24 | box-shadow: none; 25 | } 26 | .header { 27 | :global(.ant-legacy-form-item) { 28 | margin-bottom: 8px; 29 | } 30 | } 31 | .section { 32 | margin-bottom: 16px; 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/tool/toolExtensions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './index.less'; 3 | import { Button } from 'antd'; 4 | import Section from 'components/section'; 5 | import { FormattedMessage } from 'react-intl'; 6 | import IconFont from '@/components/IconFont'; 7 | import { IExtensionWithId } from '@/extensions/common'; 8 | 9 | type ToolExtensionsProps = { 10 | extensions: IExtensionWithId[]; 11 | onClick(router: IExtensionWithId): void; 12 | }; 13 | 14 | const ToolExtensions: React.FC = ({ extensions, onClick }) => { 15 | if (extensions.length === 0) { 16 | return null; 17 | } 18 | return ( 19 |
26 | } 27 | > 28 | {extensions.map(o => ( 29 | 37 | ))} 38 |
39 | ); 40 | }; 41 | 42 | export default ToolExtensions; 43 | -------------------------------------------------------------------------------- /src/service/common/config.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | import { ObservableSet } from 'mobx'; 3 | 4 | export interface RemoteConfig { 5 | iconfont: string; 6 | 7 | chromeWebStoreVersion: string; 8 | 9 | privacyLocale: string[]; 10 | 11 | changelogLocale: string[]; 12 | } 13 | 14 | export interface IConfigService { 15 | config?: RemoteConfig; 16 | 17 | isLatestVersion: boolean; 18 | 19 | readonly localVersion: string; 20 | 21 | remoteIconSet: ObservableSet; 22 | 23 | id: string; 24 | 25 | load(): Promise; 26 | } 27 | 28 | export const IConfigService = new Token(); 29 | -------------------------------------------------------------------------------- /src/service/common/configuration.ts: -------------------------------------------------------------------------------- 1 | export interface WebClipperConfiguration { 2 | resource: { 3 | host: string; 4 | privacy: string; 5 | changelog: string; 6 | }; 7 | yuque_oauth: { 8 | clientId: string; 9 | callback: string; 10 | scope: string; 11 | }; 12 | onenote_oauth: { 13 | clientId: string; 14 | callback: string; 15 | }; 16 | google_oauth: { 17 | clientId: string; 18 | callback: string; 19 | }; 20 | github_oauth: { 21 | clientId: string; 22 | callback: string; 23 | }; 24 | } 25 | 26 | export interface IConfigurationService {} 27 | 28 | export type GetLocalConfiguration = () => {}; 29 | -------------------------------------------------------------------------------- /src/service/common/contentScript.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export interface IToggleConfig { 4 | pathname: string; 5 | query?: string; 6 | } 7 | export interface IContentScriptService { 8 | hide(): Promise; 9 | remove(): Promise; 10 | checkStatus(): Promise; 11 | toggle(config?: IToggleConfig): Promise; 12 | runScript(id: string, lifeCycle: 'run' | 'destroy'): Promise; 13 | getSelectionMarkdown(): Promise; 14 | getPageUrl(): Promise; 15 | } 16 | 17 | export const IContentScriptService = new Token('IContentScriptService'); 18 | -------------------------------------------------------------------------------- /src/service/common/cookie.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export interface ICookieService { 4 | get(details: chrome.cookies.Details): Promise; 5 | getAll(details: chrome.cookies.GetAllDetails): Promise; 6 | getAllCookieStores(): Promise; 7 | } 8 | 9 | export const ICookieService = new Token('ICookieService'); 10 | -------------------------------------------------------------------------------- /src/service/common/extension.ts: -------------------------------------------------------------------------------- 1 | import { IExtensionWithId, IContextMenusWithId } from '@/extensions/common'; 2 | import { Token } from 'typedi'; 3 | 4 | export interface Extension { 5 | // 6 | } 7 | 8 | export interface IExtensionService { 9 | init(): Promise; 10 | DefaultExtensionId: string | null; 11 | 12 | DisabledExtensionIds: string[]; 13 | 14 | EnabledAutomaticExtensionIds: string[]; 15 | 16 | getExtensionConfig(id: string): T | undefined; 17 | 18 | setExtensionConfig(id: string, data: T): Promise; 19 | 20 | toggleDefault(id: string): Promise; 21 | 22 | toggleDisableExtension(id: string): Promise; 23 | 24 | toggleAutomaticExtension(id: string): Promise; 25 | } 26 | 27 | export interface IExtensionContainer { 28 | init(): Promise; 29 | extensions: IExtensionWithId[]; 30 | contextMenus: IContextMenusWithId[]; 31 | } 32 | 33 | export const IExtensionService = new Token(); 34 | 35 | export const IExtensionContainer = new Token(); 36 | -------------------------------------------------------------------------------- /src/service/common/ipc.ts: -------------------------------------------------------------------------------- 1 | import { generateUuid } from '@web-clipper/shared/lib/uuid'; 2 | import { SerializedError } from '@/common/error'; 3 | 4 | export interface IServerChannel { 5 | callCommand(context: C, command: string, arg?: any): Promise; 6 | } 7 | 8 | export interface IChannel { 9 | call(command: string, arg?: any): Promise; 10 | } 11 | 12 | export interface IChannelClient { 13 | getChannel(channelName: string): IChannel; 14 | } 15 | 16 | export interface IChannelServer { 17 | registerChannel(channelName: string, channel: IServerChannel): void; 18 | } 19 | 20 | export interface IPCMessageRequest { 21 | uuid: string; 22 | command: string; 23 | arg?: T; 24 | } 25 | 26 | export interface IPCMessageResponse { 27 | uuid: string; 28 | result?: { 29 | data: T; 30 | }; 31 | error?: { 32 | data: SerializedError; 33 | }; 34 | } 35 | 36 | export class ChannelClient implements IChannel { 37 | constructor(private channelName: string) {} 38 | 39 | async call(command: string, arg?: any): Promise { 40 | const uuid = generateUuid(); 41 | const response: any = await chrome.runtime.sendMessage({ 42 | uuid, 43 | command: command, 44 | arg, 45 | channelName: this.channelName, 46 | }); 47 | if (response.error) { 48 | const errorData: SerializedError = response.error.data; 49 | if (errorData.$isError) { 50 | const error = new Error(errorData.message); 51 | error.name = errorData.name; 52 | error.stack = errorData.stack; 53 | throw error; 54 | } else { 55 | throw response.error.data; 56 | } 57 | } 58 | if (response.result) { 59 | return response.result.data; 60 | } 61 | throw new Error('some error'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/service/common/locale.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | import { MessageDescriptor } from 'react-intl'; 3 | 4 | export interface ILocaleService { 5 | locale: string; 6 | init(): Promise; 7 | format(descriptor: MessageDescriptor): string; 8 | } 9 | 10 | export const ILocaleService = new Token('locale'); 11 | -------------------------------------------------------------------------------- /src/service/common/permissions.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export interface Permissions { 4 | origins?: string[]; 5 | 6 | permissions?: string[]; 7 | } 8 | 9 | export interface IPermissionsService { 10 | contains(permissions: Permissions): Promise; 11 | 12 | request(permissions: Permissions): Promise; 13 | 14 | remove(permissions: Permissions): Promise; 15 | } 16 | 17 | export const IPermissionsService = new Token('IPermissionsService'); 18 | -------------------------------------------------------------------------------- /src/service/common/preference.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export type TIconColor = 'dark' | 'light' | 'auto'; 4 | 5 | export interface IUserPreference { 6 | iconColor: TIconColor; 7 | } 8 | 9 | export interface IPreferenceService { 10 | userPreference: IUserPreference; 11 | init: () => Promise; 12 | 13 | updateIconColor(color: TIconColor): Promise; 14 | } 15 | 16 | export const IPreferenceService = new Token(); 17 | -------------------------------------------------------------------------------- /src/service/common/storage.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | import { IStorageService } from '@web-clipper/shared/lib/storage'; 3 | 4 | export const ILocalStorageService = new Token(); 5 | 6 | export const ISyncStorageService = new Token(); 7 | -------------------------------------------------------------------------------- /src/service/common/tab.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | export interface Tab { 3 | id?: number; 4 | title?: string; 5 | url?: string; 6 | } 7 | 8 | export interface CaptureVisibleTabOptions { 9 | quality?: number; 10 | 11 | format?: string; 12 | } 13 | export interface ITabService { 14 | getCurrent(): Promise; 15 | 16 | closeCurrent(): Promise; 17 | 18 | remove(tabId: number): Promise; 19 | 20 | captureVisibleTab(option: CaptureVisibleTabOptions | number): Promise; 21 | 22 | sendMessage(tabId: number, message: any): Promise; 23 | 24 | sendActionToCurrentTab(action: any): Promise; 25 | 26 | create(createProperties: chrome.tabs.CreateProperties): Promise; 27 | } 28 | 29 | export abstract class AbstractTabService implements ITabService { 30 | closeCurrent = async () => { 31 | const current = await this.getCurrent(); 32 | return this.remove(current.id!); 33 | }; 34 | 35 | sendActionToCurrentTab = async (action: any): Promise => { 36 | const current = await this.getCurrent(); 37 | if (!current || !current.id) { 38 | throw new Error('No Tab'); 39 | } 40 | return this.sendMessage(current.id, action); 41 | }; 42 | 43 | abstract getCurrent(): Promise; 44 | abstract remove(tabId: number): Promise; 45 | abstract captureVisibleTab(option: CaptureVisibleTabOptions | number): Promise; 46 | abstract sendMessage(tabId: number, message: any): Promise; 47 | abstract create(createProperties: chrome.tabs.CreateProperties): Promise; 48 | } 49 | 50 | export const ITabService = new Token('ITabService'); 51 | -------------------------------------------------------------------------------- /src/service/common/webRequest.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export interface WebBlockHeader { 4 | name: string; 5 | value: string; 6 | } 7 | 8 | export interface WebRequestBlockOption { 9 | requestHeaders: chrome.webRequest.HttpHeader[]; 10 | urls: string[]; 11 | } 12 | 13 | export interface RequestInBackgroundOptions { 14 | method?: string; 15 | data?: any; 16 | prefix?: string; 17 | headers?: HeadersInit; 18 | } 19 | 20 | export interface IWebRequestService { 21 | startChangeHeader(option: WebRequestBlockOption): Promise; 22 | 23 | end(webBlockHeader: WebBlockHeader): Promise; 24 | 25 | requestInBackground(url: string, options?: RequestInBackgroundOptions): Promise; 26 | 27 | changeUrl(url: string, query: WebBlockHeader): Promise; 28 | } 29 | 30 | export const IWebRequestService = new Token('IWebRequestService'); 31 | -------------------------------------------------------------------------------- /src/service/config/browser/configService.ts: -------------------------------------------------------------------------------- 1 | import { hasUpdate } from '@/common/version'; 2 | import { RemoteConfig as _RemoteConfig, IConfigService } from '@/service/common/config'; 3 | import { Service } from 'typedi'; 4 | import packageJson from '@/../package.json'; 5 | import localConfig from '@/../config.json'; 6 | import { observable, ObservableSet, runInAction } from 'mobx'; 7 | import request from 'umi-request'; 8 | import { getResourcePath } from '@/common/getResource'; 9 | 10 | type RemoteConfig = _RemoteConfig; 11 | 12 | class BrowserConfigService implements IConfigService { 13 | @observable 14 | public isLatestVersion: boolean = true; 15 | 16 | @observable 17 | public config: RemoteConfig = localConfig; 18 | 19 | @observable 20 | public remoteIconSet: ObservableSet = observable.set(); 21 | 22 | public readonly localVersion = packageJson.version; 23 | 24 | load = async () => { 25 | const iconsFile = await request.get('./icon.js'); 26 | const matchResult: string[] = iconsFile.match(/id="([A-Za-z]+)"/g) || []; 27 | const remoteIcons = matchResult.map((o) => o.match(/id="([A-Za-z]+)"/)![1]); 28 | runInAction(() => { 29 | remoteIcons.forEach((icon) => { 30 | this.remoteIconSet.add(icon); 31 | }); 32 | }); 33 | try { 34 | runInAction(() => { 35 | this.isLatestVersion = !hasUpdate(this.config.chromeWebStoreVersion, this.localVersion); 36 | }); 37 | } catch (_error) { 38 | console.log('Load Config Error'); 39 | } 40 | }; 41 | 42 | get id() { 43 | const url = chrome.runtime.getURL('tool.html'); 44 | const match = /(chrome-extension|moz-extension):\/\/(.*)\/tool.html/.exec(url); 45 | if (!match) { 46 | throw new Error('Get ExtensionId failed'); 47 | } 48 | return match[2]; 49 | } 50 | } 51 | 52 | Service(IConfigService)(BrowserConfigService); 53 | -------------------------------------------------------------------------------- /src/service/configuration/common/generate-local-config.ts: -------------------------------------------------------------------------------- 1 | import { WebClipperConfiguration } from '@/service/common/configuration'; 2 | 3 | interface IGenerateLocalConfigOptions { 4 | locale: string; 5 | } 6 | 7 | const generateLocalConfig = (_options: IGenerateLocalConfigOptions): WebClipperConfiguration => { 8 | return { 9 | resource: { 10 | host: '', 11 | privacy: '', 12 | changelog: '', 13 | }, 14 | yuque_oauth: { 15 | clientId: 'D1AwzCeDPLFWGfcGv7ze', 16 | callback: 'http://webclipper-oauth.yfd.im/yuque_oauth', 17 | scope: '94f779401c7bed8734ce', 18 | }, 19 | onenote_oauth: { 20 | clientId: '', 21 | callback: '', 22 | }, 23 | google_oauth: { 24 | clientId: '', 25 | callback: '', 26 | }, 27 | github_oauth: { 28 | clientId: '', 29 | callback: '', 30 | }, 31 | }; 32 | }; 33 | 34 | export { generateLocalConfig }; 35 | -------------------------------------------------------------------------------- /src/service/configuration/configuration.ts: -------------------------------------------------------------------------------- 1 | import { IStorageService } from '@web-clipper/shared/lib/storage'; 2 | import { Inject } from 'typedi'; 3 | import { ILocalStorageService } from './../common/storage'; 4 | import { IConfigurationService } from '@/service/common/configuration'; 5 | 6 | export class ConfigurationService implements IConfigurationService { 7 | private _initialized: Promise | null = null; 8 | constructor(@Inject(ILocalStorageService) private localStorageService: IStorageService) { 9 | // 10 | } 11 | 12 | public async init(): Promise { 13 | if (!this._initialized) { 14 | this._initialized = this.doInitialize(); 15 | } 16 | return this._initialized; 17 | } 18 | 19 | private async doInitialize(): Promise { 20 | await this.localStorageService.init(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/service/contentScript/browser/contentScript/contentScript.less: -------------------------------------------------------------------------------- 1 | .toolFrame { 2 | position: fixed !important; 3 | right: 0 !important; 4 | top: 0 !important; 5 | width: 100% !important; 6 | height: 100% !important; 7 | z-index: 2147483646 !important; 8 | border: none !important; 9 | } 10 | 11 | .web-clipper-loading-box { 12 | position: fixed; 13 | right: 0; 14 | top: 0; 15 | width: 100%; 16 | height: 100%; 17 | z-index: 2147483646; 18 | border: none; 19 | :global { 20 | .web-clipper-loading { 21 | position: fixed; 22 | right: 10px; 23 | top: 10px; 24 | box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px; 25 | background: white; 26 | width: 324px; 27 | height: 150px; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | 33 | .web-clipper-loading .line :local { 34 | animation: expand 1s ease-in-out infinite; 35 | border-radius: 10px; 36 | display: inline-block; 37 | transform-origin: center center; 38 | margin: 0 3px; 39 | width: 1px; 40 | height: 25px; 41 | } 42 | 43 | .web-clipper-loading .line:nth-child(1) { 44 | background: #27ae60; 45 | } 46 | 47 | .web-clipper-loading .line:nth-child(2) { 48 | animation-delay: 180ms; 49 | background: #f1c40f; 50 | } 51 | 52 | .web-clipper-loading .line:nth-child(3) { 53 | animation-delay: 360ms; 54 | background: #e67e22; 55 | } 56 | 57 | .web-clipper-loading .line:nth-child(4) { 58 | animation-delay: 540ms; 59 | background: #2980b9; 60 | } 61 | 62 | @keyframes expand { 63 | 0% { 64 | transform: scale(1); 65 | } 66 | 25% { 67 | transform: scale(2); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/service/cookie/background/cookieService.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'typedi'; 2 | import { ICookieService } from '@/service/common/cookie'; 3 | 4 | class ChromeCookieService implements ICookieService { 5 | get(detail: chrome.cookies.Details): Promise { 6 | return new Promise(r => { 7 | chrome.cookies.get(detail, r); 8 | }); 9 | } 10 | 11 | getAll(detail: chrome.cookies.GetAllDetails): Promise { 12 | return new Promise(r => { 13 | chrome.cookies.getAll(detail, r); 14 | }); 15 | } 16 | getAllCookieStores(): Promise { 17 | return new Promise(r => { 18 | chrome.cookies.getAllCookieStores(cookieStores => { 19 | r(cookieStores); 20 | }); 21 | }); 22 | } 23 | } 24 | 25 | Service(ICookieService)(ChromeCookieService); 26 | -------------------------------------------------------------------------------- /src/service/cookie/common/cookieIpc.ts: -------------------------------------------------------------------------------- 1 | import { IServerChannel, IChannel } from '@/service/common/ipc'; 2 | import { ICookieService } from '@/service/common/cookie'; 3 | 4 | export class CookieChannel implements IServerChannel { 5 | constructor(private service: ICookieService) {} 6 | 7 | callCommand = async ( 8 | _context: chrome.runtime.Port['sender'], 9 | command: string, 10 | arg: any 11 | ): Promise => { 12 | switch (command) { 13 | case 'get': 14 | return this.service.get(arg); 15 | case 'getAll': 16 | return this.service.getAll(arg); 17 | case 'getAllCookieStores': 18 | return this.service.getAllCookieStores(); 19 | default: { 20 | throw new Error(`Call not found: ${command}`); 21 | } 22 | } 23 | }; 24 | } 25 | 26 | export class CookieChannelClient implements ICookieService { 27 | constructor(private channel: IChannel) {} 28 | 29 | get = async (detail: chrome.cookies.Details): Promise => { 30 | return this.channel.call('get', detail); 31 | }; 32 | 33 | getAll = async (detail: chrome.cookies.GetAllDetails): Promise => { 34 | return this.channel.call('getAll', detail); 35 | }; 36 | 37 | getAllCookieStores = async (): Promise => { 38 | return this.channel.call('getAllCookieStores'); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/service/ipc/browser/background-main/ipcService.ts: -------------------------------------------------------------------------------- 1 | import { transformErrorForSerialization } from '@/common/error'; 2 | import { IChannelServer, IServerChannel } from '@/service/common/ipc'; 3 | 4 | export class BackgroundIPCServer implements IChannelServer { 5 | public registerChannel(channelName: string, server: IServerChannel) { 6 | chrome.runtime.onMessage.addListener((message: any, _sender, sendResponse) => { 7 | if (channelName !== message.channelName) { 8 | return false; 9 | } 10 | const { uuid, command, arg } = message; 11 | server 12 | .callCommand(_sender, command, arg) 13 | .then((result) => { 14 | sendResponse({ 15 | uuid, 16 | result: { data: result }, 17 | }); 18 | }) 19 | .catch((error) => { 20 | sendResponse({ 21 | uuid, 22 | error: { data: transformErrorForSerialization(error) }, 23 | }); 24 | }); 25 | return true; 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/service/ipc/browser/contentScript/contentScriptIPCServer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IChannelServer, 3 | IServerChannel, 4 | IPCMessageRequest, 5 | IPCMessageResponse, 6 | } from '@/service/common/ipc'; 7 | import { transformErrorForSerialization } from '@/common/error'; 8 | 9 | export class ContentScriptIPCServer implements IChannelServer { 10 | public registerChannel( 11 | channelName: string, 12 | server: IServerChannel 13 | ) { 14 | const uuid = channelName; 15 | chrome.runtime.onMessage.addListener( 16 | (message: IPCMessageRequest, sender: chrome.runtime.MessageSender, sendResponse) => { 17 | if (message.uuid !== uuid) { 18 | return; 19 | } 20 | (async () => { 21 | let response: IPCMessageResponse; 22 | try { 23 | const result = await server.callCommand(sender, message.command, message.arg); 24 | response = { 25 | uuid, 26 | result: { data: result }, 27 | }; 28 | } catch (error) { 29 | response = { 30 | uuid, 31 | error: { data: transformErrorForSerialization(error) }, 32 | }; 33 | } 34 | sendResponse(response); 35 | })(); 36 | return true; 37 | } 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/service/ipc/browser/popup/ipcClient.ts: -------------------------------------------------------------------------------- 1 | import { SerializedError } from '@/common/error'; 2 | import { ITabService } from '@/service/common/tab'; 3 | import { 4 | IChannelClient, 5 | ChannelClient, 6 | IChannel, 7 | IPCMessageRequest, 8 | IPCMessageResponse, 9 | } from '@/service/common/ipc'; 10 | 11 | export class PopupIpcClient implements IChannelClient { 12 | getChannel(channelName: string) { 13 | return new ChannelClient(channelName); 14 | } 15 | } 16 | 17 | export class PopupContentScriptChannelClient implements IChannel { 18 | constructor(private namespace: string, private tabService: ITabService) {} 19 | 20 | async call(command: string, arg?: any): Promise { 21 | const action: IPCMessageRequest = { 22 | uuid: this.namespace, 23 | command, 24 | arg, 25 | }; 26 | const message: IPCMessageResponse = await this.tabService.sendActionToCurrentTab(action); 27 | if (!message) { 28 | return Promise.reject( 29 | new Error(chrome.runtime.lastError?.message ?? 'ContentScript not ready yet.') 30 | ); 31 | } 32 | return new Promise((resolve, reject) => { 33 | if (message.error) { 34 | const errorData: SerializedError = message.error.data; 35 | if (errorData.$isError) { 36 | const error = new Error(errorData.message); 37 | error.name = errorData.name; 38 | error.stack = errorData.stack; 39 | reject(error); 40 | } else { 41 | reject(message.error.data); 42 | } 43 | return; 44 | } 45 | if (message.result) { 46 | resolve(message.result.data); 47 | return; 48 | } 49 | }); 50 | } 51 | } 52 | 53 | export class PopupContentScriptIPCClient implements IChannelClient { 54 | constructor(private tabService: ITabService) {} 55 | getChannel(channelName: string) { 56 | return new PopupContentScriptChannelClient(channelName, this.tabService); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/service/permissions/chrome/permissionsService.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionsService, Permissions } from '@/service/common/permissions'; 2 | import { Service } from 'typedi'; 3 | 4 | class PermissionsService implements IPermissionsService { 5 | contains(p: Permissions) { 6 | return new Promise((r) => { 7 | if (!chrome.permissions) { 8 | r(true); 9 | return; 10 | } 11 | chrome.permissions.contains(p, r); 12 | }); 13 | } 14 | 15 | remove(p: Permissions) { 16 | return new Promise((r) => { 17 | if (!chrome.permissions) { 18 | r(true); 19 | return; 20 | } 21 | chrome.permissions.remove(p, r); 22 | }); 23 | } 24 | 25 | request(p: Permissions) { 26 | return new Promise((r) => { 27 | if (!chrome.permissions) { 28 | r(true); 29 | return; 30 | } 31 | console.log('chrome.permissions.request ', chrome); 32 | chrome.permissions.request(p, r); 33 | }); 34 | } 35 | } 36 | 37 | Service(IPermissionsService)(PermissionsService); 38 | -------------------------------------------------------------------------------- /src/service/permissions/common/permissionsIpc.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionsService, Permissions } from '@/service/common/permissions'; 2 | import { IServerChannel, IChannel } from '@/service/common/ipc'; 3 | 4 | export class PermissionsChannel implements IServerChannel { 5 | constructor(private service: IPermissionsService) {} 6 | 7 | callCommand = async ( 8 | _context: chrome.runtime.Port['sender'], 9 | command: string, 10 | arg: any 11 | ): Promise => { 12 | switch (command) { 13 | case 'contains': 14 | return this.service.contains(arg); 15 | case 'remove': 16 | return this.service.remove(arg); 17 | case 'request': 18 | return this.service.request(arg); 19 | default: { 20 | throw new Error(`Call not found: ${command}`); 21 | } 22 | } 23 | }; 24 | } 25 | 26 | export class PermissionsChannelClient implements IPermissionsService { 27 | constructor(private channel: IChannel) {} 28 | 29 | remove = async (permissions: Permissions): Promise => { 30 | return this.channel.call('remove', permissions); 31 | }; 32 | 33 | contains = async (permissions: Permissions): Promise => { 34 | return this.channel.call('contains', permissions); 35 | }; 36 | request = async (permissions: Permissions): Promise => { 37 | return this.channel.call('request', permissions); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/service/preference/browser/preferenceService.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Service } from 'typedi'; 2 | import { observable } from 'mobx'; 3 | import { ISyncStorageService } from '@/service/common/storage'; 4 | import { IStorageService } from '@web-clipper/shared/lib/storage'; 5 | import { IPreferenceService } from '@/service/common/preference'; 6 | import type { IUserPreference, TIconColor } from '@/service/common/preference'; 7 | 8 | class PreferenceService implements IPreferenceService { 9 | @observable 10 | public userPreference: IUserPreference = { 11 | iconColor: 'dark', 12 | }; 13 | 14 | constructor(@Inject(ISyncStorageService) private syncStorageService: IStorageService) { 15 | this.syncStorageService.onDidChangeStorage((e) => { 16 | if (e === 'iconColor') { 17 | this.userPreference.iconColor = this.getIconColor(); 18 | } 19 | }); 20 | } 21 | 22 | init = async () => { 23 | try { 24 | this.userPreference.iconColor = this.getIconColor(); 25 | } catch (_error) { 26 | console.log('Load Config Error'); 27 | } 28 | }; 29 | 30 | updateIconColor = async (color: TIconColor) => { 31 | await this.syncStorageService.set('iconColor', color); 32 | }; 33 | 34 | private getIconColor = () => { 35 | return (this.syncStorageService.get('iconColor') as 'dark' | 'light' | 'auto' | null) ?? 'auto'; 36 | }; 37 | } 38 | 39 | Service(IPreferenceService)(PreferenceService); 40 | -------------------------------------------------------------------------------- /src/service/request/tool/basic.ts: -------------------------------------------------------------------------------- 1 | import { IPermissionsService } from './../../common/permissions'; 2 | import { extend, RequestMethod } from 'umi-request'; 3 | import { IRequestService, IBasicRequestService, TRequestOption } from '@/service/common/request'; 4 | import Container, { Service } from 'typedi'; 5 | class BasicRequestService implements IRequestService { 6 | private requestMethod: RequestMethod; 7 | constructor() { 8 | this.requestMethod = extend({}); 9 | } 10 | 11 | async download(url: string) { 12 | const permissionsService = Container.get(IPermissionsService); 13 | await permissionsService.request({ origins: [`${new URL(url).origin}/*`] }); 14 | return new Promise(resolve => { 15 | let oReq = new XMLHttpRequest(); 16 | oReq.open('GET', url, true); 17 | oReq.responseType = 'blob'; 18 | oReq.onload = function() { 19 | let blob = oReq.response; 20 | resolve(blob); 21 | }; 22 | oReq.send(); 23 | }); 24 | } 25 | 26 | request(url: string, options: TRequestOption) { 27 | switch (options.method) { 28 | case 'get': { 29 | return this.requestMethod.get(url, { 30 | headers: options.headers, 31 | }); 32 | } 33 | case 'put': { 34 | return this.requestMethod.put(url, { 35 | headers: options.headers, 36 | data: options.data, 37 | }); 38 | } 39 | case 'post': { 40 | return this.requestMethod.post(url, { 41 | headers: options.headers, 42 | data: options.data, 43 | requestType: options.requestType, 44 | }); 45 | } 46 | default: { 47 | throw new Error('Unsupported request method'); 48 | } 49 | } 50 | } 51 | } 52 | 53 | Service(IBasicRequestService)(BasicRequestService); 54 | -------------------------------------------------------------------------------- /src/service/tab/browser/background/tabService.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ITabService, 3 | CaptureVisibleTabOptions, 4 | AbstractTabService, 5 | Tab, 6 | } from '@/service/common/tab'; 7 | import * as browser from '@web-clipper/chrome-promise'; 8 | import { Service } from 'typedi'; 9 | 10 | class ChromeTabService extends AbstractTabService { 11 | getCurrent() { 12 | return new Promise(r => { 13 | chrome.tabs.query( 14 | { 15 | currentWindow: true, 16 | active: true, 17 | }, 18 | tab => r(tab[0]) 19 | ); 20 | }); 21 | } 22 | 23 | remove(tabId: number): Promise { 24 | return browser.tabs.remove(tabId) as Promise; 25 | } 26 | 27 | captureVisibleTab(option: CaptureVisibleTabOptions | number) { 28 | return browser.tabs.captureVisibleTab(option); 29 | } 30 | 31 | sendMessage(tabId: number, message: any): Promise { 32 | return browser.tabs.sendMessage(tabId, message); 33 | } 34 | 35 | create(createProperties: chrome.tabs.CreateProperties): Promise { 36 | return new Promise(r => { 37 | chrome.tabs.create(createProperties, tab => { 38 | r(tab); 39 | }); 40 | }); 41 | } 42 | } 43 | 44 | Service(ITabService)(ChromeTabService); 45 | -------------------------------------------------------------------------------- /src/service/tab/common/tabIpc.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ITabService, 3 | Tab, 4 | CaptureVisibleTabOptions, 5 | AbstractTabService, 6 | } from '@/service/common/tab'; 7 | import { IServerChannel, IChannel } from '@/service/common/ipc'; 8 | 9 | export class TabChannel implements IServerChannel { 10 | constructor(private service: ITabService) {} 11 | 12 | callCommand = async ( 13 | context: chrome.runtime.Port['sender'], 14 | command: string, 15 | arg: any 16 | ): Promise => { 17 | switch (command) { 18 | case 'getCurrent': 19 | return context?.tab; 20 | case 'remove': 21 | return this.service.remove(arg); 22 | case 'captureVisibleTab': 23 | return this.service.captureVisibleTab(arg); 24 | case 'create': 25 | return this.service.create(arg); 26 | case 'sendMessage': 27 | return this.service.sendMessage(arg[0], arg[1]); 28 | default: { 29 | throw new Error(`Call not found: ${command}`); 30 | } 31 | } 32 | }; 33 | } 34 | 35 | export class TabChannelClient extends AbstractTabService { 36 | constructor(private channel: IChannel) { 37 | super(); 38 | } 39 | 40 | getCurrent = async (): Promise => { 41 | return this.channel.call('getCurrent'); 42 | }; 43 | 44 | remove = async (tabId: number): Promise => { 45 | return this.channel.call('remove', tabId); 46 | }; 47 | 48 | captureVisibleTab = async (option: CaptureVisibleTabOptions | number) => { 49 | return this.channel.call('captureVisibleTab', option); 50 | }; 51 | 52 | sendMessage = async (tabId: number, message: any) => { 53 | return this.channel.call('sendMessage', [tabId, message]); 54 | }; 55 | 56 | create = async (createProperties: chrome.tabs.CreateProperties) => { 57 | return this.channel.call('create', createProperties); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/service/webRequest/chrome/background/tabService.ts: -------------------------------------------------------------------------------- 1 | import { IWebRequestService } from '@/service/common/webRequest'; 2 | import { Service } from 'typedi'; 3 | import { BackgroundWebRequestService } from '@/service/webRequest/browser/background/tabService'; 4 | 5 | class ChromeBackgroundWebRequestService extends BackgroundWebRequestService { 6 | constructor() { 7 | super(); 8 | } 9 | } 10 | 11 | Service(IWebRequestService)(ChromeBackgroundWebRequestService); 12 | -------------------------------------------------------------------------------- /src/service/webRequest/common/webRequestIPC.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IWebRequestService, 3 | WebRequestBlockOption, 4 | WebBlockHeader, 5 | RequestInBackgroundOptions, 6 | } from '@/service/common/webRequest'; 7 | import { IServerChannel, IChannel } from '@/service/common/ipc'; 8 | 9 | export class WebRequestChannel implements IServerChannel { 10 | constructor(private service: IWebRequestService) {} 11 | 12 | callCommand = async ( 13 | _context: chrome.runtime.Port['sender'], 14 | command: string, 15 | arg: any 16 | ): Promise => { 17 | switch (command) { 18 | case 'end': 19 | return this.service.end(arg); 20 | case 'startChangeHeader': 21 | return this.service.startChangeHeader(arg); 22 | case 'requestInBackground': 23 | return this.service.requestInBackground(arg[0], arg[1]); 24 | case 'changeUrl': { 25 | return this.service.changeUrl(arg[0], arg[1]); 26 | } 27 | default: { 28 | throw new Error(`Call not found: ${command}`); 29 | } 30 | } 31 | }; 32 | } 33 | 34 | export class WebRequestChannelClient implements IWebRequestService { 35 | constructor(private channel: IChannel) {} 36 | 37 | startChangeHeader = async (option: WebRequestBlockOption): Promise => 38 | this.channel.call('startChangeHeader', option); 39 | 40 | end = async (webBlockHeader: WebBlockHeader): Promise => 41 | this.channel.call('end', webBlockHeader); 42 | 43 | requestInBackground = async (url: string, options: RequestInBackgroundOptions): Promise => 44 | this.channel.call('requestInBackground', [url, options]); 45 | 46 | changeUrl = async (url: string, query: WebBlockHeader): Promise => 47 | this.channel.call('changeUrl', [url, query]); 48 | } 49 | -------------------------------------------------------------------------------- /src/service/worker/common/index.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export interface IWorkerService { 4 | changeIcon(icon: string): Promise; 5 | 6 | initContextMenu(): Promise; 7 | } 8 | 9 | export const IWorkerService = new Token('IWorkerService'); 10 | -------------------------------------------------------------------------------- /src/service/worker/common/workserServiceIPC.ts: -------------------------------------------------------------------------------- 1 | import { IChannel, IServerChannel } from '@/service/common/ipc'; 2 | import { IWorkerService } from '.'; 3 | 4 | export class WorkerServiceChannel implements IServerChannel { 5 | constructor(private service: IWorkerService) {} 6 | 7 | callCommand = async (_ctx: any, command: string, arg: any): Promise => { 8 | switch (command) { 9 | case 'changeIcon': 10 | return this.service.changeIcon(arg); 11 | case 'initContextMenu': 12 | return this.service.initContextMenu(); 13 | default: { 14 | throw new Error(`Call not found: ${command}`); 15 | } 16 | } 17 | }; 18 | } 19 | 20 | export class WorkerServiceChannelClient implements IWorkerService { 21 | constructor(private channel: IChannel) {} 22 | 23 | changeIcon = async (icon: string) => { 24 | return this.channel.call('changeIcon', icon); 25 | }; 26 | 27 | initContextMenu = async () => { 28 | return this.channel.call('initContextMenu'); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/service/worker/worker/workerService.ts: -------------------------------------------------------------------------------- 1 | import { IExtensionContainer, IExtensionService } from '@/service/common/extension'; 2 | import Container, { Service } from 'typedi'; 3 | import { IWorkerService } from '../common'; 4 | import { getResourcePath } from '@/common/getResource'; 5 | 6 | class WorkerService implements IWorkerService { 7 | constructor() {} 8 | async changeIcon(iconColor: string): Promise { 9 | if (iconColor === 'light') { 10 | chrome.action.setIcon({ path: await getResourcePath('icons/icon-dark.png') }); 11 | } else { 12 | chrome.action.setIcon({ path: await getResourcePath('icons/icon.png') }); 13 | } 14 | } 15 | async initContextMenu(): Promise { 16 | const extensionContainer = Container.get(IExtensionContainer); 17 | const extensionService = Container.get(IExtensionService); 18 | await extensionContainer.init(); 19 | await extensionService.init(); 20 | const contextMenus = extensionContainer.contextMenus; 21 | const currentContextMenus = contextMenus.filter( 22 | (p) => !extensionService.DisabledExtensionIds.includes(p.id) 23 | ); 24 | chrome.contextMenus.removeAll(() => { 25 | for (const iterator of currentContextMenus) { 26 | const Factory = iterator.contextMenu; 27 | const instance = new Factory(); 28 | chrome.contextMenus.create({ 29 | id: iterator.id, 30 | title: instance.manifest.name, 31 | contexts: instance.manifest.contexts as any[], 32 | }); 33 | } 34 | }); 35 | } 36 | } 37 | 38 | Service(IWorkerService)(WorkerService); 39 | -------------------------------------------------------------------------------- /src/services/account/common.ts: -------------------------------------------------------------------------------- 1 | import { AccountPreference } from '@/common/modelTypes/account'; 2 | 3 | export function unpackAccountPreference(account: AccountPreference) { 4 | const { 5 | id, 6 | type, 7 | defaultRepositoryId, 8 | imageHosting, 9 | name, 10 | avatar, 11 | description, 12 | homePage, 13 | ...info 14 | } = account; 15 | return { 16 | id, 17 | account: { 18 | type, 19 | defaultRepositoryId, 20 | imageHosting, 21 | info, 22 | }, 23 | userInfo: { 24 | name, 25 | avatar, 26 | description, 27 | homePage, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/services/configuration/common/configuration.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export interface IWebClipperConfiguration { 4 | yuque_oauth: { 5 | clientId: string; 6 | callback: string; 7 | scope: string; 8 | }; 9 | } 10 | export interface IConfigurationService { 11 | getConfiguration(): IWebClipperConfiguration; 12 | 13 | init(): void; 14 | } 15 | 16 | export const IConfigurationService = new Token(); 17 | -------------------------------------------------------------------------------- /src/services/configuration/common/configurationService.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webclipper/web-clipper/dc978f0154de435824ee3435c5ff4483299f1fdf/src/services/configuration/common/configurationService.ts -------------------------------------------------------------------------------- /src/services/environment/common/environment.ts: -------------------------------------------------------------------------------- 1 | import { Token } from 'typedi'; 2 | 3 | export const IEnvironmentService = new Token(); 4 | 5 | export interface IEnvironmentService { 6 | privacy(): Promise; 7 | changelog(): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/services/environment/common/environmentService.ts: -------------------------------------------------------------------------------- 1 | import { ILocaleService } from '@/service/common/locale'; 2 | import { Inject, Service } from 'typedi'; 3 | import { IEnvironmentService } from './environment'; 4 | 5 | //@ts-ignore 6 | import ChangelogEnUS from './changelog/CHANGELOG.en-US.md'; 7 | //@ts-ignore 8 | import ChangelogZhCN from './changelog/CHANGELOG.zh-CN.md'; 9 | //@ts-ignore 10 | import PrivacyEnUS from './privacy/PRIVACY.en-US.md'; 11 | //@ts-ignore 12 | import PrivacyZhCN from './privacy/PRIVACY.zh-CN.md'; 13 | 14 | const privacyLocale = { 15 | 'en-US': PrivacyEnUS, 16 | 'zh-CN': PrivacyZhCN, 17 | } as const; 18 | 19 | const changelogLocale = { 20 | 'en-US': ChangelogEnUS, 21 | 'zh-CN': ChangelogZhCN, 22 | } as const; 23 | 24 | type Locale = 'en-US' | 'zh-CN'; 25 | 26 | function keys(data: T): (keyof T)[] { 27 | return (Object.keys(data as any) as any) as (keyof T)[]; 28 | } 29 | 30 | export class EnvironmentService implements IEnvironmentService { 31 | constructor(@Inject(ILocaleService) private localeService: ILocaleService) {} 32 | 33 | async privacy(): Promise { 34 | let workLocale: Locale = 'en-US'; 35 | if (keys(privacyLocale).some(o => o === this.localeService.locale)) { 36 | workLocale = this.localeService.locale as Locale; 37 | } 38 | return privacyLocale[workLocale]; 39 | } 40 | 41 | async changelog(): Promise { 42 | let workLocale: Locale = 'en-US'; 43 | if (Object.keys(changelogLocale).some(o => o === this.localeService.locale)) { 44 | workLocale = this.localeService.locale as Locale; 45 | } 46 | return changelogLocale[workLocale]; 47 | } 48 | } 49 | 50 | Service(IEnvironmentService)(EnvironmentService); 51 | -------------------------------------------------------------------------------- /src/services/environment/common/privacy/PRIVACY.zh-CN.md: -------------------------------------------------------------------------------- 1 | # 隐私政策 2 | 3 | 作为许多其他互联网服务的用户,我们认识到隐私非常重要。下面概述了 Web Clipper 隐私策略的更多详细信息。 4 | 5 | ## 您的数据保存在哪里 6 | 7 | Web Clipper 的服务器位于美利坚合众国的 Mongodb Cloud 和 zeit.co。 8 | 9 | ## 我们收集的数据 10 | 11 | 当您注册 Web Clipper 服务或以其他方式自愿提供此类信息时,Web Clipper 只会收集“电子邮件地址”。 **我们不会收集其他个人信息。** 12 | 13 | Web Clipper 使用 Github OAuth 从您的 Github 帐户注册 Web Clipper 帐户。我们不会将密码和其他信息保存到 Web Clipper。因此,无需担心密码泄漏。 14 | 15 | Web Clipper 使用 cookie 和其他技术来增强您的在线体验,并了解您如何使用该服务,从而提高我们的服务质量。 16 | 17 | ## 使用 18 | 19 | 日志信息主要用于 Google Analytics(分析)中的使用情况分析,例如有多少用户访问 X 功能等。我们不会收集和分析个人信息。我们还可能将日志信息用于审核,研究和分析,以操作和改进 Web Clipper 技术和服务。如果上述信息以外的任何其他方式使用信息,则该信息将采用汇总形式,并删除所有个人身份信息。 Web Clipper 不会将用户存储在服务器中的信息透露给任何第三方。 20 | 21 | ## 安全 22 | 23 | Web Clipper 采取了预防措施,以确保成员帐户信息保持私有。我们使用合理的措施来保护存储在数据库中的会员信息,并且将访问会员信息的权限限制为需要访问才能执行其工作职能的那些员工,例如我们的客户服务人员和技术人员。请注意,我们不能保证会员帐户信息的安全性。未经授权的输入或使用,硬件或软件故障以及其他因素可能随时损害成员信息的安全性。 24 | 25 | ## 本隐私政策的变更 26 | 27 | 在本页面上发布对本隐私政策的更改后,这些更改才生效。更改历史记录将保存在此 [存储库](https://github.com/webclipper/web-clipper) 中。 28 | 29 | 如果对我们的隐私政策有任何疑问,请通过**admin@diamonyuan.com**与我们联系。 30 | -------------------------------------------------------------------------------- /src/services/log/common/index.ts: -------------------------------------------------------------------------------- 1 | export enum LogLevel { 2 | Trace, 3 | Debug, 4 | Info, 5 | Warning, 6 | Error, 7 | Critical, 8 | Off, 9 | } 10 | 11 | export const DEFAULT_LOG_LEVEL: LogLevel = LogLevel.Info; 12 | 13 | export interface ILogger { 14 | getLevel(): LogLevel; 15 | setLevel(level: LogLevel): void; 16 | 17 | trace(message: string, ...args: any[]): void; 18 | debug(message: string, ...args: any[]): void; 19 | info(message: string, ...args: any[]): void; 20 | warn(message: string, ...args: any[]): void; 21 | error(message: string | Error, ...args: any[]): void; 22 | critical(message: string | Error, ...args: any[]): void; 23 | 24 | flush(): void; 25 | } 26 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /src/vendor/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less'; 2 | declare module '*.png'; 3 | declare module '@web-clipper/readability'; 4 | declare module 'turndown-plugin-gfm'; 5 | declare module '@web-clipper/remark-pangu'; 6 | declare module 'dva-loading'; 7 | 8 | type PromiseType> = T extends Promise ? U : never; 9 | 10 | type Unpack = T extends Promise ? U : T; 11 | // eslint-disable-next-line no-unused-vars 12 | type CallResult any> = Unpack>; 13 | 14 | interface Type extends Function { 15 | new (...args: any[]): T; 16 | } 17 | 18 | /// 19 | 20 | interface Window { 21 | _gaq: string[][]; 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "resolveJsonModule": true, 5 | "experimentalDecorators": true, 6 | "target": "ES2018", 7 | "module": "CommonJS", 8 | "declaration": false, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "rootDir": ".", 12 | "outDir": "lib", 13 | "strict": true, 14 | "removeComments": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "allowSyntheticDefaultImports": true, 18 | "moduleResolution": "node", 19 | "sourceMap": true, 20 | "lib": ["dom", "es2020", "es5", "ESNext.String"], 21 | "inlineSources": true, 22 | "esModuleInterop": true, 23 | "jsx": "react", 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["src/*"], 27 | "common/*": ["src/common/*"], 28 | "components/*": ["src/components/*"], 29 | "pageActions/*": ["src/actions/*"], 30 | "extensions/*": ["src/extensions/*"], 31 | "browserActions/*": ["src/browser/actions/*"] 32 | } 33 | }, 34 | "include": ["src/**/*"], 35 | "exclude": ["./node_modules/*", "lib", "es"] 36 | } 37 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | globals: true, 7 | testTimeout: 10000, 8 | setupFiles: path.resolve(__dirname, 'src/setupTests.ts'), 9 | }, 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(__dirname, './src'), 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development'; 2 | 3 | const merge = require('webpack-merge'); 4 | const common = require('./webpack.common.js'); 5 | 6 | module.exports = merge(common, { 7 | devtool: 'source-map', 8 | mode: 'development', 9 | watchOptions: { 10 | ignored: /dist/, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'production', 7 | optimization: { 8 | minimize: true, 9 | minimizer: [ 10 | new TerserPlugin({ 11 | terserOptions: { 12 | keep_classnames: true, 13 | keep_fnames: true, 14 | }, 15 | }), 16 | ], 17 | }, 18 | }); 19 | --------------------------------------------------------------------------------