├── .babelrc ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── .vscodeignore ├── .yarnclean ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── media ├── editNote.svg ├── logo-dark.svg ├── logo-light.svg ├── logo.svg ├── logo128.png ├── logo192.png ├── logo512.png ├── logo64.png ├── notes.svg ├── refresh.svg └── reload.svg ├── package.json ├── public └── deps │ ├── README.md │ ├── echarts-gl │ └── echarts-gl.min.js │ ├── echarts │ └── echarts.min.js │ ├── html2pdf.js │ └── html2pdf.bundle.min.js │ ├── marked │ └── marked.min.js │ ├── mermaid │ └── mermaid.min.js │ ├── plantuml-encoder │ └── plantuml-encoder.min.js │ ├── prism │ ├── prism.css │ └── prism.js │ ├── vega-embed │ └── vega-embed.min.js │ ├── vega-lite │ └── vega-lite.min.js │ ├── vega │ └── vega.min.js │ ├── wavedrom │ ├── skins │ │ └── default.js │ └── wavedrom.min.js │ └── yamljs │ └── yaml.min.js ├── src ├── extension.ts ├── extension │ ├── EditorPanelWebviewPanel.ts │ ├── NotesPanelWebviewPanel.ts │ ├── TreeItem.ts │ ├── TreeView.ts │ ├── WebviewConfig.ts │ └── settings.ts ├── lib │ ├── crossnote.ts │ ├── keymap.ts │ ├── message.ts │ ├── note.ts │ ├── notebook.ts │ ├── section.ts │ └── settings.ts ├── util │ ├── image_uploader.ts │ └── util.ts └── views │ ├── EditorPanelWebview.tsx │ ├── NotesPanelWebview.tsx │ ├── components │ ├── ChangeFilePathDialog.tsx │ ├── DeleteDialog.tsx │ ├── EditImageDialog.tsx │ ├── EditorPanel.tsx │ ├── NoteCard.tsx │ ├── Notes.tsx │ ├── NotesPanel.tsx │ └── TagsMenuPopover.tsx │ ├── editor │ ├── index.ts │ ├── views │ │ ├── README.md │ │ ├── float-win.ts │ │ └── math-preview.ts │ └── widgets │ │ ├── README.md │ │ ├── abc │ │ └── index.tsx │ │ ├── audio │ │ └── index.tsx │ │ ├── bilibili │ │ └── index.tsx │ │ ├── github_gist │ │ └── index.tsx │ │ ├── image │ │ └── index.tsx │ │ ├── kanban │ │ └── index.tsx │ │ ├── netease_music │ │ └── index.tsx │ │ ├── ocr │ │ └── index.tsx │ │ ├── timer │ │ └── index.tsx │ │ ├── video │ │ └── index.tsx │ │ └── youtube │ │ └── index.tsx │ ├── i18n │ ├── i18n.ts │ └── lang │ │ ├── enUS.ts │ │ ├── jaJP.ts │ │ ├── zhCN.ts │ │ └── zhTW.ts │ ├── index.css │ ├── index.less │ ├── themes │ ├── dark.ts │ ├── light.ts │ ├── manager.ts │ ├── one-dark.ts │ ├── solarized-light.ts │ └── theme.ts │ └── util │ ├── markdown.ts │ ├── preview.ts │ └── util.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | "@babel/preset-react" 6 | ], 7 | "plugins": ["@babel/plugin-transform-runtime"] 8 | } 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [shd101wyy] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | # Crossnote 108 | out 109 | *.vsix 110 | *.zip 111 | public/styles -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Run Extension", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/out/**/*.js" 17 | ], 18 | "preLaunchTask": "npm: watch" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | styles/src/** 11 | webpack.config.js 12 | **/*.ts 13 | **/*.tsx 14 | **/*.scss 15 | **/*.less 16 | *.vsix 17 | *.zip 18 | 19 | # Manually include the node modules that we used 20 | node_modules 21 | !node_modules/tslib 22 | !node_modules/yamljs 23 | !node_modules/crypto-js 24 | !node_modules/crypto-random-string 25 | !node_modules/mkdirp 26 | !node_modules/slash 27 | !node_modules/vickymd/theme -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | appveyor.yml 29 | circle.yml 30 | codeship-services.yml 31 | codeship-steps.yml 32 | wercker.yml 33 | .tern-project 34 | .gitattributes 35 | .editorconfig 36 | .*ignore 37 | .eslintrc 38 | .jshintrc 39 | .flowconfig 40 | .documentup.json 41 | .yarn-metadata.json 42 | .travis.yml 43 | 44 | # misc 45 | *.md 46 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Clone this project, open it in VSCode. 4 | 5 | ```bash 6 | $ yarn # install all dependencies 7 | $ yarn watch # watch mode 8 | $ yarn package # production build 9 | ``` 10 | 11 | Check the `package.json` file. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: "2020-04-12T12:45:54.675Z" 3 | modified: "2020-04-12T13:01:03.840Z" 4 | tags: [] 5 | --- 6 | 7 | ![](./media/logo128.png) 8 | 9 | # vscode-crossnote 10 | 11 | (WIP) Turn your VSCode into a decent markdown note taking platform :grinning: 12 | 13 | **This project is still in its very early stage of development. There are lots of bugs and missing features :frog:** 14 | 15 | [![](https://img.shields.io/github/tag/0xGG/vscode-crossnote.svg)](https://github.com/0xGG/vscode-crossnote/releases) [![](https://img.shields.io/visual-studio-marketplace/azure-devops/installs/total/shd101wyy.crossnote)](https://marketplace.visualstudio.com/items?itemName=shd101wyy.crossnote) [![](https://img.shields.io/github/stars/0xGG/vscode-crossnote.svg?style=social&label=Star)](https://github.com/0xGG/vscode-crossnote) 16 | 17 | [![](https://img.shields.io/badge/Supported%20by-VSCode%20Power%20User%20Course%20%E2%86%92-gray.svg?colorA=655BE1&colorB=4F44D6&style=for-the-badge)](https://a.paddle.com/v2/click/16413/111518?link=1227) 18 | 19 | ![](https://i.loli.net/2020/04/12/lTMIFCuUcyeSGjV.gif) 20 | 21 | The goal of this project is to be consistent with the web version [crossnote.app](https://crossnote.app) | [GitHub 0xGG/crossnote](https://github.com/0xGG/crossnote). 22 | You can read the introduction notebook of the Crossnote project directly on [Crossnote](https://crossnote.app/?repo=https%3A%2F%2Fgithub.com%2F0xGG%2Fwelcome-notebook&branch=master&filePath=README.md) website or on [GitHub 0xGG/welcome-notebook](https://github.com/0xGG/welcome-notebook). 23 | Right now many key pieces are missing, such as: 24 | 25 | - [x] Dark theme (We now support `dark`, `one-dark`, etc themes) 26 | - [ ] Crossnote cloud platform integration 27 | - [ ] Cloud widgets support 28 | - [ ] Notification system 29 | - [ ] User settings synchronization 30 | - [ ] etc.. 31 | 32 | ## Settings 33 | 34 | Please go to the vscode extension settings, then search for: 35 | 36 | - `crossnote.theme`: Change the Crossnote theme. Please reload the window after you make changes. 37 | - `crossnote.keyMap`: Change the Crossnote editor key map. `default`, `vim`, and `emacs` key maps are supported. 38 | 39 | ## Thanks 40 | 41 | - The WYSIWYG editor [0xGG/VickyMD](https://github.com/0xGG/VickyMD) was built on top of [laobubu/HyperMD](https://github.com/laobubu/HyperMD/). 42 | - Icons made by [srip](https://www.flaticon.com/authors/srip) from [www.flaticon.com](https://www.flaticon.com/). 43 | - And many more that support this project! :grin: 44 | 45 | ## License 46 | 47 | AGPL3 48 | -------------------------------------------------------------------------------- /media/editNote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /media/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /media/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /media/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xGG/vscode-crossnote/57139615ecba530492c9985d823e787a098afbeb/media/logo128.png -------------------------------------------------------------------------------- /media/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xGG/vscode-crossnote/57139615ecba530492c9985d823e787a098afbeb/media/logo192.png -------------------------------------------------------------------------------- /media/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xGG/vscode-crossnote/57139615ecba530492c9985d823e787a098afbeb/media/logo512.png -------------------------------------------------------------------------------- /media/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xGG/vscode-crossnote/57139615ecba530492c9985d823e787a098afbeb/media/logo64.png -------------------------------------------------------------------------------- /media/notes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /media/reload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crossnote", 3 | "displayName": "Crossnote", 4 | "description": "(WIP) Turn your VSCode into a decent markdown note taking platform", 5 | "version": "0.1.7", 6 | "publisher": "shd101wyy", 7 | "main": "./out/extension.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/0xGG/vscode-crossnote.git" 11 | }, 12 | "author": "Yiyi Wang ", 13 | "license": "AGPL-3.0-or-later", 14 | "engines": { 15 | "vscode": "^1.43.0" 16 | }, 17 | "categories": [ 18 | "Other" 19 | ], 20 | "icon": "media/logo128.png", 21 | "preview": true, 22 | "scripts": { 23 | "test": "echo \"Error: no test specified\" && exit 1", 24 | "vscode:prepublish": "npm run compile", 25 | "compile": "npm-run-all compile:*", 26 | "compile:extension": "tsc -p ./", 27 | "compile:views": "webpack --mode production", 28 | "watch": "npm-run-all -p watch:*", 29 | "watch:extension": "tsc -watch -p ./", 30 | "watch:views": "webpack --watch --mode development", 31 | "lint": "tslint -p ./", 32 | "package": "vsce package --yarn", 33 | "postinstall": "patch-package" 34 | }, 35 | "activationEvents": [ 36 | "onLanguage:markdown", 37 | "onView:crossnoteTreeView" 38 | ], 39 | "contributes": { 40 | "commands": [ 41 | { 42 | "command": "crossnote.refreshTreeView", 43 | "title": "Refresh", 44 | "icon": { 45 | "light": "media/refresh.svg", 46 | "dark": "media/refresh.svg" 47 | } 48 | }, 49 | { 50 | "command": "crossnote.openNoteInEditor", 51 | "title": "Crossnote: Open Note in Editor", 52 | "category": "Markdown", 53 | "icon": { 54 | "light": "media/logo-light.svg", 55 | "dark": "media/logo-dark.svg" 56 | } 57 | } 58 | ], 59 | "viewsContainers": { 60 | "activitybar": [ 61 | { 62 | "id": "crossnote", 63 | "title": "Crossnote", 64 | "icon": "media/logo.svg" 65 | } 66 | ] 67 | }, 68 | "views": { 69 | "crossnote": [ 70 | { 71 | "id": "crossnoteTreeView", 72 | "name": "Crossnote" 73 | } 74 | ] 75 | }, 76 | "menus": { 77 | "view/title": [ 78 | { 79 | "command": "crossnote.refreshTreeView", 80 | "when": "view == crossnoteTreeView", 81 | "group": "navigation" 82 | } 83 | ], 84 | "editor/context": [ 85 | { 86 | "command": "crossnote.openNoteInEditor", 87 | "when": "editorLangId == markdown", 88 | "group": "crossnote" 89 | } 90 | ], 91 | "editor/title": [ 92 | { 93 | "command": "crossnote.openNoteInEditor", 94 | "when": "editorLangId == markdown", 95 | "group": "navigation" 96 | } 97 | ] 98 | }, 99 | "configuration": { 100 | "title": "Crossnote", 101 | "properties": { 102 | "crossnote.theme": { 103 | "description": "Crossnote theme (Please reload window after make changes)", 104 | "default": "dark", 105 | "type": "string", 106 | "enum": [ 107 | "light", 108 | "dark", 109 | "solarized-light", 110 | "one-dark" 111 | ] 112 | }, 113 | "crossnote.keyMap": { 114 | "description": "Crossnote editor key map (Please reload window after make changes)", 115 | "default": "default", 116 | "enum": [ 117 | "default", 118 | "vim", 119 | "emacs" 120 | ] 121 | } 122 | } 123 | } 124 | }, 125 | "dependencies": { 126 | "@lourenci/react-kanban": "^1.1.0", 127 | "@material-ui/core": "^4.9.12", 128 | "@material-ui/icons": "^4.9.1", 129 | "@material-ui/lab": "^4.0.0-alpha.51", 130 | "@material-ui/pickers": "^3.2.10", 131 | "@material-ui/styles": "^4.9.10", 132 | "@mdi/font": "^5.1.45", 133 | "@use-it/interval": "^0.1.3", 134 | "abcjs": "^5.12.0", 135 | "clsx": "^1.1.0", 136 | "codemirror": "^5.55.0", 137 | "crypto-js": "^4.0.0", 138 | "date-fns": "^2.12.0", 139 | "emoji-mart": "^3.0.0", 140 | "i18next": "^19.4.4", 141 | "identicon.js": "^2.3.3", 142 | "mdi-material-ui": "^6.14.0", 143 | "mkdirp": "^1.0.4", 144 | "noty": "^3.2.0-beta", 145 | "react": "^16.13.1", 146 | "react-dom": "^16.13.1", 147 | "react-i18next": "^11.4.0", 148 | "react-lazyload": "^2.6.7", 149 | "slash": "^3.0.0", 150 | "styled-components": "^5.1.0", 151 | "subscriptions-transport-ws": "^0.9.16", 152 | "super-react-gist": "^1.0.4", 153 | "tesseract.js": "^2.1.1", 154 | "tslint": "^6.1.2", 155 | "typeface-noto-sans-sc": "^0.0.71", 156 | "typeface-roboto": "^0.0.75", 157 | "typescript": "^3.8.3", 158 | "unstated-next": "^1.1.0", 159 | "vickymd": "^0.2.4", 160 | "yamljs": "^0.3.0" 161 | }, 162 | "devDependencies": { 163 | "@babel/core": "^7.9.0", 164 | "@babel/plugin-proposal-class-properties": "^7.8.3", 165 | "@babel/plugin-transform-runtime": "^7.9.0", 166 | "@babel/preset-env": "^7.9.5", 167 | "@babel/preset-react": "^7.9.4", 168 | "@babel/preset-typescript": "^7.9.0", 169 | "@babel/runtime": "^7.9.2", 170 | "@types/codemirror": "^0.0.91", 171 | "@types/crypto-js": "^3.1.45", 172 | "@types/date-fns": "^2.6.0", 173 | "@types/mkdirp": "^1.0.0", 174 | "@types/node": "^13.13.4", 175 | "@types/react": "16.9.34", 176 | "@types/react-dom": "^16.9.7", 177 | "@types/vscode": "1.43.0", 178 | "@types/yamljs": "^0.2.30", 179 | "babel-loader": "^8.1.0", 180 | "css-loader": "^3.5.3", 181 | "css-to-string-loader": "^0.1.3", 182 | "file-loader": "^6.0.0", 183 | "html-webpack-plugin": "^4.2.1", 184 | "less": "^3.11.1", 185 | "less-loader": "^6.0.0", 186 | "npm-run-all": "^4.1.5", 187 | "patch-package": "^6.2.2", 188 | "postinstall-postinstall": "^2.1.0", 189 | "style-loader": "^1.2.1", 190 | "url-loader": "^4.1.0", 191 | "webpack": "^4.43.0", 192 | "webpack-cli": "^3.3.11", 193 | "webpack-dev-server": "^3.10.3" 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /public/deps/README.md: -------------------------------------------------------------------------------- 1 | ```json 2 | { 3 | "prism": "1.17.1", 4 | "katex": "0.11.1", 5 | "mermaid": "8.4.8", 6 | "marked": "0.7.0", 7 | "plantuml-encoder": "1.4.0", 8 | "echarts": "4.6.0", 9 | "echarts-gl": "1.1.1", 10 | "wavedrom": "2.3.2", 11 | "yamljs": "0.3.0", 12 | "mume": "0.4.11", 13 | "vega": "5.9.2", 14 | "vega-lite": "4.4.0", 15 | "vega-embed": "6.3.2" 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /public/deps/prism/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.17.1 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+antlr4+apacheconf+apl+applescript+aql+c+arff+asciidoc+asm6502+csharp+autohotkey+autoit+bash+basic+batch+bison+bnf+brainfuck+bro+cpp+aspnet+arduino+cil+coffeescript+cmake+clojure+ruby+csp+css-extras+d+dart+diff+markup-templating+dns-zone-file+docker+ebnf+eiffel+ejs+elixir+elm+lua+erb+erlang+fsharp+firestore-security-rules+flow+fortran+ftl+gcode+gdscript+gedcom+gherkin+git+glsl+gml+go+graphql+groovy+less+handlebars+haskell+haxe+hcl+http+hpkp+hsts+ichigojam+icon+inform7+ini+io+j+java+scala+php+javastacktrace+jolie+jq+javadoclike+n4js+markdown+json+jsonp+json5+julia+keyman+kotlin+latex+crystal+scheme+liquid+lisp+livescript+lolcode+etlua+makefile+js-templates+django+matlab+mel+mizar+monkey+n1ql+typescript+nand2tetris-hdl+nasm+nginx+nim+nix+nsis+objectivec+ocaml+opencl+oz+parigp+parser+pascal+pascaligo+pcaxis+perl+jsdoc+phpdoc+php-extras+sql+powershell+processing+prolog+properties+protobuf+scss+puppet+pure+python+q+qore+r+js-extras+jsx+renpy+reason+vala+rest+rip+roboconf+robot-framework+textile+rust+sas+sass+stylus+javadoc+lilypond+shell-session+smalltalk+smarty+solidity+soy+turtle+splunk-spl+sqf+plsql+twig+swift+yaml+tcl+haml+toml+tt2+sparql+pug+tsx+t4-templating+visual-basic+t4-cs+regex+vbnet+velocity+verilog+vhdl+vim+t4-vb+wasm+wiki+xeora+xojo+xquery+tap+zig */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | text-shadow: 0 1px white; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 34 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 35 | text-shadow: none; 36 | background: #b3d4fc; 37 | } 38 | 39 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 40 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 41 | text-shadow: none; 42 | background: #b3d4fc; 43 | } 44 | 45 | @media print { 46 | code[class*="language-"], 47 | pre[class*="language-"] { 48 | text-shadow: none; 49 | } 50 | } 51 | 52 | /* Code blocks */ 53 | pre[class*="language-"] { 54 | padding: 1em; 55 | margin: .5em 0; 56 | overflow: auto; 57 | } 58 | 59 | :not(pre) > code[class*="language-"], 60 | pre[class*="language-"] { 61 | background: #f5f2f0; 62 | } 63 | 64 | /* Inline code */ 65 | :not(pre) > code[class*="language-"] { 66 | padding: .1em; 67 | border-radius: .3em; 68 | white-space: normal; 69 | } 70 | 71 | .token.comment, 72 | .token.prolog, 73 | .token.doctype, 74 | .token.cdata { 75 | color: slategray; 76 | } 77 | 78 | .token.punctuation { 79 | color: #999; 80 | } 81 | 82 | .namespace { 83 | opacity: .7; 84 | } 85 | 86 | .token.property, 87 | .token.tag, 88 | .token.boolean, 89 | .token.number, 90 | .token.constant, 91 | .token.symbol, 92 | .token.deleted { 93 | color: #905; 94 | } 95 | 96 | .token.selector, 97 | .token.attr-name, 98 | .token.string, 99 | .token.char, 100 | .token.builtin, 101 | .token.inserted { 102 | color: #690; 103 | } 104 | 105 | .token.operator, 106 | .token.entity, 107 | .token.url, 108 | .language-css .token.string, 109 | .style .token.string { 110 | color: #9a6e3a; 111 | background: hsla(0, 0%, 100%, .5); 112 | } 113 | 114 | .token.atrule, 115 | .token.attr-value, 116 | .token.keyword { 117 | color: #07a; 118 | } 119 | 120 | .token.function, 121 | .token.class-name { 122 | color: #DD4A68; 123 | } 124 | 125 | .token.regex, 126 | .token.important, 127 | .token.variable { 128 | color: #e90; 129 | } 130 | 131 | .token.important, 132 | .token.bold { 133 | font-weight: bold; 134 | } 135 | .token.italic { 136 | font-style: italic; 137 | } 138 | 139 | .token.entity { 140 | cursor: help; 141 | } 142 | 143 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from "vscode"; 4 | import { CrossnoteTreeViewProvider } from "./extension/TreeView"; 5 | import { Crossnote } from "./lib/crossnote"; 6 | 7 | export function isMarkdownFile(document: vscode.TextDocument) { 8 | return ( 9 | document.languageId === "markdown" && 10 | document.uri.scheme !== "markdown-preview-enhanced" 11 | ); // prevent processing of own documents 12 | } 13 | 14 | // this method is called when your extension is activated 15 | // your extension is activated the very first time the command is executed 16 | export function activate(context: vscode.ExtensionContext) { 17 | const crossnote = new Crossnote(context); 18 | vscode.workspace.workspaceFolders?.forEach((workspaceFolder) => { 19 | crossnote.addNotebook(workspaceFolder.name, workspaceFolder.uri.fsPath); 20 | }); 21 | 22 | const treeViewProvider = new CrossnoteTreeViewProvider(crossnote); 23 | const treeView = vscode.window.createTreeView("crossnoteTreeView", { 24 | treeDataProvider: treeViewProvider, 25 | }); 26 | crossnote.bindTreeViewRefresh(() => { 27 | treeViewProvider.refresh(); 28 | }); 29 | context.subscriptions.push( 30 | vscode.commands.registerCommand("crossnote.refreshTreeView", () => { 31 | treeViewProvider.refresh(); 32 | }) 33 | ); 34 | context.subscriptions.push( 35 | vscode.workspace.onDidChangeWorkspaceFolders((e) => { 36 | e.added.forEach((workspaceFolder) => { 37 | crossnote.addNotebook(workspaceFolder.name, workspaceFolder.uri.fsPath); 38 | }); 39 | e.removed.forEach((workspaceFolder) => { 40 | crossnote.removeNotebook(workspaceFolder.uri.fsPath); 41 | }); 42 | treeViewProvider.refresh(); 43 | }) 44 | ); 45 | context.subscriptions.push( 46 | vscode.workspace.onDidSaveTextDocument((document) => { 47 | if (isMarkdownFile(document)) { 48 | crossnote.updateNoteMarkdownIfNecessary(document.uri); 49 | } 50 | }) 51 | ); 52 | context.subscriptions.push( 53 | treeView.onDidChangeSelection((e) => { 54 | if (e.selection.length) { 55 | crossnote.openNotesPanelWebview(e.selection[0]); 56 | } 57 | }) 58 | ); 59 | 60 | context.subscriptions.push( 61 | vscode.commands.registerCommand( 62 | "crossnote.openNoteInEditor", 63 | (uri?: vscode.Uri) => { 64 | let resource = uri; 65 | if (!(resource instanceof vscode.Uri)) { 66 | if (vscode.window.activeTextEditor) { 67 | // we are relaxed and don't check for markdown files 68 | resource = vscode.window.activeTextEditor.document.uri; 69 | } 70 | } 71 | if (resource) { 72 | crossnote.openNoteByPath(resource.fsPath); 73 | } 74 | } 75 | ) 76 | ); 77 | 78 | /* 79 | // TODO: Support update settings in real-time 80 | context.subscriptions.push( 81 | vscode.workspace.onDidChangeConfiguration(() => { 82 | crossnote.updateConfiguration(); 83 | }) 84 | ); 85 | */ 86 | } 87 | 88 | // this method is called when your extension is deactivated 89 | export function deactivate() {} 90 | -------------------------------------------------------------------------------- /src/extension/EditorPanelWebviewPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { getWebviewCSP } from "./WebviewConfig"; 4 | import { VSCodeCrossnoteSettings } from "./settings"; 5 | 6 | export function createEditorPanelWebviewPanel( 7 | context: vscode.ExtensionContext, 8 | onDidDispose: () => void 9 | ) { 10 | const panel = vscode.window.createWebviewPanel( 11 | "editorPanel", 12 | "Editor Panel", 13 | vscode.ViewColumn.Two, 14 | { 15 | enableScripts: true, 16 | } 17 | ); 18 | 19 | const cssArr = [].map((filePath) => 20 | panel.webview.asWebviewUri(vscode.Uri.file(filePath)) 21 | ); 22 | 23 | const jsArr = [ 24 | // mermaid 25 | path.join(context.extensionPath, "./public/deps/mermaid/mermaid.min.js"), 26 | // marked 27 | path.join(context.extensionPath, "./public/deps/marked/marked.min.js"), 28 | // plantuml-encoder 29 | path.join( 30 | context.extensionPath, 31 | "./public/deps/plantuml-encoder/plantuml-encoder.min.js" 32 | ), 33 | // echarts 34 | path.join(context.extensionPath, "./public/deps/echarts/echarts.min.js"), 35 | path.join( 36 | context.extensionPath, 37 | "./public/deps/echarts-gl/echarts-gl.min.js" 38 | ), 39 | // wavedrom 40 | path.join(context.extensionPath, "./public/deps/wavedrom/skins/default.js"), 41 | path.join(context.extensionPath, "./public/deps/wavedrom/wavedrom.min.js"), 42 | // yamljs 43 | path.join(context.extensionPath, "./public/deps/yamljs/yaml.min.js"), 44 | // prismjs 45 | path.join(context.extensionPath, "./public/deps/prism/prism.js"), 46 | // vega 47 | path.join(context.extensionPath, "./public/deps/vega/vega.min.js"), 48 | path.join( 49 | context.extensionPath, 50 | "./public/deps/vega-lite/vega-lite.min.js" 51 | ), 52 | path.join( 53 | context.extensionPath, 54 | "./public/deps/vega-embed/vega-embed.min.js" 55 | ), 56 | 57 | path.join( 58 | context.extensionPath, 59 | "./out/views/EditorPanelWebview.bundle.js" 60 | ), 61 | ].map((filePath) => panel.webview.asWebviewUri(vscode.Uri.file(filePath))); 62 | 63 | panel.webview.html = getWebviewContent(context, panel, jsArr, cssArr); 64 | panel.iconPath = vscode.Uri.file( 65 | path.join(context.extensionPath, "media", "editNote.svg") 66 | ); 67 | 68 | panel.onDidDispose(onDidDispose, null, context.subscriptions); 69 | return panel; 70 | } 71 | 72 | function getWebviewContent( 73 | context: vscode.ExtensionContext, 74 | panel: vscode.WebviewPanel, 75 | jsArr: vscode.Uri[], 76 | cssArr: vscode.Uri[] 77 | ) { 78 | return ` 79 | 80 | 81 | 82 | 83 | 87 | ${getWebviewCSP(panel.webview)} 88 | ${cssArr 89 | .map((css) => ``) 90 | .join("\n")} 91 | 92 | 93 | 94 |
95 | 96 | 106 | ${jsArr.map((js) => ``).join("\n")} 107 | 108 | `; 109 | } 110 | -------------------------------------------------------------------------------- /src/extension/NotesPanelWebviewPanel.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { getWebviewCSP } from "./WebviewConfig"; 4 | import { VSCodeCrossnoteSettings } from "./settings"; 5 | 6 | export function createNotesPanelWebviewPanel( 7 | context: vscode.ExtensionContext, 8 | onDidDispose: () => void 9 | ) { 10 | const panel = vscode.window.createWebviewPanel( 11 | "notesPanel", 12 | "Notes Panel", 13 | vscode.ViewColumn.One, 14 | { 15 | enableScripts: true, 16 | } 17 | ); 18 | const cssArr = [].map((filePath) => 19 | panel.webview.asWebviewUri(vscode.Uri.file(filePath)) 20 | ); 21 | 22 | const jsArr = [ 23 | path.join(context.extensionPath, "./out/views/NotesPanelWebview.bundle.js"), 24 | ].map((filePath) => panel.webview.asWebviewUri(vscode.Uri.file(filePath))); 25 | 26 | panel.webview.html = getWebviewContent(context, panel, jsArr, cssArr); 27 | panel.iconPath = vscode.Uri.file( 28 | path.join(context.extensionPath, "media", "notes.svg") 29 | ); 30 | 31 | panel.onDidDispose(onDidDispose, null, context.subscriptions); 32 | return panel; 33 | } 34 | 35 | function getWebviewContent( 36 | context: vscode.ExtensionContext, 37 | panel: vscode.WebviewPanel, 38 | jsArr: vscode.Uri[], 39 | cssArr: vscode.Uri[] 40 | ) { 41 | return ` 42 | 43 | 44 | 45 | 46 | 50 | ${getWebviewCSP(panel.webview)} 51 | ${cssArr 52 | .map((css) => ``) 53 | .join("\n")} 54 | 55 | 56 | 57 |
58 | 59 | 69 | ${jsArr.map((js) => ``).join("\n")} 70 | 71 | `; 72 | } 73 | -------------------------------------------------------------------------------- /src/extension/TreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Notebook } from "../lib/notebook"; 3 | import { CrossnoteSectionType } from "../lib/section"; 4 | 5 | export class CrossnoteTreeItem extends vscode.TreeItem { 6 | public notebook: Notebook; 7 | public type: CrossnoteSectionType; 8 | public path: string; 9 | constructor( 10 | label: string, 11 | collapsibleState: vscode.TreeItemCollapsibleState, 12 | notebook: Notebook, 13 | type: CrossnoteSectionType, 14 | path: string 15 | ) { 16 | super(label, collapsibleState); 17 | this.notebook = notebook; 18 | this.type = type; 19 | this.path = path; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/extension/TreeView.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { Crossnote } from "../lib/crossnote"; 3 | import { CrossnoteTreeItem } from "./TreeItem"; 4 | import { CrossnoteSectionType } from "../lib/section"; 5 | 6 | export class CrossnoteTreeViewProvider 7 | implements vscode.TreeDataProvider { 8 | private _onDidChangeTreeData: vscode.EventEmitter< 9 | CrossnoteTreeItem | undefined 10 | > = new vscode.EventEmitter(); 11 | readonly onDidChangeTreeData: vscode.Event< 12 | CrossnoteTreeItem | undefined 13 | > = this._onDidChangeTreeData.event; 14 | 15 | constructor(private crossnote: Crossnote) {} 16 | 17 | getTreeItem(element: CrossnoteTreeItem): vscode.TreeItem { 18 | return element; 19 | } 20 | 21 | async getChildren(element?: CrossnoteTreeItem): Promise { 22 | if (!this.crossnote.notebooks.length) { 23 | vscode.window.showInformationMessage("No notebooks found"); 24 | return []; 25 | } 26 | 27 | if (element) { 28 | // Open specific element 29 | if (element.type === CrossnoteSectionType.Notebook) { 30 | try { 31 | const notebook = element.notebook; 32 | await notebook.initData(); 33 | this.crossnote.refreshNotesPanelWebview(); 34 | const treeItems: CrossnoteTreeItem[] = [ 35 | new CrossnoteTreeItem( 36 | "📅 " + "Today", 37 | vscode.TreeItemCollapsibleState.None, 38 | notebook, 39 | CrossnoteSectionType.Today, 40 | "." 41 | ), 42 | new CrossnoteTreeItem( 43 | "☑️ " + "Todo", 44 | vscode.TreeItemCollapsibleState.None, 45 | notebook, 46 | 47 | CrossnoteSectionType.Todo, 48 | "." 49 | ), 50 | new CrossnoteTreeItem( 51 | "📁 " + "Notes", 52 | notebook.rootDirectory?.children.length 53 | ? vscode.TreeItemCollapsibleState.Collapsed 54 | : vscode.TreeItemCollapsibleState.None, 55 | notebook, 56 | 57 | CrossnoteSectionType.Notes, 58 | "." 59 | ), 60 | new CrossnoteTreeItem( 61 | "🏷️ " + "Tagged", 62 | notebook.rootTagNode?.children.length 63 | ? vscode.TreeItemCollapsibleState.Collapsed 64 | : vscode.TreeItemCollapsibleState.None, 65 | notebook, 66 | CrossnoteSectionType.Tagged, 67 | "." 68 | ), 69 | new CrossnoteTreeItem( 70 | "🈚 " + "Untagged", 71 | vscode.TreeItemCollapsibleState.None, 72 | notebook, 73 | CrossnoteSectionType.Untagged, 74 | "." 75 | ), 76 | new CrossnoteTreeItem( 77 | "🔐 " + "Encrypted", 78 | vscode.TreeItemCollapsibleState.None, 79 | notebook, 80 | CrossnoteSectionType.Encrypted, 81 | "." 82 | ), 83 | ]; 84 | return treeItems; 85 | } catch (error) { 86 | return [ 87 | new CrossnoteTreeItem( 88 | "⚠️ " + "Failed to load", 89 | vscode.TreeItemCollapsibleState.None, 90 | element.notebook, 91 | CrossnoteSectionType.Error, 92 | "." 93 | ), 94 | ]; 95 | } 96 | } else if ( 97 | element.type === CrossnoteSectionType.Notes || 98 | element.type === CrossnoteSectionType.Directory 99 | ) { 100 | const dirArr = element.path.split("/"); 101 | let directory = element.notebook.rootDirectory; 102 | let children = directory?.children; 103 | if (element.path !== ".") { 104 | for (let i = 0; i < dirArr.length; i++) { 105 | directory = children?.find((dir) => dir.name === dirArr[i]); 106 | children = directory?.children; 107 | } 108 | } 109 | return (children || []).map((child) => { 110 | return new CrossnoteTreeItem( 111 | "📁 " + child.name, 112 | child.children.length 113 | ? vscode.TreeItemCollapsibleState.Collapsed 114 | : vscode.TreeItemCollapsibleState.None, 115 | element.notebook, 116 | CrossnoteSectionType.Directory, 117 | child.path 118 | ); 119 | }); 120 | } else if ( 121 | element.type === CrossnoteSectionType.Tagged || 122 | element.type === CrossnoteSectionType.Tag 123 | ) { 124 | const tagArr = element.path.split("/"); 125 | let tagNode = element.notebook.rootTagNode; 126 | let children = tagNode?.children; 127 | if (element.path !== ".") { 128 | for (let i = 0; i < tagArr.length; i++) { 129 | tagNode = children?.find((tn) => tn.name === tagArr[i]); 130 | children = tagNode?.children; 131 | } 132 | } 133 | return (children || []).map((child) => { 134 | return new CrossnoteTreeItem( 135 | "🏷️ " + child.name, 136 | child.children.length 137 | ? vscode.TreeItemCollapsibleState.Collapsed 138 | : vscode.TreeItemCollapsibleState.None, 139 | element.notebook, 140 | CrossnoteSectionType.Tag, 141 | child.path 142 | ); 143 | }); 144 | } else { 145 | return []; 146 | } 147 | } else { 148 | // Read all notebooks 149 | return this.crossnote.notebooks.map((notebook) => { 150 | return new CrossnoteTreeItem( 151 | "📔 " + notebook.name, 152 | vscode.TreeItemCollapsibleState.Collapsed, 153 | notebook, 154 | CrossnoteSectionType.Notebook, 155 | "." // workspaceFolder.uri.fsPath 156 | ); 157 | }); 158 | } 159 | } 160 | 161 | public refresh(): void { 162 | this._onDidChangeTreeData.fire(); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/extension/WebviewConfig.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | export function getWebviewCSP(webview: vscode.Webview) { 3 | return ``; 7 | } 8 | -------------------------------------------------------------------------------- /src/extension/settings.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { CrossnoteSettings } from "../lib/settings"; 3 | import { KeyMap, getKeyMap } from "../lib/keymap"; 4 | 5 | export class VSCodeCrossnoteSettings implements CrossnoteSettings { 6 | public static getCurrentSettings() { 7 | return new VSCodeCrossnoteSettings(); 8 | } 9 | 10 | public readonly theme: string; 11 | public readonly keyMap: KeyMap; 12 | 13 | constructor() { 14 | const config = vscode.workspace.getConfiguration("crossnote"); 15 | this.theme = config.get("theme") || "dark"; 16 | this.keyMap = getKeyMap(config.get("keyMap") || "default"); 17 | } 18 | 19 | public isEqualTo(otherConfig: VSCodeCrossnoteSettings) { 20 | const json1 = JSON.stringify(this); 21 | const json2 = JSON.stringify(otherConfig); 22 | return json1 === json2; 23 | } 24 | 25 | [key: string]: any; 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/keymap.ts: -------------------------------------------------------------------------------- 1 | export enum KeyMap { 2 | "DEFAULT" = "hypermd", 3 | "VIM" = "vim", 4 | "EMACS" = "emacs", 5 | } 6 | 7 | export function getKeyMap(v: string): KeyMap { 8 | if (v === "hypermd" || v === "default") { 9 | return KeyMap.DEFAULT; 10 | } else if (v === "vim") { 11 | return KeyMap.VIM; 12 | } else if (v === "emacs") { 13 | return KeyMap.EMACS; 14 | } else { 15 | return KeyMap.DEFAULT; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/message.ts: -------------------------------------------------------------------------------- 1 | import { Note } from "./note"; 2 | import { SelectedSection } from "./section"; 3 | import { TagNode } from "./notebook"; 4 | 5 | export enum MessageAction { 6 | SelectedSection = "SelectedSection", 7 | SetSelectedSection = "SetSelectedSection", 8 | SelectedNote = "SelectedNote", 9 | SendNotes = "SendNotes", 10 | InitializedNotesPanelWebview = "InitializedNotesPanelWebview", 11 | InitializedEditorPanelWebview = "InitializedEditorPanelWebview", 12 | CreateNewNote = "CreateNewNote", 13 | CreatedNewNote = "CreatedNewNote", 14 | OpenNote = "OpenNote", 15 | OpenNoteIfNoNoteSelected = "OpenNoteIfNoNoteSelected", 16 | SendNote = "SendNote", 17 | UpdateNote = "UpdateNote", 18 | UpdatedNote = "UpdatedNote", 19 | SendNotebookTagNode = "SendNotebookTagNode", 20 | DeleteNote = "DeleteNote", 21 | ChangeNoteFilePath = "ChangeNoteFilePath", 22 | DuplicateNote = "DuplicateNote", 23 | OpenURL = "OpenURL", 24 | } 25 | 26 | export interface SendNotesMessage { 27 | action: MessageAction.SendNotes; 28 | data: Note[]; 29 | } 30 | 31 | export interface SelectedSectionMessage { 32 | action: MessageAction.SelectedSection; 33 | data: SelectedSection; 34 | } 35 | 36 | export interface SetSelectedSectionMessage { 37 | action: MessageAction.SetSelectedSection; 38 | data: SelectedSection; 39 | } 40 | 41 | export interface SelectedNoteMessage { 42 | action: MessageAction.SelectedNote; 43 | data: Note; 44 | } 45 | 46 | export interface InitializedNotesPanelWebviewMessage { 47 | action: MessageAction.InitializedNotesPanelWebview; 48 | data: any; 49 | } 50 | 51 | export interface InitializedEditorPanelWebviewMessage { 52 | action: MessageAction.InitializedEditorPanelWebview; 53 | data: any; 54 | } 55 | 56 | export interface CreateNewNoteMessage { 57 | action: MessageAction.CreateNewNote; 58 | data: SelectedSection; 59 | } 60 | 61 | export interface CreatedNewNoteMessage { 62 | action: MessageAction.CreatedNewNote; 63 | data: Note; 64 | } 65 | 66 | export interface OpenNoteMessage { 67 | action: MessageAction.OpenNote; 68 | data: Note; 69 | } 70 | 71 | export interface OpenNoteIfNoNoteSelected { 72 | action: MessageAction.OpenNoteIfNoNoteSelected; 73 | data: Note; 74 | } 75 | 76 | export interface SendNoteMessage { 77 | action: MessageAction.SendNote; 78 | data: Note; 79 | } 80 | 81 | export interface UpdateNoteMessage { 82 | action: MessageAction.UpdateNote; 83 | data: { 84 | note: Note; 85 | markdown: string; 86 | password: string; 87 | }; 88 | } 89 | 90 | export interface UpdatedNoteMessage { 91 | action: MessageAction.UpdatedNote; 92 | data: Note; 93 | } 94 | 95 | export interface SendNotebookTagNodeMessage { 96 | action: MessageAction.SendNotebookTagNode; 97 | data: TagNode | undefined; 98 | } 99 | 100 | export interface DeleteNoteMessage { 101 | action: MessageAction.DeleteNote; 102 | data: Note; 103 | } 104 | 105 | export interface ChangeNoteFilePathMessage { 106 | action: MessageAction.ChangeNoteFilePath; 107 | data: { 108 | note: Note; 109 | newFilePath: string; 110 | }; 111 | } 112 | 113 | export interface DuplicateNoteMessage { 114 | action: MessageAction.DuplicateNote; 115 | data: Note; 116 | } 117 | 118 | export interface OpenURLMessage { 119 | action: MessageAction.OpenURL; 120 | data: { 121 | note: Note; 122 | url: string; 123 | }; 124 | } 125 | 126 | export type Message = 127 | | SendNotesMessage 128 | | SelectedSectionMessage 129 | | SetSelectedSectionMessage 130 | | SelectedNoteMessage 131 | | InitializedNotesPanelWebviewMessage 132 | | InitializedEditorPanelWebviewMessage 133 | | CreateNewNoteMessage 134 | | CreatedNewNoteMessage 135 | | OpenNoteMessage 136 | | OpenNoteIfNoNoteSelected 137 | | SendNoteMessage 138 | | UpdateNoteMessage 139 | | UpdatedNoteMessage 140 | | SendNotebookTagNodeMessage 141 | | DeleteNoteMessage 142 | | ChangeNoteFilePathMessage 143 | | DuplicateNoteMessage 144 | | OpenURLMessage; 145 | -------------------------------------------------------------------------------- /src/lib/note.ts: -------------------------------------------------------------------------------- 1 | import { UUIDNil } from "../util/util"; 2 | 3 | export interface NoteConfigEncryption { 4 | title: string; 5 | // method: string;? // Default AES256 6 | } 7 | 8 | export interface NoteConfig { 9 | createdAt: Date; 10 | modifiedAt: Date; 11 | tags?: string[]; 12 | pinned?: boolean; 13 | encryption?: NoteConfigEncryption; 14 | } 15 | 16 | export interface Note { 17 | notebookPath: string; 18 | filePath: string; 19 | markdown: string; 20 | config: NoteConfig; 21 | } 22 | 23 | export interface Summary { 24 | title: string; 25 | summary: string; 26 | images: string[]; // If has `title`, then images[0] is cover. 27 | tags: string[]; // TODO: support tags 28 | html: string; // original html 29 | } 30 | 31 | /* 32 | export async function getTopicsAndMentionsFromHTML( 33 | html: string, 34 | ribbit: Ribbit 35 | ): Promise<{ 36 | topics: string[]; 37 | mentions: { name: string; address: string }[]; 38 | }> { 39 | const div = document.createElement("div"); 40 | const topics = []; 41 | const mentions = []; 42 | div.innerHTML = html; 43 | 44 | const tagElems = div.getElementsByClassName("tag"); 45 | for (let i = 0; i < tagElems.length; i++) { 46 | const tagElem = tagElems[i] as HTMLAnchorElement; 47 | if (tagElem.classList.contains("tag-mention")) { 48 | const mention = tagElem.getAttribute("data-mention"); 49 | const userInfo = await ribbit.getUserInfoFromUsername(mention); 50 | mentions.push({ 51 | name: userInfo.username, 52 | address: userInfo.address 53 | }); 54 | } else if (tagElem.classList.contains("tag-topic")) { 55 | const topic = tagElem.getAttribute("data-topic"); 56 | topics.push(topic); 57 | } 58 | } 59 | div.remove(); 60 | 61 | return { 62 | topics: Array.from(new Set(topics)), 63 | mentions 64 | }; 65 | } 66 | */ 67 | 68 | export function getHeaderFromMarkdown(markdown: string): string { 69 | const titleMatch = markdown.match(/^#\s.+$/gim); 70 | if (titleMatch && titleMatch.length) { 71 | return titleMatch[0].replace(/^#/, "").trim(); 72 | } 73 | return ""; 74 | } 75 | 76 | export function getMentionsFromMarkdown(markdown: string): string[] { 77 | return (markdown.match(/@(?:[a-zA-Z\d]+-)*[a-zA-Z\d]+/g) || []) 78 | .map((username) => username.replace(/^@/, "").toLocaleLowerCase()) 79 | .filter( 80 | (username, index, self) => 81 | index === self.findIndex((username2) => username === username2) 82 | ); 83 | } 84 | 85 | export async function generateSummaryFromMarkdown( 86 | markdown: string 87 | ): Promise { 88 | let title = "", 89 | summary = "", 90 | images: string[] = []; 91 | markdown = markdown.replace(/^---([\w\W]+?\n---)/, "").trim(); // Remove front matter 92 | let contentString = markdown; 93 | 94 | const titleMatch = markdown.match(/^#\s.+$/gim); 95 | if (titleMatch && titleMatch.length) { 96 | title = titleMatch[0].replace(/^#/, "").trim(); 97 | const aheadString = markdown 98 | .slice(0, markdown.indexOf(titleMatch[0])) 99 | .trim(); 100 | contentString = markdown.slice( 101 | markdown.indexOf(titleMatch[0]) + titleMatch[0].length, 102 | markdown.length 103 | ); 104 | const coverMatch = aheadString.match(/^!\[.*?\]\(.+?\)/gim); 105 | if (coverMatch && coverMatch.length > 0) { 106 | // @ts-ignore 107 | images.push(coverMatch[0].match(/\(([^)"]+?)\)/)[1].trim()); 108 | } 109 | } 110 | 111 | const coverImagesMatch = markdown.match(/^!\[.*?\]\(.+?\)/gim); 112 | if (coverImagesMatch && coverImagesMatch.length) { 113 | coverImagesMatch.forEach((mdImage) => { 114 | // @ts-ignore 115 | images.push(mdImage.match(/\(([^)"]+?)\)/)[1].trim()); 116 | }); 117 | images = images.filter( 118 | (image, index, self) => index === self.findIndex((m) => m === image) 119 | ); 120 | } 121 | 122 | summary = contentString 123 | .split("\n") 124 | .filter( 125 | (x) => 126 | x.trim().length > 0 && 127 | !x.match(/!\[.*?\]\(.+?\)/) && // Remove image 128 | !x.match(//) // Remove widget 129 | ) 130 | .map((x) => x.replace(/#+\s(.+)\s*$/, "**$1**").trim()) // Replace headers to bold 131 | .slice(0, 10) 132 | .join(" \n"); 133 | 134 | return { 135 | title, 136 | summary, 137 | images, 138 | tags: [], 139 | html: "", 140 | }; 141 | } 142 | 143 | export const EmptyPageInfo = { 144 | hasPreviousPage: false, 145 | hasNextPage: false, 146 | startCursor: UUIDNil, 147 | endCursor: UUIDNil, 148 | }; 149 | -------------------------------------------------------------------------------- /src/lib/section.ts: -------------------------------------------------------------------------------- 1 | export enum CrossnoteSectionType { 2 | Notebook = "Notebook", 3 | Notes = "Notes", 4 | Today = "Today", 5 | Todo = "Todo", 6 | Tagged = "Tagged", 7 | Untagged = "Untagged", 8 | Conflicted = "Conflicted", 9 | Encrypted = "Encrypted", 10 | Wiki = "Wiki", 11 | Error = "Error", 12 | Tag = "Tag", 13 | Directory = "Directory", 14 | } 15 | 16 | export interface SelectedSection { 17 | path: string; 18 | type: CrossnoteSectionType; 19 | notebook: { 20 | name: string; 21 | dir: string; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/settings.ts: -------------------------------------------------------------------------------- 1 | import { KeyMap } from "./keymap"; 2 | 3 | export interface CrossnoteSettings { 4 | readonly theme: string; 5 | readonly keyMap: KeyMap; 6 | } 7 | -------------------------------------------------------------------------------- /src/util/image_uploader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Upload image to sm.ms 3 | * @param filePath 4 | */ 5 | export async function smmsUploadImages(files: File[] = []): Promise { 6 | const promises = []; 7 | for (let i = 0; i < files.length; i++) { 8 | const file = files[i]; 9 | if (!file.type.match("image.*")) { 10 | continue; 11 | } 12 | promises.push( 13 | new Promise((resolve, reject) => { 14 | const headers = { 15 | // "Content-Type": "multipart/form-data" // <= Adding this will cause problem. 16 | }; 17 | const data = new FormData(); 18 | data.append("smfile", file); 19 | data.append("format", "json"); 20 | fetch("https://sm.ms/api/v2/upload", { 21 | method: "POST", 22 | mode: "cors", 23 | headers, 24 | referrer: "", 25 | body: data, 26 | }) 27 | .then((response) => response.json()) 28 | .then((json) => { 29 | if (json["success"]) { 30 | return resolve(json["data"]["url"]); 31 | } else if ( 32 | json["code"] === "image_repeated" || 33 | (json["code"] === "exception" && 34 | json["message"].match(/this image exists at:/)) 35 | ) { 36 | return resolve(json["message"].match(/https:\/\/.+$/)[0]); 37 | } else { 38 | return reject(json["message"]); 39 | } 40 | }) 41 | .catch(() => { 42 | return reject("Failed to connect to sm.ms host"); 43 | }); 44 | }) 45 | ); 46 | } 47 | return await Promise.all(promises); 48 | } 49 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | export const OneDay = 1000 * 60 * 60 * 24; 2 | 3 | export const UUIDNil = "00000000-0000-0000-0000-000000000000"; 4 | 5 | export function randomID() { 6 | return Math.random().toString(36).substr(2, 9); 7 | } 8 | 9 | export function generateUUID() { 10 | // Public Domain/MIT 11 | var d = new Date().getTime(); //Timestamp 12 | var d2 = (performance && performance.now && performance.now() * 1000) || 0; //Time in microseconds since page-load or 0 if unsupported 13 | return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { 14 | var r = Math.random() * 16; //random number between 0 and 16 15 | if (d > 0) { 16 | //Use timestamp until depleted 17 | r = (d + r) % 16 | 0; 18 | d = Math.floor(d / 16); 19 | } else { 20 | //Use microseconds since page-load if supported 21 | r = (d2 + r) % 16 | 0; 22 | d2 = Math.floor(d2 / 16); 23 | } 24 | return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/views/EditorPanelWebview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "typeface-roboto"; 4 | import "typeface-noto-sans-sc"; 5 | import "noty/lib/noty.css"; 6 | import "noty/lib/themes/relax.css"; 7 | import { ThemeProvider } from "@material-ui/styles"; 8 | import "./editor/index"; 9 | import "./i18n/i18n"; 10 | import "./index.less"; 11 | import EditorPanel from "./components/EditorPanel"; 12 | import { CssBaseline } from "@material-ui/core"; 13 | import { selectedTheme } from "./themes/manager"; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | 19 | , 20 | 21 | document.getElementById("root") 22 | ); 23 | -------------------------------------------------------------------------------- /src/views/NotesPanelWebview.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "typeface-roboto"; 4 | import "typeface-noto-sans-sc"; 5 | import "noty/lib/noty.css"; 6 | import "noty/lib/themes/relax.css"; 7 | import { ThemeProvider } from "@material-ui/styles"; 8 | import "./i18n/i18n"; 9 | import "./index.less"; 10 | import { NotesPanel } from "./components/NotesPanel"; 11 | import { CssBaseline } from "@material-ui/core"; 12 | import { selectedTheme } from "./themes/manager"; 13 | 14 | ReactDOM.render( 15 | 16 | 17 | 18 | , 19 | 20 | document.getElementById("root") 21 | ); 22 | -------------------------------------------------------------------------------- /src/views/components/ChangeFilePathDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | TextField, 7 | DialogActions, 8 | Button, 9 | } from "@material-ui/core"; 10 | import { useTranslation } from "react-i18next"; 11 | import { Note } from "../../lib/note"; 12 | import { vscode } from "../util/util"; 13 | import { MessageAction } from "../../lib/message"; 14 | import slash from "slash"; 15 | 16 | interface Props { 17 | open: boolean; 18 | onClose: () => void; 19 | note: Note; 20 | } 21 | 22 | export default function ChangeFilePathDialog(props: Props) { 23 | const note = props.note; 24 | const [inputEl, setInputEl] = useState(null); 25 | const [newFilePath, setNewFilePath] = useState( 26 | (note && note.filePath) || "" 27 | ); 28 | const { t } = useTranslation(); 29 | 30 | const changeFilePath = useCallback( 31 | (newFilePath: string) => { 32 | if (!note) { 33 | return; 34 | } 35 | (async () => { 36 | newFilePath = newFilePath.replace(/^\/+/, ""); 37 | if (!newFilePath.endsWith(".md")) { 38 | newFilePath = newFilePath + ".md"; 39 | } 40 | newFilePath = slash(newFilePath); 41 | if (note.filePath !== newFilePath) { 42 | vscode.postMessage({ 43 | action: MessageAction.ChangeNoteFilePath, 44 | data: { 45 | note, 46 | newFilePath: newFilePath, 47 | }, 48 | }); 49 | } 50 | props.onClose(); 51 | })(); 52 | }, 53 | [note, props.onClose, props] 54 | ); 55 | 56 | useEffect(() => { 57 | return () => { 58 | setInputEl(null); 59 | }; 60 | }, []); 61 | 62 | useEffect(() => { 63 | if (note) { 64 | setNewFilePath(note.filePath); 65 | } 66 | }, [note, props.open]); 67 | 68 | useEffect(() => { 69 | if (!inputEl) { 70 | return; 71 | } 72 | inputEl.focus(); 73 | if (inputEl.setSelectionRange) { 74 | const start = inputEl.value.lastIndexOf("/") + 1; 75 | let end = inputEl.value.lastIndexOf(".md"); 76 | if (end < 0) { 77 | end = inputEl.value.length; 78 | } 79 | inputEl.setSelectionRange(start, end); 80 | } 81 | }, [inputEl]); 82 | 83 | return ( 84 | 85 | {t("general/change-file-path")} 86 | 87 | setNewFilePath(event.target.value)} 91 | onKeyUp={(event) => { 92 | if (event.which === 13) { 93 | changeFilePath(newFilePath); 94 | } 95 | }} 96 | inputRef={(input: HTMLInputElement) => { 97 | setInputEl(input); 98 | }} 99 | fullWidth={true} 100 | > 101 | 102 | 103 | 110 | 111 | 112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/views/components/DeleteDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogContentText, 7 | DialogActions, 8 | Button, 9 | } from "@material-ui/core"; 10 | import { useTranslation } from "react-i18next"; 11 | import { Note } from "../../lib/note"; 12 | import { vscode } from "../util/util"; 13 | import { MessageAction } from "../../lib/message"; 14 | 15 | interface Props { 16 | open: boolean; 17 | onClose: () => void; 18 | note: Note; 19 | } 20 | export function DeleteDialog(props: Props) { 21 | const { t } = useTranslation(); 22 | const note = props.note; 23 | 24 | const deleteNote = useCallback((note: Note) => { 25 | vscode.postMessage({ 26 | action: MessageAction.DeleteNote, 27 | data: note, 28 | }); 29 | }, []); 30 | 31 | return ( 32 | 33 | {t("delete-file-dialog/title")} 34 | 35 | 36 | {t("delete-file-dialog/subtitle")} 37 | 38 | 39 | 40 | 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/views/components/EditImageDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import { 3 | Dialog, 4 | DialogTitle, 5 | DialogContent, 6 | DialogActions, 7 | Button, 8 | Box, 9 | TextField, 10 | } from "@material-ui/core"; 11 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; 12 | import clsx from "clsx"; 13 | import { Editor as CodeMirrorEditor, TextMarker } from "codemirror"; 14 | import { useTranslation } from "react-i18next"; 15 | import { Note } from "../../lib/note"; 16 | import { resolveNoteImageSrc } from "../util/util"; 17 | 18 | interface Props { 19 | open: boolean; 20 | onClose: () => void; 21 | editor: CodeMirrorEditor; 22 | marker: TextMarker; 23 | imageElement: HTMLImageElement; 24 | note: Note; 25 | } 26 | 27 | const useStyles = makeStyles((theme: Theme) => 28 | createStyles({ 29 | imageWrapper: { 30 | textAlign: "center", 31 | }, 32 | imagePreview: { 33 | maxWidth: "100%", 34 | maxHeight: "400px", 35 | }, 36 | }) 37 | ); 38 | 39 | export default function EditImageDialog(props: Props) { 40 | const classes = useStyles(props); 41 | const { t } = useTranslation(); 42 | const editor = props.editor; 43 | const marker = props.marker; 44 | const imageElement = props.imageElement; 45 | const [imageSrc, setImageSrc] = useState(""); 46 | const [imageAlt, setImageAlt] = useState(""); 47 | const [imageTitle, setImageTitle] = useState(""); 48 | 49 | const deleteImage = useCallback(() => { 50 | if (!imageElement || !editor || !marker) { 51 | return; 52 | } 53 | const pos = marker.find(); 54 | editor.replaceRange("", pos.from, pos.to); 55 | props.onClose(); 56 | }, [imageElement, editor, marker, props]); 57 | 58 | const updateImage = useCallback(() => { 59 | if (!imageElement || !editor || !marker) { 60 | return; 61 | } 62 | const pos = marker.find(); 63 | if (!pos) { 64 | return; 65 | } 66 | if (imageTitle.trim().length) { 67 | editor.replaceRange( 68 | `![${imageAlt.trim()}](${imageSrc.trim()} ${JSON.stringify( 69 | imageTitle 70 | )})`, 71 | pos.from, 72 | pos.to 73 | ); 74 | } else { 75 | editor.replaceRange( 76 | `![${imageAlt.trim()}](${imageSrc.trim()})`, 77 | pos.from, 78 | pos.to 79 | ); 80 | } 81 | props.onClose(); 82 | }, [imageElement, editor, marker, props, imageAlt, imageSrc, imageTitle]); 83 | 84 | useEffect(() => { 85 | if (imageElement && marker && editor) { 86 | setImageSrc(imageElement.getAttribute("data-src") || ""); 87 | setImageTitle(imageElement.title || ""); 88 | setImageAlt(imageElement.alt || ""); 89 | } 90 | }, [imageElement, marker, editor]); 91 | 92 | if (!editor || !marker || !imageElement) { 93 | return null; 94 | } 95 | 96 | return ( 97 | 98 | {t("edit-image-dialog/title")} 99 | 100 | 101 | {imageAlt}{" "} 107 | 108 | setImageSrc(event.target.value)} 114 | > 115 | setImageTitle(event.target.value)} 121 | > 122 | setImageAlt(event.target.value)} 128 | > 129 | 130 | 131 | 134 | 137 | 138 | 139 | 140 | ); 141 | } 142 | -------------------------------------------------------------------------------- /src/views/components/NoteCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; 3 | import clsx from "clsx"; 4 | import { Box, Typography, ButtonBase, Tooltip } from "@material-ui/core"; 5 | import { formatDistanceStrict } from "date-fns/esm"; 6 | import { useTranslation } from "react-i18next"; 7 | import { Pin } from "mdi-material-ui"; 8 | import { formatRelative } from "date-fns"; 9 | import { basename } from "path"; 10 | import { Message, MessageAction } from "../../lib/message"; 11 | import { 12 | Note, 13 | Summary, 14 | getHeaderFromMarkdown, 15 | generateSummaryFromMarkdown, 16 | } from "../../lib/note"; 17 | import { vscode, resolveNoteImageSrc } from "../util/util"; 18 | 19 | const useStyles = makeStyles((theme: Theme) => 20 | createStyles({ 21 | noteCard: { 22 | width: "100%", 23 | display: "flex", 24 | flexDirection: "row", 25 | alignItems: "flex-start", 26 | padding: theme.spacing(2, 0.5, 0), 27 | textAlign: "left", 28 | cursor: "default", 29 | backgroundColor: theme.palette.background.paper, 30 | }, 31 | selected: { 32 | borderLeft: `4px solid ${theme.palette.primary.main}`, 33 | }, 34 | unselected: { 35 | borderLeft: `4px solid rgba(0, 0, 0, 0)`, 36 | }, 37 | leftPanel: { 38 | width: "48px", 39 | paddingLeft: theme.spacing(0.5), 40 | }, 41 | duration: { 42 | color: theme.palette.text.secondary, 43 | }, 44 | rightPanel: { 45 | width: "calc(100% - 48px)", 46 | borderBottom: `1px solid ${theme.palette.divider}`, 47 | }, 48 | header: { 49 | marginBottom: theme.spacing(1), 50 | wordBreak: "break-all", 51 | }, 52 | summary: { 53 | color: theme.palette.text.secondary, 54 | marginBottom: theme.spacing(1), 55 | paddingRight: theme.spacing(2), 56 | display: "-webkit-box", 57 | lineHeight: "1.3rem !important", 58 | textOverflow: "ellipsis !important", 59 | overflow: "hidden !important", 60 | maxWidth: "100%", 61 | maxHeight: "2.6rem", // lineHeight x -website-line-clamp 62 | "-webkit-line-clamp": 2, 63 | "-webkit-box-orient": "vertical", 64 | wordBreak: "break-all", 65 | }, 66 | filePath: {}, 67 | images: { 68 | display: "flex", 69 | width: "100%", 70 | overflow: "hidden", 71 | position: "relative", 72 | marginBottom: theme.spacing(1), 73 | }, 74 | imagesWrapper: { 75 | display: "flex", 76 | alignItems: "center", 77 | flexDirection: "row", 78 | }, 79 | image: { 80 | width: "128px", 81 | height: "80px", 82 | marginRight: theme.spacing(1), 83 | position: "relative", 84 | backgroundSize: "cover", 85 | backgroundPosition: "center", 86 | display: "block", 87 | borderRadius: "6px", 88 | }, 89 | pin: { 90 | color: theme.palette.secondary.main, 91 | marginTop: theme.spacing(1), 92 | }, 93 | }) 94 | ); 95 | 96 | interface Props { 97 | note: Note; 98 | selectedNote: Note; 99 | setSelectedNote: (note: Note) => void; 100 | } 101 | 102 | export default function NoteCard(props: Props) { 103 | const classes = useStyles(props); 104 | const note = props.note; 105 | const [header, setHeader] = useState(""); 106 | const [summary, setSummary] = useState(null); 107 | const [images, setImages] = useState([]); 108 | const { t } = useTranslation(); 109 | const duration = formatDistanceStrict(note.config.modifiedAt, Date.now()) 110 | .replace(/\sseconds?/, "s") 111 | .replace(/\sminutes?/, "m") 112 | .replace(/\shours?/, "h") 113 | .replace(/\sdays?/, "d") 114 | .replace(/\sweeks?/, "w") 115 | .replace(/\smonths?/, "mo") 116 | .replace(/\syears?/, "y"); 117 | 118 | const openNote = useCallback(() => { 119 | if (!note) { 120 | return; 121 | } 122 | const message: Message = { 123 | action: MessageAction.OpenNote, 124 | data: note, 125 | }; 126 | vscode.postMessage(message); 127 | props.setSelectedNote(note); 128 | }, [note, props.setSelectedNote]); 129 | 130 | useEffect(() => { 131 | setHeader( 132 | (note.config.encryption && note.config.encryption.title) || 133 | getHeaderFromMarkdown(note.markdown) 134 | ); 135 | generateSummaryFromMarkdown( 136 | note.config.encryption 137 | ? `🔐 ${t("general/encrypted")}` 138 | : note.markdown.trim() || t("general/this-note-is-empty") 139 | ) 140 | .then((summary) => { 141 | setSummary(summary); 142 | 143 | // render images 144 | const images = summary.images 145 | .map((image) => { 146 | return resolveNoteImageSrc(note, image); 147 | }) 148 | .filter((x) => x) 149 | .slice(0, 3); // TODO: Support local image 150 | setImages(images); 151 | }) 152 | .catch((error) => {}); 153 | }, [note.markdown, note.config.encryption, t]); 154 | 155 | /* 156 | useEffect(() => { 157 | crossnoteContainer.crossnote.getStatus(note).then((status) => { 158 | setGitStatus(status); 159 | }); 160 | }, [ 161 | note.markdown, 162 | note.config.modifiedAt, 163 | note, 164 | crossnoteContainer.crossnote, 165 | ]); 166 | */ 167 | 168 | return ( 169 | 180 | 181 | 184 |

185 | {t("general/created-at") + 186 | " " + 187 | formatRelative(new Date(note.config.createdAt), new Date())} 188 |

189 |

190 | {t("general/modified-at") + 191 | " " + 192 | formatRelative(new Date(note.config.modifiedAt), new Date())} 193 |

194 | 195 | } 196 | arrow 197 | > 198 | {duration} 199 |
200 | 201 | {note.config.pinned && } 202 |
203 | 204 | {header && ( 205 | 210 | {header} 211 | 212 | )} 213 | {summary && summary.summary.trim().length > 0 && ( 214 | 215 | {summary && summary.summary} 216 | 217 | )} 218 | {images.length > 0 && ( 219 | 220 | 221 | {images.map((image, offset) => ( 222 |
229 | ))} 230 |
231 |
232 | )} 233 | 234 | {basename(note.filePath).startsWith("unnamed_") ? "" : note.filePath} 235 | 236 |
237 |
238 | ); 239 | } 240 | -------------------------------------------------------------------------------- /src/views/components/Notes.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import LazyLoad from "react-lazyload"; 3 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; 4 | import clsx from "clsx"; 5 | import { Box, Typography, Button } from "@material-ui/core"; 6 | import NoteCard from "./NoteCard"; 7 | import { useTranslation } from "react-i18next"; 8 | import useInterval from "@use-it/interval"; 9 | import { CloudDownloadOutline } from "mdi-material-ui"; 10 | import Noty from "noty"; 11 | import { Skeleton } from "@material-ui/lab"; 12 | import { Note, getHeaderFromMarkdown } from "../../lib/note"; 13 | import { SelectedSection } from "../../lib/section"; 14 | import { Message, MessageAction } from "../../lib/message"; 15 | import { vscode } from "../util/util"; 16 | 17 | export enum OrderBy { 18 | CreatedAt = "CreatedAt", 19 | ModifiedAt = "ModifiedAt", 20 | Title = "Title", 21 | } 22 | 23 | export enum OrderDirection { 24 | ASC = "ASC", 25 | DESC = "DESC", 26 | } 27 | 28 | const lazyLoadPlaceholderHeight = 92; 29 | 30 | const useStyles = makeStyles((theme: Theme) => 31 | createStyles({ 32 | notesList: { 33 | position: "relative", 34 | flex: "1", 35 | overflowY: "auto", 36 | paddingBottom: theme.spacing(12), 37 | backgroundColor: theme.palette.background.default, 38 | marginTop: "3px", 39 | }, 40 | updatePanel: { 41 | padding: theme.spacing(2), 42 | textAlign: "center", 43 | borderBottom: "1px solid #ededed", 44 | }, 45 | }) 46 | ); 47 | 48 | interface Props { 49 | searchValue: string; 50 | notes: Note[]; 51 | orderBy: OrderBy; 52 | orderDirection: OrderDirection; 53 | selectedNote: Note; 54 | setSelectedNote: (note: Note) => void; 55 | } 56 | 57 | export default function Notes(props: Props) { 58 | const classes = useStyles(props); 59 | const { t } = useTranslation(); 60 | const [notes, setNotes] = useState([]); 61 | const [notesListElement, setNotesListElement] = useState(null); 62 | const [forceUpdate, setForceUpdate] = useState(Date.now()); 63 | const searchValue = props.searchValue; 64 | const orderBy = props.orderBy; 65 | const orderDirection = props.orderDirection; 66 | const selectedNote = props.selectedNote; 67 | const setSelectedNote = props.setSelectedNote; 68 | 69 | useEffect(() => { 70 | const pinned: Note[] = []; 71 | const unpinned: Note[] = []; 72 | props.notes.forEach((note) => { 73 | if (searchValue.trim().length) { 74 | const regexp = new RegExp( 75 | "(" + 76 | searchValue 77 | .trim() 78 | .split(/\s+/g) 79 | .map((s) => s.replace(/[.!@#$%^&*()_+\-=[\]]/g, (x) => `\\${x}`)) // escape special regexp characters 80 | .join("|") + 81 | ")", 82 | "i" 83 | ); 84 | 85 | if (note.markdown.match(regexp) || note.filePath.match(regexp)) { 86 | if (note.config.pinned) { 87 | pinned.push(note); 88 | } else { 89 | unpinned.push(note); 90 | } 91 | } 92 | } else { 93 | if (note.config.pinned) { 94 | pinned.push(note); 95 | } else { 96 | unpinned.push(note); 97 | } 98 | } 99 | }); 100 | 101 | const sort = (notes: Note[]) => { 102 | if (orderBy === OrderBy.ModifiedAt) { 103 | if (orderDirection === OrderDirection.DESC) { 104 | notes.sort( 105 | (a, b) => 106 | b.config.modifiedAt.getTime() - a.config.modifiedAt.getTime() 107 | ); 108 | } else { 109 | notes.sort( 110 | (a, b) => 111 | a.config.modifiedAt.getTime() - b.config.modifiedAt.getTime() 112 | ); 113 | } 114 | } else if (orderBy === OrderBy.CreatedAt) { 115 | if (orderDirection === OrderDirection.DESC) { 116 | notes.sort( 117 | (a, b) => 118 | b.config.createdAt.getTime() - a.config.createdAt.getTime() 119 | ); 120 | } else { 121 | notes.sort( 122 | (a, b) => 123 | a.config.createdAt.getTime() - b.config.createdAt.getTime() 124 | ); 125 | } 126 | } else if (orderBy === OrderBy.Title) { 127 | if (orderDirection === OrderDirection.DESC) { 128 | notes.sort((a, b) => 129 | ( 130 | (b.config.encryption && b.config.encryption.title) || 131 | getHeaderFromMarkdown(b.markdown) 132 | ).localeCompare( 133 | (a.config.encryption && a.config.encryption.title) || 134 | getHeaderFromMarkdown(a.markdown) 135 | ) 136 | ); 137 | } else { 138 | notes.sort((a, b) => 139 | ( 140 | (a.config.encryption && a.config.encryption.title) || 141 | getHeaderFromMarkdown(a.markdown) 142 | ).localeCompare( 143 | (b.config.encryption && b.config.encryption.title) || 144 | getHeaderFromMarkdown(b.markdown) 145 | ) 146 | ); 147 | } 148 | } 149 | return notes; 150 | }; 151 | 152 | setNotes([...sort(pinned), ...sort(unpinned)]); 153 | }, [props.notes, searchValue, orderBy, orderDirection]); 154 | 155 | useEffect(() => { 156 | if ( 157 | notes && 158 | notes.length > 0 && 159 | (!selectedNote || selectedNote.notebookPath !== notes[0].notebookPath) 160 | ) { 161 | setSelectedNote(notes[0]); 162 | } 163 | }, [notes, selectedNote]); 164 | 165 | useEffect(() => { 166 | if (selectedNote) { 167 | const message: Message = { 168 | action: MessageAction.OpenNoteIfNoNoteSelected, 169 | data: selectedNote, 170 | }; 171 | vscode.postMessage(message); 172 | } 173 | }, [selectedNote]); 174 | 175 | /* 176 | useEffect(() => { 177 | if (notesListElement) { 178 | const keyDownHandler = (event: KeyboardEvent) => { 179 | const selectedNote = crossnoteContainer.selectedNote; 180 | if (!selectedNote || !notes.length) { 181 | return; 182 | } 183 | const currentIndex = notes.findIndex( 184 | (n) => n.filePath === selectedNote.filePath 185 | ); 186 | if (currentIndex < 0) { 187 | crossnoteContainer.setSelectedNote(notes[0]); 188 | } else if (event.which === 40) { 189 | // Up 190 | if (currentIndex >= 0 && currentIndex < notes.length - 1) { 191 | crossnoteContainer.setSelectedNote(notes[currentIndex + 1]); 192 | } 193 | } else if (event.which === 38) { 194 | // Down 195 | if (currentIndex > 0 && currentIndex < notes.length) { 196 | crossnoteContainer.setSelectedNote(notes[currentIndex - 1]); 197 | } 198 | } 199 | }; 200 | notesListElement.addEventListener("keydown", keyDownHandler); 201 | return () => { 202 | notesListElement.removeEventListener("keydown", keyDownHandler); 203 | }; 204 | } 205 | }, [notesListElement, notes, crossnoteContainer.selectedNote]); 206 | */ 207 | 208 | useEffect(() => { 209 | if (notesListElement) { 210 | // Hack: fix note cards not displaying bug when searchValue is not empty 211 | const hack = () => { 212 | const initialHeight = notesListElement.style.height; 213 | const initialFlex = notesListElement.style.flex; 214 | notesListElement.style.flex = "initial"; 215 | notesListElement.style.height = "10px"; 216 | notesListElement.scrollTop += 1; 217 | notesListElement.scrollTop -= 1; 218 | notesListElement.style.height = initialHeight; 219 | notesListElement.style.flex = initialFlex; 220 | }; 221 | window.addEventListener("resize", hack); 222 | hack(); 223 | return () => { 224 | window.removeEventListener("resize", hack); 225 | }; 226 | } 227 | }, [notes, notesListElement]); 228 | 229 | /* 230 | useInterval(() => { 231 | if (crossnoteContainer.needsToRefreshNotes) { 232 | crossnoteContainer.setNeedsToRefreshNotes(false); 233 | setForceUpdate(Date.now()); 234 | } 235 | }, 15000); 236 | */ 237 | 238 | return ( 239 |
{ 242 | setNotesListElement(element); 243 | }} 244 | > 245 | {/*crossnoteContainer.selectedNotebook && 246 | crossnoteContainer.selectedNotebook.localSha !== 247 | crossnoteContainer.selectedNotebook.remoteSha && ( 248 | 249 | 250 | {"🔔 " + t("general/notebook-updates-found")} 251 | 252 | 266 | 267 | )*/} 268 | {(notes || []).map((note) => { 269 | return ( 270 | 282 | 283 | 284 | 285 | 286 | } 287 | height={lazyLoadPlaceholderHeight} 288 | overflow={true} 289 | once={true} 290 | scrollContainer={notesListElement} 291 | resize={true} 292 | > 293 | 299 | 300 | ); 301 | })} 302 | { 303 | /*crossnoteContainer.initialized && 304 | !crossnoteContainer.isLoadingNotebook &&*/ 305 | notes.length === 0 && ( 306 | 313 | {"🧐 " + t("general/no-notes-found")} 314 | 315 | ) 316 | } 317 |
318 | ); 319 | } 320 | -------------------------------------------------------------------------------- /src/views/components/NotesPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from "react"; 2 | import { 3 | fade, 4 | createStyles, 5 | makeStyles, 6 | Theme, 7 | } from "@material-ui/core/styles"; 8 | import clsx from "clsx"; 9 | import { 10 | Box, 11 | InputBase, 12 | Card, 13 | IconButton, 14 | Typography, 15 | Hidden, 16 | CircularProgress, 17 | Popover, 18 | List, 19 | ListItem, 20 | ListItemText, 21 | ListItemIcon, 22 | Divider, 23 | } from "@material-ui/core"; 24 | import { 25 | Magnify, 26 | FileEditOutline, 27 | Cog, 28 | Menu as MenuIcon, 29 | SortVariant, 30 | SortDescending, 31 | SortAscending, 32 | } from "mdi-material-ui"; 33 | import { useTranslation } from "react-i18next"; 34 | import { Message, MessageAction } from "../../lib/message"; 35 | import { Note } from "../../lib/note"; 36 | import { CrossnoteSectionType, SelectedSection } from "../../lib/section"; 37 | import Notes, { OrderDirection, OrderBy } from "./Notes"; 38 | import { vscode } from "../util/util"; 39 | 40 | const useStyles = makeStyles((theme: Theme) => 41 | createStyles({ 42 | notesPanel: { 43 | position: "relative", 44 | display: "flex", 45 | flexDirection: "column", 46 | height: "100%", 47 | width: "100%", 48 | backgroundColor: theme.palette.background.default, 49 | }, 50 | topPanel: { 51 | padding: theme.spacing(0, 1), 52 | borderRadius: 0, 53 | backgroundColor: theme.palette.background.paper, 54 | }, 55 | row: { 56 | display: "flex", 57 | alignItems: "center", 58 | }, 59 | sectionName: { 60 | marginLeft: theme.spacing(1), 61 | }, 62 | search: { 63 | position: "relative", 64 | borderRadius: theme.shape.borderRadius, 65 | backgroundColor: fade(theme.palette.common.white, 0.15), 66 | "&:hover": { 67 | backgroundColor: fade(theme.palette.common.white, 0.25), 68 | }, 69 | marginRight: 0, // theme.spacing(2), 70 | marginLeft: 0, 71 | width: "100%", 72 | [theme.breakpoints.up("sm")]: { 73 | // marginLeft: theme.spacing(3), 74 | // width: "auto" 75 | }, 76 | }, 77 | searchIcon: { 78 | width: theme.spacing(7), 79 | height: "100%", 80 | position: "absolute", 81 | pointerEvents: "none", 82 | display: "flex", 83 | alignItems: "center", 84 | justifyContent: "center", 85 | }, 86 | inputRoot: { 87 | color: "inherit", 88 | border: "1px solid #bbb", 89 | borderRadius: "4px", 90 | width: "100%", 91 | }, 92 | inputInput: { 93 | padding: theme.spacing(1, 1, 1, 7), 94 | transition: theme.transitions.create("width"), 95 | width: "100%", 96 | [theme.breakpoints.up("md")]: { 97 | // width: 200 98 | }, 99 | }, 100 | notesList: { 101 | position: "relative", 102 | flex: "1", 103 | overflowY: "auto", 104 | paddingBottom: theme.spacing(12), 105 | }, 106 | loading: { 107 | position: "absolute", 108 | top: "40%", 109 | left: "50%", 110 | transform: "translateX(-50%)", 111 | }, 112 | sortSelected: { 113 | color: theme.palette.primary.main, 114 | "& svg": { 115 | color: theme.palette.primary.main, 116 | }, 117 | }, 118 | }) 119 | ); 120 | 121 | interface Props {} 122 | export function NotesPanel(props: Props) { 123 | const classes = useStyles(props); 124 | const { t } = useTranslation(); 125 | const [selectedSection, setSelectedSection] = useState(null); 126 | const [selectedNote, setSelectedNote] = useState(null); 127 | const [notes, setNotes] = useState([]); 128 | const [searchValue, setSearchValue] = useState(""); 129 | const [searchValueInputTimeout, setSearchValueInputTimeout] = useState< 130 | NodeJS.Timeout 131 | >(null); 132 | const [finalSearchValue, setFinalSearchValue] = useState(""); 133 | const [sortMenuAnchorEl, setSortMenuAnchorEl] = useState(null); 134 | const [orderBy, setOrderBy] = useState(OrderBy.ModifiedAt); 135 | const [orderDirection, setOrderDirection] = useState( 136 | OrderDirection.DESC 137 | ); 138 | 139 | const createNewNote = useCallback(() => { 140 | if (!selectedSection) { 141 | return; 142 | } 143 | const message: Message = { 144 | action: MessageAction.CreateNewNote, 145 | data: selectedSection, 146 | }; 147 | vscode.postMessage(message); 148 | }, [selectedSection]); 149 | 150 | const onChangeSearchValue = useCallback( 151 | (event: React.ChangeEvent) => { 152 | const value = event.target.value; 153 | setSearchValue(value); 154 | if (searchValueInputTimeout) { 155 | clearTimeout(searchValueInputTimeout); 156 | } 157 | const timeout = setTimeout(() => { 158 | setFinalSearchValue(value); 159 | }, 400); 160 | setSearchValueInputTimeout(timeout); 161 | }, 162 | [searchValueInputTimeout] 163 | ); 164 | 165 | useEffect(() => { 166 | const message: Message = { 167 | action: MessageAction.InitializedNotesPanelWebview, 168 | data: {}, 169 | }; 170 | vscode.postMessage(message); 171 | }, []); 172 | 173 | useEffect(() => { 174 | const onMessage = (event) => { 175 | const message: Message = event.data; 176 | let note: Note; 177 | switch (message.action) { 178 | case MessageAction.SelectedSection: 179 | setSelectedSection(message.data); 180 | if ( 181 | selectedSection && 182 | message.data.notebook.dir !== selectedSection.notebook.dir 183 | ) { 184 | setSearchValue(""); 185 | } 186 | break; 187 | case MessageAction.SendNotes: 188 | const notes: Note[] = message.data; 189 | notes.forEach((note) => { 190 | note.config.createdAt = new Date(note.config.createdAt || 0); 191 | note.config.modifiedAt = new Date(note.config.modifiedAt || 0); 192 | }); 193 | setNotes(message.data || []); 194 | break; 195 | case MessageAction.CreatedNewNote: 196 | note = message.data; 197 | note.config.createdAt = new Date(note.config.createdAt || 0); 198 | note.config.modifiedAt = new Date(note.config.modifiedAt || 0); 199 | 200 | setNotes((notes) => [note, ...notes]); 201 | setSelectedNote(note); 202 | break; 203 | case MessageAction.SelectedNote: 204 | note = message.data; 205 | note.config.createdAt = new Date(note.config.createdAt || 0); 206 | note.config.modifiedAt = new Date(note.config.modifiedAt || 0); 207 | setSelectedNote(note); 208 | break; 209 | default: 210 | break; 211 | } 212 | }; 213 | window.addEventListener("message", onMessage); 214 | return () => { 215 | window.removeEventListener("message", onMessage); 216 | }; 217 | }, [selectedSection]); 218 | 219 | return ( 220 | 221 | 222 | 223 |
224 |
225 | 226 |
227 | 239 |
240 | 241 | 242 | 243 |
244 | 248 | {selectedSection?.type === CrossnoteSectionType.Notes || 249 | selectedSection?.type === CrossnoteSectionType.Notebook ? ( 250 | 251 | 252 | 📔 253 | 254 | 255 | {selectedSection.notebook.name} 256 | 257 | 258 | ) : selectedSection?.type === CrossnoteSectionType.Today ? ( 259 | 260 | 261 | 📅 262 | 263 | 264 | {t("general/today")} 265 | 266 | 267 | ) : selectedSection?.type === CrossnoteSectionType.Todo ? ( 268 | 269 | 270 | ☑️ 271 | 272 | 273 | {t("general/todo")} 274 | 275 | 276 | ) : selectedSection?.type === CrossnoteSectionType.Tagged ? ( 277 | 278 | 279 | 🏷️ 280 | 281 | 282 | {t("general/tagged")} 283 | 284 | 285 | ) : selectedSection?.type === CrossnoteSectionType.Untagged ? ( 286 | 287 | 288 | 🈚 289 | 290 | 291 | {t("general/untagged")} 292 | 293 | 294 | ) : selectedSection?.type === CrossnoteSectionType.Tag ? ( 295 | 296 | 297 | 🏷️ 298 | 299 | 300 | {selectedSection.path} 301 | 302 | 303 | ) : selectedSection?.type === CrossnoteSectionType.Encrypted ? ( 304 | 305 | 306 | 🔐 307 | 308 | 309 | {t("general/encrypted")} 310 | 311 | 312 | ) : selectedSection?.type === CrossnoteSectionType.Conflicted ? ( 313 | 314 | 315 | ⚠️ 316 | 317 | 318 | {t("general/conflicted")} 319 | 320 | 321 | ) : ( 322 | selectedSection?.type === CrossnoteSectionType.Directory && ( 323 | 324 | 325 | {"📁"} 326 | 327 | 328 | {selectedSection.path} 329 | 330 | 331 | ) 332 | )} 333 | 334 | 335 | setSortMenuAnchorEl(event.currentTarget)} 337 | > 338 | 339 | 340 | setSortMenuAnchorEl(null)} 345 | > 346 | 347 | setOrderBy(OrderBy.ModifiedAt)} 350 | className={clsx( 351 | orderBy === OrderBy.ModifiedAt && classes.sortSelected 352 | )} 353 | > 354 | 357 | 358 | setOrderBy(OrderBy.CreatedAt)} 361 | className={clsx( 362 | orderBy === OrderBy.CreatedAt && classes.sortSelected 363 | )} 364 | > 365 | 368 | 369 | setOrderBy(OrderBy.Title)} 372 | className={clsx( 373 | orderBy === OrderBy.Title && classes.sortSelected 374 | )} 375 | > 376 | 377 | 378 | 379 | setOrderDirection(OrderDirection.DESC)} 382 | className={clsx( 383 | orderDirection === OrderDirection.DESC && 384 | classes.sortSelected 385 | )} 386 | > 387 | 388 | 389 | 390 | 391 | 392 | setOrderDirection(OrderDirection.ASC)} 395 | className={clsx( 396 | orderDirection === OrderDirection.ASC && 397 | classes.sortSelected 398 | )} 399 | > 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 |
410 | 411 | 419 |
420 | ); 421 | } 422 | -------------------------------------------------------------------------------- /src/views/components/TagsMenuPopover.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from "react"; 2 | import { 3 | Popover, 4 | List, 5 | ListItem, 6 | TextField, 7 | Box, 8 | Typography, 9 | IconButton, 10 | } from "@material-ui/core"; 11 | import { TrashCan } from "mdi-material-ui"; 12 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; 13 | import clsx from "clsx"; 14 | import { useTranslation } from "react-i18next"; 15 | import { Autocomplete } from "@material-ui/lab"; 16 | import { TagNode } from "../../lib/notebook"; 17 | 18 | const useStyles = makeStyles((theme: Theme) => 19 | createStyles({ 20 | menuItemOverride: { 21 | cursor: "default", 22 | padding: `0 0 0 ${theme.spacing(2)}px`, 23 | "&:hover": { 24 | backgroundColor: "inherit", 25 | }, 26 | }, 27 | menuItemTextField: { 28 | paddingRight: theme.spacing(2), 29 | }, 30 | }) 31 | ); 32 | interface Props { 33 | anchorElement: HTMLElement; 34 | onClose: () => void; 35 | addTag: (tagName: string) => void; 36 | deleteTag: (tagName: string) => void; 37 | tagNames: string[]; 38 | notebookTagNode: TagNode; 39 | } 40 | export function TagsMenuPopover(props: Props) { 41 | const classes = useStyles(props); 42 | const { t } = useTranslation(); 43 | const [tagName, setTagName] = useState(""); 44 | const [options, setOptions] = useState([]); 45 | 46 | const addTag = useCallback( 47 | (tagName: string) => { 48 | if (!tagName || tagName.trim().length === 0) { 49 | return; 50 | } 51 | props.addTag(tagName); 52 | setTagName(""); 53 | }, 54 | [props] 55 | ); 56 | 57 | useEffect(() => { 58 | setTagName(""); 59 | }, [props.anchorElement]); 60 | 61 | useEffect(() => { 62 | if (!props.anchorElement) { 63 | return; 64 | } 65 | let options: string[] = []; 66 | const helper = (children: TagNode[]) => { 67 | if (!children || !children.length) { 68 | return; 69 | } 70 | for (let i = 0; i < children.length; i++) { 71 | const tag = children[i].path; 72 | options.push(tag); 73 | helper(children[i].children); 74 | } 75 | }; 76 | helper(props.notebookTagNode.children); 77 | setOptions(options); 78 | }, [props.notebookTagNode, props]); 79 | 80 | return ( 81 | 87 | 88 | 91 | { 94 | setTagName(newInputValue); 95 | }} 96 | options={(tagName.trim().length > 0 && 97 | options.findIndex((x) => x === tagName.trim()) < 0 98 | ? [tagName, ...options] 99 | : options 100 | ).map((opt) => "+ " + opt)} 101 | style={{ width: 300, maxWidth: "100%" }} 102 | value={""} 103 | onChange={(event: any, newValue: string = "") => { 104 | if (newValue) { 105 | addTag(newValue.replace(/^+/, "").trim()); 106 | } 107 | }} 108 | renderInput={(params) => ( 109 | { 114 | if (event.which === 13) { 115 | addTag(tagName); 116 | } 117 | }} 118 | {...params} 119 | > 120 | )} 121 | noOptionsText={t("general/no-tags")} 122 | openText={t("general/Open")} 123 | closeText={t("general/close")} 124 | loadingText={t("general/loading")} 125 | clearText={t("general/clear-all")} 126 | > 127 | 128 | {props.tagNames.length > 0 ? ( 129 | props.tagNames.map((tagName) => { 130 | return ( 131 | 135 | 144 | {tagName} 145 | props.deleteTag(tagName)}> 146 | 147 | 148 | 149 | 150 | ); 151 | }) 152 | ) : ( 153 | 154 | 155 | {t("general/no-tags")} 156 | 157 | 158 | )} 159 | 160 | 161 | ); 162 | } 163 | -------------------------------------------------------------------------------- /src/views/editor/index.ts: -------------------------------------------------------------------------------- 1 | // Import VickyMD related modules 2 | // VickyMD 3 | import "codemirror"; 4 | // Load these modes if you want highlighting ... 5 | import "codemirror/lib/codemirror.css"; 6 | import "codemirror/addon/hint/show-hint.css"; 7 | import "codemirror/addon/dialog/dialog.css"; 8 | import "codemirror/mode/htmlmixed/htmlmixed"; // for embedded HTML 9 | import "codemirror/mode/markdown/markdown"; 10 | import "codemirror/mode/stex/stex"; // for Math TeX Formular 11 | import "codemirror/mode/yaml/yaml"; // for Front Matters 12 | import "codemirror/mode/javascript/javascript"; // eg. javascript 13 | import "codemirror/mode/python/python"; 14 | import "codemirror/addon/display/placeholder"; 15 | import "codemirror/addon/hint/show-hint"; 16 | import "codemirror/keymap/vim"; 17 | import "codemirror/keymap/emacs"; 18 | 19 | // Essential 20 | import "vickymd"; // ESSENTIAL 21 | // Widgets 22 | // Load PowerPacks if you want to utilize 3rd-party libs 23 | import "vickymd/powerpack/fold-math-with-katex"; 24 | import "vickymd/powerpack/fold-code-with-mermaid"; 25 | import "vickymd/powerpack/fold-code-with-plantuml"; 26 | import "vickymd/powerpack/fold-code-with-echarts"; 27 | import "vickymd/powerpack/fold-code-with-wavedrom"; 28 | import "vickymd/powerpack/hover-with-marked"; 29 | import { registerWidgetCreator } from "vickymd/widget"; 30 | import { TimerWidgetCreator } from "./widgets/timer"; 31 | import { ImageWidgetCreator } from "./widgets/image"; 32 | import { AudioWidgetCreator } from "./widgets/audio"; 33 | // import { NeteaseMusicWidgetCreator } from "./widgets/netease_music"; 34 | import { VideoWidgetCreator } from "./widgets/video"; 35 | import { BilibiliWidgetCreator } from "./widgets/bilibili"; 36 | import { YoutubeWidgetCreator } from "./widgets/youtube"; 37 | import { OCRWidgetCreator } from "./widgets/ocr"; 38 | import { KanbanWidgetCreator } from "./widgets/kanban"; 39 | // import { ABCWidgetCreator } from "./widgets/abc"; 40 | import { GitHubGistWidgetCreator } from "./widgets/github_gist"; 41 | 42 | // Set necessary window scope variables 43 | window["CodeMirror"] = require("codemirror"); 44 | 45 | // Register widget creators 46 | registerWidgetCreator("timer", TimerWidgetCreator); 47 | registerWidgetCreator("crossnote.image", ImageWidgetCreator); 48 | registerWidgetCreator("crossnote.audio", AudioWidgetCreator); 49 | // registerWidgetCreator("crossnote.netease_music", NeteaseMusicWidgetCreator); 50 | registerWidgetCreator("crossnote.video", VideoWidgetCreator); 51 | registerWidgetCreator("crossnote.bilibili", BilibiliWidgetCreator); 52 | registerWidgetCreator("crossnote.youtube", YoutubeWidgetCreator); 53 | registerWidgetCreator("crossnote.ocr", OCRWidgetCreator); 54 | registerWidgetCreator("crossnote.kanban", KanbanWidgetCreator); 55 | // registerWidgetCreator("crossnote.abc", ABCWidgetCreator); 56 | registerWidgetCreator("crossnote.github_gist", GitHubGistWidgetCreator); 57 | -------------------------------------------------------------------------------- /src/views/editor/views/README.md: -------------------------------------------------------------------------------- 1 | I have been too lazy to implement this part myself. 2 | So the code here is mainly copied from 3 | 4 | https://github.com/laobubu/HyperMD/tree/master/demo 5 | 6 | and slightly modified a bit afterwards. 7 | -------------------------------------------------------------------------------- /src/views/editor/views/float-win.ts: -------------------------------------------------------------------------------- 1 | export class FloatWin { 2 | private el: HTMLElement; 3 | public closeBtn: HTMLElement; 4 | public visible: boolean; 5 | 6 | constructor(id: string) { 7 | const win = document.getElementById(id); 8 | if (!win) { 9 | return; 10 | } 11 | 12 | /** @type {HTMLDivElement} */ 13 | const titlebar = win.querySelector(".float-win-title") as HTMLElement; 14 | titlebar.addEventListener( 15 | "selectstart", 16 | function () { 17 | return false; 18 | }, 19 | false, 20 | ); 21 | 22 | /** @type {HTMLButtonElement} */ 23 | const closeBtn = win.querySelector(".float-win-close") as HTMLElement; 24 | if (closeBtn) { 25 | closeBtn.addEventListener( 26 | "click", 27 | () => { 28 | this.hide(); 29 | }, 30 | false, 31 | ); 32 | win.addEventListener( 33 | "keyup", 34 | (ev) => { 35 | if (ev.keyCode === 27) this.hide(); // ESC 36 | }, 37 | false, 38 | ); 39 | } 40 | 41 | let boxX: number, 42 | boxY: number, 43 | mouseX: number, 44 | mouseY: number, 45 | offsetX: number, 46 | offsetY: number; 47 | 48 | titlebar.addEventListener( 49 | "mousedown", 50 | function (e) { 51 | if (e.target === closeBtn) return; 52 | 53 | boxX = win.offsetLeft; 54 | boxY = win.offsetTop; 55 | mouseX = getMouseXY(e).x; 56 | mouseY = getMouseXY(e).y; 57 | offsetX = mouseX - boxX; 58 | offsetY = mouseY - boxY; 59 | 60 | document.addEventListener("mousemove", move, false); 61 | document.addEventListener("mouseup", up, false); 62 | }, 63 | false, 64 | ); 65 | 66 | function move(e: any) { 67 | var x = getMouseXY(e).x - offsetX; 68 | var y = getMouseXY(e).y - offsetY; 69 | var width = document.documentElement.clientWidth - titlebar.offsetWidth; 70 | var height = 71 | document.documentElement.clientHeight - titlebar.offsetHeight; 72 | 73 | x = Math.min(Math.max(0, x), width); 74 | y = Math.min(Math.max(0, y), height); 75 | 76 | win.style.left = x + "px"; 77 | win.style.top = y + "px"; 78 | } 79 | 80 | function up(e: any) { 81 | document.removeEventListener("mousemove", move, false); 82 | document.removeEventListener("mouseup", up, false); 83 | } 84 | 85 | function getMouseXY(e: any) { 86 | var x = 0, 87 | y = 0; 88 | e = e || window.event; 89 | if (e.pageX) { 90 | x = e.pageX; 91 | y = e.pageY; 92 | } else { 93 | x = e.clientX + document.body.scrollLeft - document.body.clientLeft; 94 | y = e.clientY + document.body.scrollTop - document.body.clientTop; 95 | } 96 | return { 97 | x: x, 98 | y: y, 99 | }; 100 | } 101 | 102 | this.el = win; 103 | this.closeBtn = closeBtn; 104 | this.visible = !/float-win-hidden/.test(win.className); 105 | } 106 | 107 | public show(moveToCenter?: boolean) { 108 | if (this.visible) return; 109 | var el = this.el; 110 | 111 | this.visible = true; 112 | el.className = this.el.className.replace(/\s*(float-win-hidden\s*)+/g, " "); 113 | el.style.display = "block"; 114 | 115 | if (moveToCenter) { 116 | setTimeout(() => { 117 | this.moveTo( 118 | (window.innerWidth - el.offsetWidth) / 2, 119 | (window.innerHeight - el.offsetHeight) / 2, 120 | ); 121 | }, 0); 122 | } 123 | } 124 | 125 | public hide() { 126 | if (!this.visible) return; 127 | this.visible = false; 128 | this.el.className += " float-win-hidden"; 129 | this.el.style.display = "none"; 130 | } 131 | 132 | public moveTo(x: number, y: number) { 133 | var s = this.el.style; 134 | s.left = x + "px"; 135 | s.top = y + "px"; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/views/editor/views/math-preview.ts: -------------------------------------------------------------------------------- 1 | import { FloatWin } from "./float-win"; 2 | 3 | export function initMathPreview(cm: CodeMirror.Editor) { 4 | let mathRenderer: any = null; 5 | const win = new FloatWin("math-preview"); 6 | var supressed = false; 7 | 8 | win.closeBtn.addEventListener( 9 | "click", 10 | function () { 11 | supressed = true; // for current TeX block 12 | }, 13 | false, 14 | ); 15 | 16 | function updatePreview(expr: string) { 17 | if (supressed) return; 18 | 19 | if (!mathRenderer) { 20 | // initialize renderer and preview window 21 | mathRenderer = cm.hmd.FoldMath.createRenderer( 22 | document.getElementById("math-preview-content"), 23 | "display", 24 | ); 25 | mathRenderer.onChanged = function () { 26 | // finished rendering. show the window 27 | if (!win.visible) { 28 | var cursorPos = cm.charCoords(cm.getCursor(), "window"); 29 | win.moveTo(cursorPos.left, cursorPos.bottom); 30 | } 31 | win.show(); 32 | }; 33 | } 34 | 35 | // console.log("[MathPreview] " + expr); 36 | 37 | if (!mathRenderer.isReady()) return; 38 | mathRenderer.startRender(expr); 39 | } 40 | 41 | function hidePreview() { 42 | // console.log("[MathPreview] (exit)"); 43 | 44 | win.hide(); 45 | supressed = false; 46 | } 47 | 48 | cm.setOption("hmdFoldMath", { 49 | onPreview: updatePreview, 50 | onPreviewEnd: hidePreview, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/views/editor/widgets/README.md: -------------------------------------------------------------------------------- 1 | # Widgets 2 | -------------------------------------------------------------------------------- /src/views/editor/widgets/abc/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState, useEffect } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import clsx from "clsx"; 5 | import { 6 | createStyles, 7 | makeStyles, 8 | Theme, 9 | ThemeProvider, 10 | } from "@material-ui/core/styles"; 11 | import { Box, IconButton, Card } from "@material-ui/core"; 12 | import { useTranslation } from "react-i18next"; 13 | import { generateUUID } from "../../../../util/util"; 14 | 15 | // @ts-ignore 16 | import abcjs from "abcjs"; 17 | import "abcjs/abcjs-audio.css"; 18 | import { ContentSave } from "mdi-material-ui"; 19 | import { selectedTheme } from "../../../themes/manager"; 20 | 21 | const useStyles = makeStyles((theme: Theme) => 22 | createStyles({ 23 | card: { 24 | padding: theme.spacing(2), 25 | position: "relative", 26 | }, 27 | editorWrapper: { 28 | position: "relative", 29 | padding: "2px", 30 | }, 31 | editor: { 32 | width: "100%", 33 | height: "128px", 34 | resize: "none", 35 | border: "none", 36 | backgroundColor: theme.palette.background.paper, 37 | color: theme.palette.text.primary, 38 | }, 39 | canvas: { 40 | overflow: "auto !important", 41 | height: "100% !important", 42 | }, 43 | saveBtn: { 44 | position: "absolute", 45 | top: "0", 46 | right: "0", 47 | }, 48 | }) 49 | ); 50 | 51 | function ABCWidget(props: WidgetArgs) { 52 | const attributes = props.attributes; 53 | const classes = useStyles(props); 54 | const { t } = useTranslation(); 55 | const [abcEditorID] = useState("abc-editor-" + generateUUID()); 56 | const [abcWarningsID] = useState("abc-warnings-" + generateUUID()); 57 | const [abcCanvasID] = useState("abc-canvas-" + generateUUID()); 58 | const [abcAudioID] = useState("abc-audio-" + generateUUID()); 59 | const [editorElement, setEditorElement] = useState(null); 60 | const [warningsElement, setWarningsElement] = useState(null); 61 | const [canvasElement, setCanvasElement] = useState(null); 62 | const [audioControlElement, setAudioControlElement] = useState( 63 | null 64 | ); 65 | const [hideEditor, setHideEditor] = useState(props.isPreview); 66 | 67 | const [abc, setABC] = useState( 68 | attributes["abc"] || 69 | `X: 1 70 | T: Cooley's 71 | M: 4/4 72 | L: 1/8 73 | K: Emin 74 | |:D2|EB{c}BA B2 EB|~B2 AB dBAG|FDAD BDAD|FDAD dAFD|` 75 | ); 76 | 77 | useEffect(() => { 78 | if ( 79 | editorElement && 80 | warningsElement && 81 | canvasElement && 82 | audioControlElement 83 | ) { 84 | setTimeout(() => { 85 | if (!document.getElementById(editorElement.id)) { 86 | return; 87 | } 88 | const editor = new abcjs.Editor(editorElement.id, { 89 | canvas_id: canvasElement.id, 90 | warnings_id: warningsElement.id, 91 | generate_warnings: true, 92 | synth: { 93 | el: `#${audioControlElement.id}`, 94 | options: { 95 | displayLoop: true, 96 | displayRestart: true, 97 | displayPlay: true, 98 | displayProgress: true, 99 | displayWarp: true, 100 | }, 101 | }, 102 | abcjsParams: { 103 | generateDownload: true, 104 | }, 105 | }); 106 | 107 | // Test synth 108 | }, 1000); 109 | } 110 | }, [editorElement, warningsElement, canvasElement, audioControlElement]); 111 | 112 | return ( 113 | 114 | {/*{t("widget/crossnote.abc/title")}*/} 115 | 122 | 134 | {!props.isPreview && attributes["abc"] !== abc && ( 135 | { 138 | props.setAttributes(Object.assign(props.attributes, { abc })); 139 | }} 140 | > 141 | 142 | 143 | )} 144 | 145 |
{ 148 | setWarningsElement(element); 149 | }} 150 | >
151 |
{ 155 | setCanvasElement(element); 156 | }} 157 | >
158 |
{ 161 | setAudioControlElement(element); 162 | }} 163 | >
164 |
165 | ); 166 | } 167 | 168 | export const ABCWidgetCreator: WidgetCreator = (args) => { 169 | const el = document.createElement("span"); 170 | ReactDOM.render( 171 | 172 | 173 | , 174 | el 175 | ); 176 | return el; 177 | }; 178 | -------------------------------------------------------------------------------- /src/views/editor/widgets/audio/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | Switch, 12 | FormControlLabel, 13 | darken, 14 | } from "@material-ui/core"; 15 | import { 16 | createStyles, 17 | makeStyles, 18 | Theme, 19 | ThemeProvider, 20 | } from "@material-ui/core/styles"; 21 | import clsx from "clsx"; 22 | import { TrashCan } from "mdi-material-ui"; 23 | import { useTranslation } from "react-i18next"; 24 | import { selectedTheme } from "../../../themes/manager"; 25 | 26 | const useStyles = makeStyles((theme: Theme) => 27 | createStyles({ 28 | card: { 29 | padding: theme.spacing(2), 30 | position: "relative", 31 | }, 32 | actionButtons: { 33 | position: "absolute", 34 | top: "0", 35 | right: "0", 36 | }, 37 | section: { 38 | marginTop: theme.spacing(2), 39 | }, 40 | disabled: { 41 | cursor: "not-allowed", 42 | }, 43 | }) 44 | ); 45 | 46 | function AudioWidget(props: WidgetArgs) { 47 | const attributes = props.attributes; 48 | const classes = useStyles(props); 49 | const { t } = useTranslation(); 50 | const [source, setSource] = useState(attributes["source"] || ""); 51 | const [autoplay, setAutoplay] = useState( 52 | attributes["autoplay"] || false 53 | ); 54 | const [controls, setControls] = useState( 55 | attributes["controls"] || true 56 | ); 57 | const [loop, setLoop] = useState(attributes["loop"] || false); 58 | const [muted, setMuted] = useState(attributes["muted"] || false); 59 | 60 | if (attributes["src"]) { 61 | return ( 62 | 63 | 73 | {!props.isPreview && !attributes["controls"] && "🎵"} 74 | 75 | ); 76 | } 77 | 78 | if (props.isPreview) { 79 | return ; 80 | } 81 | 82 | return ( 83 | 84 | {t("general/Audio")} 85 | 86 | 87 | props.removeSelf()}> 88 | 89 | 90 | 91 | 92 | 93 | 94 | {t("general/source-url")} 95 | 96 | { 101 | setSource(event.target.value); 102 | }} 103 | onKeyDown={(event) => { 104 | if (event.which === 13) { 105 | if (source) { 106 | const attrs = { 107 | autoplay, 108 | controls, 109 | loop, 110 | muted, 111 | src: source, 112 | }; 113 | props.setAttributes(attrs); 114 | } 115 | } 116 | }} 117 | fullWidth={true} 118 | > 119 | 120 | 121 | setAutoplay(!autoplay)} 127 | color={"primary"} 128 | > 129 | } 130 | > 131 | setControls(!controls)} 137 | color={"primary"} 138 | > 139 | } 140 | > 141 | setLoop(!loop)} 147 | color={"primary"} 148 | > 149 | } 150 | > 151 | setMuted(!muted)} 157 | color={"primary"} 158 | > 159 | } 160 | > 161 | 162 | 163 | ); 164 | } 165 | 166 | export const AudioWidgetCreator: WidgetCreator = (args) => { 167 | const el = document.createElement("span"); 168 | ReactDOM.render( 169 | 170 | 171 | , 172 | el 173 | ); 174 | return el; 175 | }; 176 | -------------------------------------------------------------------------------- /src/views/editor/widgets/bilibili/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | } from "@material-ui/core"; 12 | import { 13 | createStyles, 14 | makeStyles, 15 | Theme, 16 | ThemeProvider, 17 | } from "@material-ui/core/styles"; 18 | import clsx from "clsx"; 19 | import { TrashCan } from "mdi-material-ui"; 20 | import { useTranslation } from "react-i18next"; 21 | import { selectedTheme } from "../../../themes/manager"; 22 | 23 | const useStyles = makeStyles((theme: Theme) => 24 | createStyles({ 25 | card: { 26 | padding: theme.spacing(2), 27 | position: "relative", 28 | }, 29 | actionButtons: { 30 | position: "absolute", 31 | top: "0", 32 | right: "0", 33 | }, 34 | section: { 35 | marginTop: theme.spacing(2), 36 | }, 37 | videoWrapper: { 38 | cursor: "default", 39 | position: "relative", 40 | width: "100%", 41 | height: "0", 42 | paddingTop: "56.25%", 43 | }, 44 | video: { 45 | backgroundColor: "#ddd", 46 | border: "none", 47 | position: "absolute", 48 | left: "0", 49 | top: "0", 50 | width: "100%", 51 | height: "100%", 52 | }, 53 | errorMessage: { 54 | color: "#f44336", 55 | marginTop: theme.spacing(2), 56 | }, 57 | }) 58 | ); 59 | 60 | function BilibiliWidget(props: WidgetArgs) { 61 | const attributes = props.attributes; 62 | const classes = useStyles(props); 63 | const { t } = useTranslation(); 64 | const [url, setURL] = useState(""); 65 | const [error, setError] = useState(""); 66 | 67 | if (attributes["aid"]) { 68 | return ( 69 | 70 | 71 | 79 | 80 | 81 | ); 82 | } 83 | if (attributes["bvid"]) { 84 | return ( 85 | 86 | 87 | 95 | 96 | 97 | ); 98 | } 99 | 100 | if (props.isPreview) { 101 | return ; 102 | } 103 | 104 | return ( 105 | 106 | 107 | {t("widget/crossnote.bilibili/Bilibili")} 108 | 109 | 110 | 111 | props.removeSelf()}> 112 | 113 | 114 | 115 | 116 | 117 | 118 | {t("widget/crossnote.bilibili/bilibili-video-url")} 119 | 120 | { 125 | setURL(event.target.value); 126 | setError(""); 127 | }} 128 | onKeyDown={(event) => { 129 | if (event.which === 13) { 130 | if (url && url.match(/\/av(\d+)/)) { 131 | const aid = url.match(/\/av(\d+)/)[1]; 132 | const attrs = { 133 | aid, 134 | }; 135 | props.setAttributes(attrs); 136 | } else if (url && url.match(/\/BV(.+?)($|\/|\?)/)) { 137 | const bvid = url.match(/\/BV(.+?)($|\/|\?)/)[1]; 138 | const attrs = { 139 | bvid, 140 | }; 141 | props.setAttributes(attrs); 142 | } else { 143 | setError(t("widget/crossnote.bilibili/error_message")); 144 | } 145 | } 146 | }} 147 | fullWidth={true} 148 | > 149 | {error} 150 | 151 | 152 | ); 153 | } 154 | 155 | export const BilibiliWidgetCreator: WidgetCreator = (args) => { 156 | const el = document.createElement("span"); 157 | ReactDOM.render( 158 | 159 | 160 | , 161 | el 162 | ); 163 | return el; 164 | }; 165 | -------------------------------------------------------------------------------- /src/views/editor/widgets/github_gist/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState, useCallback, useEffect } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | TextField, 10 | Tooltip, 11 | } from "@material-ui/core"; 12 | import { 13 | createStyles, 14 | makeStyles, 15 | Theme, 16 | ThemeProvider, 17 | } from "@material-ui/core/styles"; 18 | import clsx from "clsx"; 19 | // @ts-ignore 20 | import Gist from "super-react-gist"; // <-- import the library 21 | import { TrashCanOutline, TrashCan } from "mdi-material-ui"; 22 | import { useTranslation } from "react-i18next"; 23 | import { selectedTheme } from "../../../themes/manager"; 24 | 25 | const useStyles = makeStyles((theme: Theme) => 26 | createStyles({ 27 | card: { 28 | padding: theme.spacing(2), 29 | position: "relative", 30 | }, 31 | actionButtonsGroup: { 32 | position: "absolute", 33 | top: "0", 34 | right: "0", 35 | display: "flex", 36 | alignItems: "center", 37 | }, 38 | }) 39 | ); 40 | 41 | function GitHubGistWidget(props: WidgetArgs) { 42 | const classes = useStyles(props); 43 | const { t } = useTranslation(); 44 | const [url, setURL] = useState(""); 45 | 46 | const setGistURL = useCallback( 47 | (url: string) => { 48 | try { 49 | const location = new URL(url); 50 | if (location.host !== "gist.github.com") { 51 | return; 52 | } else { 53 | props.setAttributes({ 54 | url: location.origin + location.pathname, 55 | }); 56 | } 57 | } catch (error) {} 58 | }, 59 | [props] 60 | ); 61 | 62 | if (props.attributes["url"]) { 63 | return ( 64 | 65 | 66 | {!props.isPreview && ( 67 | 68 | props.removeSelf()}> 69 | 70 | 71 | 72 | )} 73 | 74 | ); 75 | } 76 | 77 | if (props.isPreview) { 78 | return null; 79 | } 80 | 81 | return ( 82 | 83 | 84 | {t("widget/crossnote.github_gist/title")} 85 | 86 | setURL(event.target.value)} 91 | fullWidth={true} 92 | onKeyUp={(event) => { 93 | if (event.which === 13) { 94 | setGistURL(url); 95 | } 96 | }} 97 | > 98 | 99 | 100 | props.removeSelf()}> 101 | 102 | 103 | 104 | 105 | 106 | ); 107 | } 108 | 109 | export const GitHubGistWidgetCreator: WidgetCreator = (args) => { 110 | const el = document.createElement("span"); 111 | ReactDOM.render( 112 | 113 | 114 | , 115 | el 116 | ); 117 | return el; 118 | }; 119 | -------------------------------------------------------------------------------- /src/views/editor/widgets/image/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | } from "@material-ui/core"; 12 | import { 13 | createStyles, 14 | makeStyles, 15 | Theme, 16 | ThemeProvider, 17 | lighten, 18 | darken, 19 | } from "@material-ui/core/styles"; 20 | import clsx from "clsx"; 21 | import { TrashCan } from "mdi-material-ui"; 22 | import { useTranslation } from "react-i18next"; 23 | import { smmsUploadImages } from "../../../../util/image_uploader"; 24 | import Noty from "noty"; 25 | import { selectedTheme } from "../../../themes/manager"; 26 | 27 | const useStyles = makeStyles((theme: Theme) => 28 | createStyles({ 29 | card: { 30 | padding: theme.spacing(2), 31 | position: "relative", 32 | }, 33 | actionButtons: { 34 | position: "absolute", 35 | top: "0", 36 | right: "0", 37 | }, 38 | section: { 39 | marginTop: theme.spacing(2), 40 | }, 41 | dropArea: { 42 | textAlign: "center", 43 | padding: "24px", 44 | border: "4px dotted #c7c7c7", 45 | backgroundColor: darken(theme.palette.background.paper, 0.01), 46 | cursor: "pointer", 47 | "&:hover": { 48 | backgroundColor: darken(theme.palette.background.paper, 0.2), 49 | }, 50 | }, 51 | disabled: { 52 | cursor: "not-allowed", 53 | }, 54 | }) 55 | ); 56 | 57 | function ImageWidget(props: WidgetArgs) { 58 | const classes = useStyles(props); 59 | const { t } = useTranslation(); 60 | const [url, setURL] = useState(""); 61 | const [imageUploaderElement, setImageUploaderElement] = useState< 62 | HTMLInputElement 63 | >(null); 64 | const [uploadingImages, setUploadingImages] = useState(false); 65 | 66 | function clickDropArea(e: any) { 67 | e.preventDefault(); 68 | e.stopPropagation(); 69 | if (!imageUploaderElement || uploadingImages) return; 70 | imageUploaderElement.onchange = function (event) { 71 | const target = event.target as any; 72 | const files = target.files || []; 73 | new Noty({ 74 | type: "info", 75 | text: t("utils/uploading-image"), 76 | layout: "topRight", 77 | theme: "relax", 78 | timeout: 2000, 79 | }).show(); 80 | setUploadingImages(true); 81 | smmsUploadImages(files) 82 | .then((urls) => { 83 | let markdown = ``; 84 | urls.forEach((url) => { 85 | markdown = markdown + `![](${url}) \n`; 86 | }); 87 | props.replaceSelf(markdown); 88 | }) 89 | .catch((error: any) => { 90 | // console.log(error); 91 | setUploadingImages(false); 92 | new Noty({ 93 | type: "error", 94 | text: t("utils/upload-image-failure"), 95 | layout: "topRight", 96 | theme: "relax", 97 | timeout: 2000, 98 | }).show(); 99 | }); 100 | }; 101 | imageUploaderElement.click(); 102 | } 103 | 104 | if (props.isPreview) { 105 | return ; 106 | } 107 | 108 | return ( 109 | 110 | 111 | {t("widget/crossnote.image/image-helper")} 112 | 113 | 114 | 115 | props.removeSelf()}> 116 | 117 | 118 | 119 | 120 | {!uploadingImages && ( 121 | 122 | 123 | {"URL"} 124 | 125 | { 130 | setURL(event.target.value); 131 | }} 132 | onKeyDown={(event) => { 133 | if (event.which === 13) { 134 | props.replaceSelf(`![](${url})\n`); 135 | } 136 | }} 137 | fullWidth={true} 138 | > 139 | 140 | )} 141 | 142 | 143 | {t("general/Upload")} 144 | 145 | 152 | 153 | {uploadingImages 154 | ? t("utils/uploading-image") 155 | : t("widget/crossnote.image/click-here-to-browse-image-file")} 156 | 157 | 158 | 159 | 160 | 161 | {t("widget/crossnote.image/thanks_sm_ms")} 162 | 163 | 164 | { 169 | setImageUploaderElement(element); 170 | }} 171 | > 172 | 173 | ); 174 | } 175 | 176 | export const ImageWidgetCreator: WidgetCreator = (args) => { 177 | const el = document.createElement("span"); 178 | ReactDOM.render( 179 | 180 | 181 | , 182 | el 183 | ); 184 | return el; 185 | }; 186 | -------------------------------------------------------------------------------- /src/views/editor/widgets/kanban/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState, useEffect } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | TextField, 10 | Button, 11 | Dialog, 12 | DialogContent, 13 | DialogActions, 14 | } from "@material-ui/core"; 15 | import { 16 | createStyles, 17 | makeStyles, 18 | Theme, 19 | ThemeProvider, 20 | } from "@material-ui/core/styles"; 21 | import clsx from "clsx"; 22 | import { 23 | CardPlus, 24 | Close, 25 | ContentSave, 26 | Cancel, 27 | Plus, 28 | TrashCan, 29 | Pencil, 30 | } from "mdi-material-ui"; 31 | import { useTranslation } from "react-i18next"; 32 | 33 | // @ts-ignore 34 | import Board from "@lourenci/react-kanban"; 35 | import { Editor as CodeMirrorEditor } from "codemirror"; 36 | import { renderPreview } from "vickymd/preview"; 37 | import { selectedTheme } from "../../../themes/manager"; 38 | import { setTheme } from "vickymd/theme"; 39 | import { crossnoteSettings } from "../../../util/util"; 40 | const VickyMD = require("vickymd/core"); 41 | 42 | const useStyles = makeStyles((theme: Theme) => 43 | createStyles({ 44 | columnHeader: { 45 | width: "256px", 46 | maxWidth: "100%", 47 | display: "flex", 48 | alignItems: "center", 49 | justifyContent: "space-between", 50 | color: "#000", // BUG: TODO: Wait for react-kanban styling support 51 | }, 52 | kanbanCard: { 53 | width: "256px", 54 | maxWidth: "100%", 55 | position: "relative", 56 | backgroundColor: theme.palette.background.paper, 57 | color: theme.palette.text.primary, 58 | [theme.breakpoints.down("sm")]: { 59 | marginTop: "4px", 60 | marginBottom: "4px", 61 | }, 62 | }, 63 | editorWrapper: { 64 | // height: "160px", 65 | // border: "2px solid #96c3e6", 66 | "& .CodeMirror-gutters": { 67 | display: "none", 68 | }, 69 | }, 70 | textarea: { 71 | width: "100%", 72 | height: "100%", 73 | }, 74 | preview: { 75 | padding: theme.spacing(2), 76 | }, 77 | }) 78 | ); 79 | 80 | interface KanbanCard { 81 | id: number; 82 | title: string; 83 | description: string; 84 | } 85 | 86 | interface kanbanColumn { 87 | id: number; 88 | title: string; 89 | wip: boolean; 90 | cards: KanbanCard[]; 91 | } 92 | 93 | interface KanbanBoard { 94 | columns: kanbanColumn[]; 95 | } 96 | 97 | interface KanbanColumnHeaderProps { 98 | column: kanbanColumn; 99 | board: KanbanBoard; 100 | refreshBoard: (board: Board) => void; 101 | isPreview: boolean; 102 | } 103 | 104 | function KanbanColumnHeaderDisplay(props: KanbanColumnHeaderProps) { 105 | const classes = useStyles(props); 106 | const { t } = useTranslation(); 107 | const column = props.column; 108 | const board = props.board; 109 | const isPreview = props.isPreview; 110 | const refreshBoard = props.refreshBoard; 111 | const [clickedTitle, setClickedTitle] = useState(false); 112 | const [titleValue, setTitleValue] = useState(column.title); 113 | 114 | useEffect(() => { 115 | if (!clickedTitle && titleValue !== column.title) { 116 | column.title = titleValue || t("general/Untitled"); 117 | setTitleValue(column.title); 118 | refreshBoard(board); 119 | } 120 | }, [clickedTitle, board, column.title, titleValue, t, refreshBoard]); 121 | 122 | return ( 123 | 124 | 125 | {clickedTitle ? ( 126 | { 129 | setTitleValue(event.target.value); 130 | }} 131 | onBlur={() => { 132 | setClickedTitle(false); 133 | }} 134 | onKeyUp={(event) => { 135 | if (event.which === 13) { 136 | setClickedTitle(false); 137 | } 138 | }} 139 | > 140 | ) : ( 141 | { 145 | if (!isPreview) { 146 | setClickedTitle(true); 147 | } 148 | }} 149 | > 150 | {titleValue} 151 | 152 | )} 153 | 154 | {!isPreview && ( 155 | 156 | { 158 | const card: KanbanCard = { 159 | id: Date.now(), 160 | title: "", //"Card " + column.cards.length, 161 | description: t("general/empty"), 162 | }; 163 | if (column) { 164 | column.cards.push(card); 165 | } 166 | props.refreshBoard(board); 167 | }} 168 | > 169 | 170 | 171 | { 173 | board.columns = board.columns.filter((l) => column.id !== l.id); 174 | props.refreshBoard(board); 175 | }} 176 | > 177 | 178 | 179 | 180 | )} 181 | 182 | ); 183 | } 184 | 185 | interface KanbanCardProps { 186 | card: KanbanCard; 187 | board: KanbanBoard; 188 | refreshBoard: (board: Board) => void; 189 | isPreview: boolean; 190 | } 191 | function KanbanCardDisplay(props: KanbanCardProps) { 192 | const classes = useStyles(props); 193 | const board = props.board; 194 | const card = props.card; 195 | const isPreview = props.isPreview; 196 | const [textAreaElement, setTextAreaElement] = useState( 197 | null 198 | ); 199 | const [previewElement, setPreviewElement] = React.useState(null); 200 | 201 | const [editor, setEditor] = useState(null); 202 | const [description, setDescription] = useState(card.description); 203 | const [editorDialogOpen, setEditDialogOpen] = useState(false); 204 | const { t } = useTranslation(); 205 | 206 | useEffect(() => { 207 | setDescription(card.description); 208 | }, [card.description]); 209 | 210 | useEffect(() => { 211 | if (textAreaElement) { 212 | const editor: CodeMirrorEditor = VickyMD.fromTextArea(textAreaElement, { 213 | mode: { 214 | name: "hypermd", 215 | hashtag: true, 216 | }, 217 | keyMap: crossnoteSettings.keyMap, 218 | showCursorWhenSelecting: true, 219 | inputStyle: "contenteditable", 220 | }); 221 | editor.setValue(card.description); 222 | editor.setOption("lineNumbers", false); 223 | editor.setOption("foldGutter", false); 224 | editor.setOption("autofocus", false); 225 | if (isPreview) { 226 | editor.setOption("readOnly", "nocursor"); 227 | } 228 | editor.on("changes", () => { 229 | setDescription(editor.getValue()); 230 | }); 231 | editor.focus(); 232 | /* 233 | // Cause save not working 234 | editor.on("blur", () => { 235 | setClickedPreview(false); 236 | setEditor(null); 237 | }); 238 | */ 239 | // editor.display.input.blur(); 240 | setEditor(editor); 241 | } 242 | }, [textAreaElement]); 243 | 244 | useEffect(() => { 245 | if (editor) { 246 | setTheme({ 247 | editor, 248 | themeName: selectedTheme.name, 249 | baseUri: "/styles/", 250 | }); 251 | } 252 | }, [editor]); 253 | 254 | useEffect(() => { 255 | if (previewElement) { 256 | renderPreview(previewElement, card.description); 257 | } 258 | }, [previewElement]); 259 | 260 | return ( 261 | 262 |
{ 265 | setPreviewElement(element); 266 | }} 267 | >
268 | {!isPreview && ( 269 | 270 | setEditDialogOpen(true)}> 271 | 272 | 273 | { 275 | board.columns.forEach((column) => { 276 | column.cards = column.cards.filter((c) => c.id !== card.id); 277 | }); 278 | props.refreshBoard(board); 279 | }} 280 | > 281 | 282 | 283 | 284 | )} 285 | setEditDialogOpen(false)} 288 | style={{ zIndex: 3000 }} 289 | > 290 | 291 | 295 | 301 | 302 | 303 | 304 | { 306 | card.description = description; 307 | props.refreshBoard(props.board); 308 | setEditDialogOpen(false); 309 | }} 310 | > 311 | 312 | 313 | { 315 | if (editor) { 316 | editor.setValue(card.description); 317 | } 318 | setDescription(card.description); 319 | setEditDialogOpen(false); 320 | }} 321 | > 322 | 323 | 324 | 325 | 326 |
327 | ); 328 | } 329 | 330 | function KanbanWidget(props: WidgetArgs) { 331 | const classes = useStyles(props); 332 | const { t } = useTranslation(); 333 | const [board, setBoard] = useState( 334 | props.attributes["board"] || { 335 | columns: [], 336 | } 337 | ); 338 | 339 | if ("lanes" in (board as any)) { 340 | board.columns = (board as any).lanes; 341 | } 342 | 343 | const refreshBoard = (board: KanbanBoard) => { 344 | const newBoard = Object.assign({}, board); 345 | props.setAttributes({ board: newBoard }); 346 | setBoard(newBoard as KanbanBoard); 347 | }; 348 | 349 | return ( 350 |
351 | ( 353 | 359 | )} 360 | renderCard={(card: KanbanCard, { dragging }: { dragging: boolean }) => { 361 | return ( 362 | 368 | ); 369 | }} 370 | allowAddColumn={!props.isPreview} 371 | allowAddCard={!props.isPreview} 372 | renderColumnAdder={(): any => { 373 | return ( 374 | 375 | 391 | 394 | 395 | ); 396 | // return ; 397 | }} 398 | onNewColumnConfirm={(newColumn: any) => { 399 | board.columns.push({ id: Date.now(), ...newColumn }); 400 | refreshBoard(board); 401 | }} 402 | disableCardDrag={props.isPreview} 403 | disableColumnDrag={props.isPreview} 404 | onCardDragEnd={( 405 | card: KanbanCard, 406 | source: { fromPosition: number; fromColumnId: number }, 407 | destination: { toPosition: number; toColumnId: number } 408 | ) => { 409 | const { fromPosition, fromColumnId } = source; 410 | let { toPosition, toColumnId } = destination; 411 | const fromColumn = board.columns.filter( 412 | (l) => l.id === fromColumnId 413 | )[0]; 414 | const toColumn = board.columns.filter((l) => l.id === toColumnId)[0]; 415 | fromColumn.cards.splice(fromPosition, 1); 416 | toColumn.cards = [ 417 | ...toColumn.cards.slice(0, toPosition), 418 | card, 419 | ...toColumn.cards.slice(toPosition, toColumn.cards.length), 420 | ]; 421 | 422 | refreshBoard(board); 423 | }} 424 | onColumnDragEnd={( 425 | b: KanbanBoard, 426 | source: { fromPosition: number }, 427 | destination: { toPosition: number } 428 | ) => { 429 | const fromPosition: number = source.fromPosition; 430 | const toPosition: number = destination.toPosition; 431 | const fromColumn = board.columns[fromPosition]; 432 | const toColumn = board.columns[toPosition]; 433 | board.columns[toPosition] = fromColumn; 434 | board.columns[fromPosition] = toColumn; 435 | refreshBoard(board); 436 | }} 437 | > 438 | {board} 439 | 440 |
441 | ); 442 | } 443 | 444 | export const KanbanWidgetCreator: WidgetCreator = (args) => { 445 | const el = document.createElement("span"); 446 | ReactDOM.render( 447 | 448 | 449 | , 450 | el 451 | ); 452 | return el; 453 | }; 454 | -------------------------------------------------------------------------------- /src/views/editor/widgets/netease_music/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | Switch, 12 | FormControlLabel, 13 | darken, 14 | } from "@material-ui/core"; 15 | import { 16 | createStyles, 17 | makeStyles, 18 | Theme, 19 | ThemeProvider, 20 | } from "@material-ui/core/styles"; 21 | import clsx from "clsx"; 22 | import { TrashCan } from "mdi-material-ui"; 23 | import { useTranslation } from "react-i18next"; 24 | import { selectedTheme } from "../../../themes/manager"; 25 | 26 | const useStyles = makeStyles((theme: Theme) => 27 | createStyles({ 28 | card: { 29 | padding: theme.spacing(2), 30 | position: "relative", 31 | }, 32 | actionButtons: { 33 | position: "absolute", 34 | top: "0", 35 | right: "0", 36 | }, 37 | section: { 38 | marginTop: theme.spacing(2), 39 | }, 40 | disabled: { 41 | cursor: "not-allowed", 42 | }, 43 | }) 44 | ); 45 | 46 | function NeteaseMusicWidget(props: WidgetArgs) { 47 | const attributes = props.attributes; 48 | const classes = useStyles(props); 49 | const { t } = useTranslation(); 50 | const [id, setID] = useState(attributes["id"] || ""); 51 | const [autoplay, setAutoplay] = useState( 52 | attributes["autoplay"] || false 53 | ); 54 | 55 | if (attributes["id"]) { 56 | return ( 57 | 69 | ); 70 | } 71 | 72 | if (props.isPreview) { 73 | return ; 74 | } 75 | 76 | return ( 77 | 78 | 79 | {t("editor/toolbar/netease-music")} 80 | 81 | 82 | 83 | props.removeSelf()}> 84 | 85 | 86 | 87 | 88 | 89 | 90 | {t("general/ID")} 91 | 92 | { 97 | setID(event.target.value); 98 | }} 99 | onKeyDown={(event) => { 100 | if (event.which === 13) { 101 | if (id) { 102 | const attrs = { 103 | autoplay, 104 | id, 105 | }; 106 | props.setAttributes(attrs); 107 | } 108 | } 109 | }} 110 | fullWidth={true} 111 | > 112 | 113 | 114 | setAutoplay(!autoplay)} 120 | color={"primary"} 121 | > 122 | } 123 | > 124 | 125 | 126 | ); 127 | } 128 | 129 | export const NeteaseMusicWidgetCreator: WidgetCreator = (args) => { 130 | const el = document.createElement("span"); 131 | ReactDOM.render( 132 | 133 | 134 | , 135 | el 136 | ); 137 | return el; 138 | }; 139 | -------------------------------------------------------------------------------- /src/views/editor/widgets/ocr/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState, useEffect } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | Switch, 12 | FormControlLabel, 13 | ListItem, 14 | ListItemText, 15 | List, 16 | ListItemSecondaryAction, 17 | FormGroup, 18 | Checkbox, 19 | ButtonGroup, 20 | Button, 21 | } from "@material-ui/core"; 22 | import { 23 | createStyles, 24 | makeStyles, 25 | Theme, 26 | ThemeProvider, 27 | darken, 28 | } from "@material-ui/core/styles"; 29 | import clsx from "clsx"; 30 | import { TrashCan } from "mdi-material-ui"; 31 | import { useTranslation } from "react-i18next"; 32 | import { createWorker } from "tesseract.js"; 33 | import { selectedTheme } from "../../../themes/manager"; 34 | 35 | const useStyles = makeStyles((theme: Theme) => 36 | createStyles({ 37 | card: { 38 | padding: theme.spacing(2), 39 | position: "relative", 40 | }, 41 | actionButtons: { 42 | position: "absolute", 43 | top: "0", 44 | right: "0", 45 | }, 46 | section: { 47 | marginTop: theme.spacing(2), 48 | }, 49 | dropArea: { 50 | textAlign: "center", 51 | padding: "24px", 52 | border: "4px dotted #c7c7c7", 53 | backgroundColor: darken(theme.palette.background.paper, 0.01), 54 | cursor: "pointer", 55 | "&:hover": { 56 | backgroundColor: darken(theme.palette.background.paper, 0.2), 57 | }, 58 | }, 59 | canvasWrapper: { 60 | marginTop: theme.spacing(2), 61 | // height: 0, 62 | // paddingTop: "56.25%" // 16:9 63 | }, 64 | canvas: { 65 | maxWidth: "100%", 66 | }, 67 | disabled: { 68 | cursor: "not-allowed", 69 | }, 70 | }) 71 | ); 72 | 73 | interface OCRProgress { 74 | status: string; 75 | progress: number; 76 | workerId?: string; 77 | } 78 | 79 | function getInitialLanguages() { 80 | try { 81 | return JSON.parse( 82 | localStorage.getItem("widget/crossnote.ocr/languages") || '["eng"]' 83 | ); 84 | } catch (error) { 85 | return ["eng"]; 86 | } 87 | } 88 | 89 | function OCRWidget(props: WidgetArgs) { 90 | const classes = useStyles(props); 91 | const { t } = useTranslation(); 92 | const [canvas, setCanvas] = useState(null); 93 | // https://github.com/tesseract-ocr/tesseract/wiki/Data-Files#data-files-for-version-400-november-29-2016 94 | const [link, setLink] = useState(""); 95 | const [imageDataURL, setImageDataURL] = useState(""); 96 | const [ocrDataURL, setOCRDataURL] = useState(""); 97 | const [imageDropAreaElement, setImageDropAreaElement] = useState< 98 | HTMLInputElement 99 | >(null); 100 | const [isProcessing, setIsProcessing] = useState(false); 101 | const [ocrProgresses, setOCRProgresses] = useState([]); 102 | const [selectedLanguages, setSelectedLanguages] = useState( 103 | getInitialLanguages() 104 | ); 105 | const [grayscaleChecked, setGrayscaleChecked] = useState(true); 106 | 107 | useEffect(() => { 108 | if (canvas && imageDataURL) { 109 | const imageObject = new Image(); 110 | const context = canvas.getContext("2d"); 111 | imageObject.onload = function () { 112 | canvas.width = imageObject.width; 113 | canvas.height = imageObject.height; 114 | context.clearRect(0, 0, canvas.width, canvas.height); 115 | if (grayscaleChecked) { 116 | context.fillStyle = "#FFF"; 117 | context.fillRect(0, 0, canvas.width, canvas.height); 118 | context.globalCompositeOperation = "luminosity"; 119 | } 120 | context.drawImage(imageObject, 0, 0); 121 | setOCRDataURL(canvas.toDataURL()); 122 | }; 123 | imageObject.onerror = (error) => { 124 | throw error; 125 | }; 126 | imageObject.setAttribute("crossOrigin", "anonymous"); 127 | imageObject.src = imageDataURL; 128 | } 129 | }, [canvas, imageDataURL, grayscaleChecked]); 130 | 131 | function clickDropArea(e: any) { 132 | e.preventDefault(); 133 | e.stopPropagation(); 134 | if (!imageDropAreaElement || isProcessing) return; 135 | imageDropAreaElement.onchange = function (event) { 136 | const target = event.target as any; 137 | const files = target.files || []; 138 | if (files.length) { 139 | try { 140 | const file = files[0] as File; 141 | const reader = new FileReader(); 142 | reader.readAsDataURL(file); 143 | reader.onload = () => { 144 | setImageDataURL(reader.result as string); 145 | }; 146 | reader.onerror = (error) => { 147 | throw error; 148 | }; 149 | } catch (error) {} 150 | } 151 | }; 152 | imageDropAreaElement.click(); 153 | } 154 | 155 | function startOCRFromLink() { 156 | try { 157 | setImageDataURL(link); 158 | } catch (error) {} 159 | } 160 | 161 | function ocr(input: File | string | HTMLCanvasElement) { 162 | const worker = createWorker({ 163 | logger: (m: OCRProgress) => { 164 | setOCRProgresses((ocrProgresses) => { 165 | if ( 166 | ocrProgresses.length && 167 | ocrProgresses[ocrProgresses.length - 1].status === m.status 168 | ) { 169 | return [...ocrProgresses.slice(0, ocrProgresses.length - 1), m]; 170 | } else { 171 | return [...ocrProgresses, m]; 172 | } 173 | }); 174 | }, 175 | }); 176 | 177 | (async () => { 178 | setIsProcessing(true); 179 | let languagesArr = selectedLanguages; 180 | if (languagesArr.length === 0) { 181 | languagesArr = ["eng"]; 182 | } 183 | 184 | await worker.load(); 185 | await worker.loadLanguage(languagesArr.join("+")); 186 | await worker.initialize(languagesArr.join("+")); 187 | const { 188 | data: { text }, 189 | } = await worker.recognize(input); 190 | props.replaceSelf("\n" + text); 191 | await worker.terminate(); 192 | setIsProcessing(false); 193 | })(); 194 | } 195 | 196 | function toggleLanguage(lang: string) { 197 | setSelectedLanguages((selectedLanguages) => { 198 | const offset = selectedLanguages.indexOf(lang); 199 | if (offset >= 0) { 200 | selectedLanguages.splice(offset, 1); 201 | selectedLanguages = [...selectedLanguages]; 202 | } else { 203 | selectedLanguages = [...selectedLanguages, lang]; 204 | } 205 | return selectedLanguages; 206 | }); 207 | } 208 | 209 | if (props.isPreview) { 210 | return ; 211 | } 212 | 213 | if (isProcessing) { 214 | return ( 215 | 216 | {t("general/Processing")} 217 | {/*{t("general/please-wait")}*/} 218 | 219 | {ocrProgresses.length > 0 && ( 220 | 221 | 222 | {t( 223 | "tesseract/" + ocrProgresses[ocrProgresses.length - 1].status 224 | )} 225 | 226 | 227 | {Math.floor( 228 | ocrProgresses[ocrProgresses.length - 1].progress * 100 229 | ).toString() + "%"} 230 | 231 | 232 | )} 233 | 234 | 235 | ); 236 | } 237 | 238 | if (imageDataURL) { 239 | return ( 240 | 241 | 242 | 243 | {t("widget/crossnote.ocr/recognize-text-in-languages")} 244 | 245 | 246 | = 0} 250 | onChange={() => toggleLanguage("eng")} 251 | value="eng" 252 | /> 253 | } 254 | label="English" 255 | /> 256 | = 0} 260 | onChange={() => toggleLanguage("chi_sim")} 261 | value="chi_sim" 262 | /> 263 | } 264 | label="简体中文" 265 | /> 266 | = 0} 270 | onChange={() => toggleLanguage("chi_tra")} 271 | value="chi_tra" 272 | /> 273 | } 274 | label="繁體中文" 275 | /> 276 | = 0} 280 | onChange={() => toggleLanguage("jpn")} 281 | value="jpn" 282 | /> 283 | } 284 | label="日本語" 285 | /> 286 | 287 | 288 | 289 | 290 | {t("widget/crossnote.ocr/extra-settings")} 291 | 292 | { 297 | setGrayscaleChecked(!grayscaleChecked); 298 | }} 299 | color={"primary"} 300 | > 301 | } 302 | label={t("widget/crossnote.ocr/grayscale")} 303 | > 304 | 305 | 306 | setCanvas(element)} 309 | > 310 | 311 | 312 | 320 | 327 | 328 | 329 | ); 330 | } 331 | 332 | return ( 333 | 334 | {t("widget/crossnote.ocr/ocr")} 335 | 336 | 337 | props.removeSelf()}> 338 | 339 | 340 | 341 | 342 | 343 | 344 | {t("general/Link")} 345 | 346 | { 351 | setLink(event.target.value); 352 | }} 353 | onKeyDown={(event) => { 354 | if (event.which === 13) { 355 | startOCRFromLink(); 356 | } 357 | }} 358 | fullWidth={true} 359 | > 360 | 361 | 365 | {t("widget/crossnote.auth/Or")} 366 | 367 | 368 | 369 | {t("widget/crossnote.ocr/local-image")} 370 | 371 | 378 | 379 | {isProcessing 380 | ? t("utils/uploading-image") 381 | : t("widget/crossnote.image/click-here-to-browse-image-file")} 382 | 383 | 384 | 385 | { 390 | setImageDropAreaElement(element); 391 | }} 392 | > 393 | 394 | ); 395 | } 396 | 397 | export const OCRWidgetCreator: WidgetCreator = (args) => { 398 | const el = document.createElement("span"); 399 | ReactDOM.render( 400 | 401 | 402 | , 403 | el 404 | ); 405 | return el; 406 | }; 407 | -------------------------------------------------------------------------------- /src/views/editor/widgets/timer/index.tsx: -------------------------------------------------------------------------------- 1 | // 0xGG Team 2 | // Distributed under AGPL3 3 | // 4 | // DESCRIPTION: This widget displays time related information 5 | import React from "react"; 6 | import ReactDOM from "react-dom"; 7 | import { WidgetArgs, WidgetCreator } from "vickymd/widget"; 8 | import { ErrorWidget } from "vickymd/widget/error/error"; 9 | import { useTheme, ThemeProvider } from "@material-ui/core"; 10 | import { selectedTheme } from "../../../themes/manager"; 11 | 12 | function Timer(props: WidgetArgs) { 13 | const attributes = props.attributes; 14 | const theme = useTheme(); 15 | 16 | return ( 17 |
36 |
37 | {"⌚ " + new Date(attributes["date"]).toLocaleString()} 38 |
39 |
40 | {attributes["duration"] ? "🎬 " + attributes["duration"] : ""} 41 |
42 |
43 | ); 44 | } 45 | 46 | export const TimerWidgetCreator: WidgetCreator = (args) => { 47 | const el = document.createElement("span"); 48 | if (!args.attributes["date"]) { 49 | return ErrorWidget({ 50 | ...args, 51 | ...{ 52 | attributes: { 53 | message: "Field 'date' is missing", 54 | }, 55 | }, 56 | }); 57 | } 58 | ReactDOM.render( 59 | 60 | 61 | , 62 | el 63 | ); 64 | return el; 65 | }; 66 | -------------------------------------------------------------------------------- /src/views/editor/widgets/video/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | Switch, 12 | FormControlLabel, 13 | } from "@material-ui/core"; 14 | import { 15 | createStyles, 16 | makeStyles, 17 | Theme, 18 | ThemeProvider, 19 | } from "@material-ui/core/styles"; 20 | import clsx from "clsx"; 21 | import { TrashCan } from "mdi-material-ui"; 22 | import { useTranslation } from "react-i18next"; 23 | import { selectedTheme } from "../../../themes/manager"; 24 | 25 | const useStyles = makeStyles((theme: Theme) => 26 | createStyles({ 27 | card: { 28 | padding: theme.spacing(2), 29 | position: "relative", 30 | }, 31 | actionButtons: { 32 | position: "absolute", 33 | top: "0", 34 | right: "0", 35 | }, 36 | section: { 37 | marginTop: theme.spacing(2), 38 | }, 39 | videoWrapper: { 40 | cursor: "default", 41 | position: "relative", 42 | width: "100%", 43 | height: "0", 44 | paddingTop: "56.25%", 45 | }, 46 | video: { 47 | position: "absolute", 48 | left: "0", 49 | top: "0", 50 | width: "100%", 51 | height: "100%", 52 | }, 53 | }) 54 | ); 55 | 56 | function VideoWidget(props: WidgetArgs) { 57 | const attributes = props.attributes; 58 | const classes = useStyles(props); 59 | const { t } = useTranslation(); 60 | const [source, setSource] = useState(attributes["source"] || ""); 61 | const [autoplay, setAutoplay] = useState( 62 | attributes["autoplay"] || false 63 | ); 64 | const [controls, setControls] = useState( 65 | attributes["controls"] || true 66 | ); 67 | const [loop, setLoop] = useState(attributes["loop"] || false); 68 | const [muted, setMuted] = useState(attributes["muted"] || false); 69 | const [poster, setPoster] = useState(attributes["poster"] || ""); 70 | 71 | if (attributes["src"]) { 72 | return ( 73 | 74 | 75 | 87 | 88 | 89 | ); 90 | } 91 | 92 | if (props.isPreview) { 93 | return ; 94 | } 95 | 96 | return ( 97 | 98 | {t("general/Video")} 99 | 100 | 101 | props.removeSelf()}> 102 | 103 | 104 | 105 | 106 | 107 | 108 | {t("general/source-url")} 109 | 110 | { 115 | setSource(event.target.value); 116 | }} 117 | onKeyDown={(event) => { 118 | if (event.which === 13) { 119 | if (source) { 120 | const attrs = { 121 | autoplay, 122 | controls, 123 | loop, 124 | muted, 125 | src: source, 126 | poster, 127 | }; 128 | props.setAttributes(attrs); 129 | } 130 | } 131 | }} 132 | fullWidth={true} 133 | > 134 | 135 | 136 | 137 | {t("widget/crossnote.video/poster-url")} 138 | 139 | { 144 | setPoster(event.target.value); 145 | }} 146 | fullWidth={true} 147 | > 148 | 149 | 150 | setAutoplay(!autoplay)} 156 | color={"primary"} 157 | > 158 | } 159 | > 160 | setControls(!controls)} 166 | color={"primary"} 167 | > 168 | } 169 | > 170 | setLoop(!loop)} 176 | color={"primary"} 177 | > 178 | } 179 | > 180 | setMuted(!muted)} 186 | color={"primary"} 187 | > 188 | } 189 | > 190 | 191 | 192 | ); 193 | } 194 | 195 | export const VideoWidgetCreator: WidgetCreator = (args) => { 196 | const el = document.createElement("span"); 197 | ReactDOM.render( 198 | 199 | 200 | , 201 | el 202 | ); 203 | return el; 204 | }; 205 | -------------------------------------------------------------------------------- /src/views/editor/widgets/youtube/index.tsx: -------------------------------------------------------------------------------- 1 | import { WidgetCreator, WidgetArgs } from "vickymd/widget"; 2 | import React, { useState } from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { 5 | Card, 6 | Typography, 7 | IconButton, 8 | Box, 9 | Input, 10 | Tooltip, 11 | } from "@material-ui/core"; 12 | import { 13 | createStyles, 14 | makeStyles, 15 | Theme, 16 | ThemeProvider, 17 | } from "@material-ui/core/styles"; 18 | import clsx from "clsx"; 19 | import { TrashCan } from "mdi-material-ui"; 20 | import { useTranslation } from "react-i18next"; 21 | import { selectedTheme } from "../../../themes/manager"; 22 | 23 | const useStyles = makeStyles((theme: Theme) => 24 | createStyles({ 25 | card: { 26 | padding: theme.spacing(2), 27 | position: "relative", 28 | }, 29 | actionButtons: { 30 | position: "absolute", 31 | top: "0", 32 | right: "0", 33 | }, 34 | section: { 35 | marginTop: theme.spacing(2), 36 | }, 37 | videoWrapper: { 38 | cursor: "default", 39 | position: "relative", 40 | width: "100%", 41 | height: "0", 42 | paddingTop: "56.25%", 43 | }, 44 | video: { 45 | backgroundColor: "#ddd", 46 | border: "none", 47 | position: "absolute", 48 | left: "0", 49 | top: "0", 50 | width: "100%", 51 | height: "100%", 52 | }, 53 | errorMessage: { 54 | color: "#f44336", 55 | marginTop: theme.spacing(2), 56 | }, 57 | }) 58 | ); 59 | 60 | function YoutubeWidget(props: WidgetArgs) { 61 | const attributes = props.attributes; 62 | const classes = useStyles(props); 63 | const { t } = useTranslation(); 64 | const [url, setURL] = useState(""); 65 | const [error, setError] = useState(""); 66 | 67 | if (attributes["videoID"]) { 68 | if (!props.isPreview) { 69 | return ( 70 | 71 | {"Youtube: { 75 | window.open( 76 | `https://www.youtube.com/watch?v=${attributes["videoID"]}`, 77 | "_blank" 78 | ); 79 | }} 80 | style={{ 81 | cursor: "pointer", 82 | width: "100%", 83 | }} 84 | > 85 | 86 | ); 87 | } else { 88 | return ( 89 | 90 | 91 | 99 | 100 | 101 | ); 102 | } 103 | } 104 | 105 | if (props.isPreview) { 106 | return ; 107 | } 108 | 109 | return ( 110 | 111 | {t("Youtube")} 112 | 113 | 114 | props.removeSelf()}> 115 | 116 | 117 | 118 | 119 | 120 | 121 | {t("Youtube video URL")} 122 | 123 | { 130 | setURL(event.target.value); 131 | setError(""); 132 | }} 133 | onKeyDown={(event) => { 134 | if (event.which === 13) { 135 | if (url && url.match(/\?v=(.+?)(&|$)/)) { 136 | const videoID = url.match(/\?v=(.+?)(&|$)/)[1]; 137 | const attrs = { 138 | videoID, 139 | }; 140 | props.setAttributes(attrs); 141 | } else if (url && url.match(/\/youtu\.be\/(.+?)(\?|$)/)) { 142 | const videoID = url.match(/\/youtu\.be\/(.+?)(\?|$)/)[1]; 143 | const attrs = { 144 | videoID, 145 | }; 146 | props.setAttributes(attrs); 147 | } else { 148 | setError(t("widget/crossnote.youtube/error_message")); 149 | } 150 | } 151 | }} 152 | fullWidth={true} 153 | > 154 | {error} 155 | 156 | 157 | ); 158 | } 159 | 160 | export const YoutubeWidgetCreator: WidgetCreator = (args) => { 161 | const el = document.createElement("span"); 162 | ReactDOM.render( 163 | 164 | 165 | , 166 | el 167 | ); 168 | return el; 169 | }; 170 | -------------------------------------------------------------------------------- /src/views/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18next from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import { zhCN, enUS, zhTW, ja } from "date-fns/locale"; 4 | import { enUS as enUSLanguage } from "./lang/enUS"; 5 | import { zhCN as zhCNLanguage } from "./lang/zhCN"; 6 | import { zhTW as zhTWLanguage } from "./lang/zhTW"; 7 | import { jaJP as jaJPLanguage } from "./lang/jaJP"; 8 | 9 | i18next.use(initReactI18next).init({ 10 | interpolation: { 11 | // React already does escaping 12 | escapeValue: false, 13 | }, 14 | keySeparator: false, // we do not use keys in form messages.welcome 15 | lng: "en-US", // "en-US" | "zh-CN" 16 | fallbackLng: "en-US", 17 | resources: { 18 | "en-US": enUSLanguage, 19 | "zh-CN": zhCNLanguage, 20 | "zh-TW": zhTWLanguage, 21 | "ja-JP": jaJPLanguage, 22 | }, 23 | }); 24 | 25 | export default i18next; 26 | 27 | export function languageCodeToLanguageName(code: string) { 28 | if (code === "zh-CN") { 29 | return "简体中文"; 30 | } else if (code === "zh-TW") { 31 | return "繁体中文"; 32 | } else if (code === "ja-JP") { 33 | return "日本語"; 34 | } else { 35 | return "English"; 36 | } 37 | } 38 | 39 | export function languageCodeToDateFNSLocale(code: string) { 40 | if (code === "zh-CN") { 41 | return zhCN; 42 | } else if (code === "en-US") { 43 | return enUS; 44 | } else if (code === "zh-HK") { 45 | return zhTW; 46 | } else if (code === "ja-JP") { 47 | return ja; 48 | } else { 49 | return enUS; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/views/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Noto Sans SC'; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-display: swap; 6 | src: local('Noto Sans SC Regular'), local('NotoSansSC-Regular'), url(https://fonts.gstatic.com/s/notosanssc/v11/k3kXo84MPvpLmixcA63oeALRLoKL.otf) format('opentype'); 7 | } 8 | html, 9 | body { 10 | width: 100%; 11 | height: 100%; 12 | overflow: hidden; 13 | background-color: #2196f1; 14 | } 15 | body { 16 | margin: 0; 17 | font-family: -apple-system, BlinkMacSystemFont, "Noto Sans SC", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | @media print { 22 | html, 23 | body { 24 | background-color: #fff; 25 | } 26 | } 27 | .CodeMirror-code.CodeMirror-code { 28 | font-family: -apple-system, BlinkMacSystemFont, "Noto Sans SC", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif !important; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-osx-font-smoothing: grayscale; 31 | } 32 | .CodeMirror-placeholder.CodeMirror-placeholder { 33 | /* font-size: 2.25rem; */ 34 | color: #ccc !important; 35 | } 36 | .HyperMD-header.HyperMD-header-1.HyperMD-header.HyperMD-header-1::after, 37 | .HyperMD-header.HyperMD-header-2.HyperMD-header.HyperMD-header-2::after { 38 | height: 0; 39 | } 40 | .CodeMirror-hints { 41 | font-size: 1rem !important; 42 | z-index: 2000 !important; 43 | } 44 | .hmd-image { 45 | cursor: pointer; 46 | } 47 | code { 48 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 49 | } 50 | #root { 51 | height: 100%; 52 | } 53 | ::-webkit-scrollbar { 54 | width: 8px; 55 | height: 8px; 56 | } 57 | ::-webkit-scrollbar-track { 58 | /* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); */ 59 | border-radius: 10px; 60 | background-color: transparent; 61 | } 62 | ::-webkit-scrollbar-thumb { 63 | border-radius: 5px; 64 | /* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);*/ 65 | background-color: rgba(150, 150, 150, 0.4); 66 | border: 4px solid rgba(150, 150, 150, 0.4); 67 | background-clip: content-box; 68 | } 69 | .float-win-hidden { 70 | display: none; 71 | } 72 | -------------------------------------------------------------------------------- /src/views/index.less: -------------------------------------------------------------------------------- 1 | // out: false 2 | 3 | html, 4 | body { 5 | width: 100%; 6 | height: 100%; 7 | overflow: hidden; 8 | // background-color: #2196f1; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | font-family: -apple-system, BlinkMacSystemFont, "Noto Sans SC", "Segoe UI", 16 | "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 17 | "Helvetica Neue", sans-serif; 18 | -webkit-font-smoothing: antialiased; 19 | -moz-osx-font-smoothing: grayscale; 20 | } 21 | 22 | #root { 23 | width: 100%; 24 | height: 100%; 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | @media print { 30 | 31 | html, 32 | body { 33 | background-color: #fff; 34 | } 35 | } 36 | 37 | .CodeMirror-code.CodeMirror-code { 38 | font-family: -apple-system, BlinkMacSystemFont, "Noto Sans SC", "Segoe UI", 39 | "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 40 | "Helvetica Neue", sans-serif !important; 41 | -webkit-font-smoothing: antialiased; 42 | -moz-osx-font-smoothing: grayscale; 43 | } 44 | 45 | .CodeMirror-placeholder.CodeMirror-placeholder { 46 | /* font-size: 2.25rem; */ 47 | color: #ccc !important; 48 | } 49 | 50 | .HyperMD-header.HyperMD-header-1.HyperMD-header.HyperMD-header-1::after, 51 | .HyperMD-header.HyperMD-header-2.HyperMD-header.HyperMD-header-2::after { 52 | height: 0; 53 | } 54 | 55 | .CodeMirror-hints { 56 | font-size: 1rem !important; 57 | z-index: 2000 !important; 58 | } 59 | 60 | .hmd-image { 61 | cursor: pointer; 62 | } 63 | 64 | code { 65 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 66 | monospace; 67 | } 68 | 69 | #root { 70 | height: 100%; 71 | } 72 | 73 | ::-webkit-scrollbar { 74 | width: 8px; 75 | height: 8px; 76 | } 77 | 78 | ::-webkit-scrollbar-track { 79 | /* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); */ 80 | border-radius: 10px; 81 | background-color: transparent; 82 | } 83 | 84 | ::-webkit-scrollbar-thumb { 85 | border-radius: 5px; 86 | /* -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5);*/ 87 | background-color: rgba(150, 150, 150, 0.4); 88 | border: 4px solid rgba(150, 150, 150, 0.4); 89 | background-clip: content-box; 90 | } 91 | 92 | .float-win-hidden { 93 | display: none; 94 | } -------------------------------------------------------------------------------- /src/views/themes/dark.ts: -------------------------------------------------------------------------------- 1 | import { CrossnoteTheme } from "./theme"; 2 | import { lighten } from "@material-ui/core"; 3 | 4 | export const DarkTheme: CrossnoteTheme = new CrossnoteTheme({ 5 | name: "dark", 6 | muiThemeOptions: { 7 | palette: { 8 | common: { black: "#000", white: "#fff" }, 9 | background: { 10 | paper: lighten("#1e1e1e", 0.05), 11 | default: "#1e1e1e", 12 | }, 13 | primary: { 14 | light: "#7986cb", 15 | main: "rgba(144, 19, 254, 1)", 16 | dark: "#303f9f", 17 | contrastText: "#fff", 18 | }, 19 | secondary: { 20 | light: "#ff4081", 21 | main: "#f50057", 22 | dark: "#c51162", 23 | contrastText: "#fff", 24 | }, 25 | error: { 26 | light: "#e57373", 27 | main: "#f44336", 28 | dark: "#d32f2f", 29 | contrastText: "rgba(197, 197, 197, 1)", 30 | }, 31 | divider: "#222", 32 | text: { 33 | primary: "#d4d4d4", 34 | secondary: "rgba(180, 180, 180, 1)", 35 | disabled: "rgba(121, 7, 7, 0.38)", 36 | hint: "rgba(0, 0, 0, 0.38)", 37 | }, 38 | action: { 39 | active: "rgba(180, 180, 180, 1)", 40 | disabled: "#353535", 41 | }, 42 | }, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /src/views/themes/light.ts: -------------------------------------------------------------------------------- 1 | import { CrossnoteTheme } from "./theme"; 2 | import { blue, orange } from "@material-ui/core/colors"; 3 | 4 | export const LightTheme: CrossnoteTheme = new CrossnoteTheme({ 5 | name: "light", 6 | muiThemeOptions: { 7 | palette: { 8 | primary: blue, 9 | secondary: orange, 10 | }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/views/themes/manager.ts: -------------------------------------------------------------------------------- 1 | import { CrossnoteTheme } from "./theme"; 2 | import { LightTheme } from "./light"; 3 | import { DarkTheme } from "./dark"; 4 | import { crossnoteSettings } from "../util/util"; 5 | import { SolarizedLight } from "./solarized-light"; 6 | import { OneDarkTheme } from "./one-dark"; 7 | 8 | export class ThemeManager { 9 | public themes: CrossnoteTheme[]; 10 | public selectedTheme: CrossnoteTheme; 11 | constructor() { 12 | this.themes = []; 13 | this.selectedTheme = null; 14 | } 15 | public addTheme(theme: CrossnoteTheme) { 16 | this.themes.push(theme); 17 | if (!this.selectedTheme) { 18 | this.selectTheme(theme.name); 19 | } 20 | } 21 | 22 | public getTheme(name: string) { 23 | return this.themes.find((theme) => theme.name === name); 24 | } 25 | 26 | public selectTheme(name: string) { 27 | if (!name) { 28 | return; 29 | } 30 | const theme = this.themes.find((t) => t.name === name); 31 | if (!theme) { 32 | return; 33 | } 34 | this.selectedTheme = theme; 35 | } 36 | } 37 | 38 | const _themeManager = new ThemeManager(); 39 | _themeManager.addTheme(LightTheme); 40 | _themeManager.addTheme(DarkTheme); 41 | _themeManager.addTheme(SolarizedLight); 42 | _themeManager.addTheme(OneDarkTheme); 43 | 44 | export const themeManager = _themeManager; 45 | 46 | export const selectedTheme = themeManager.getTheme(crossnoteSettings.theme); 47 | -------------------------------------------------------------------------------- /src/views/themes/one-dark.ts: -------------------------------------------------------------------------------- 1 | import { CrossnoteTheme } from "./theme"; 2 | import { lighten } from "@material-ui/core"; 3 | import { blueGrey, cyan } from "@material-ui/core/colors"; 4 | 5 | export const OneDarkTheme: CrossnoteTheme = new CrossnoteTheme({ 6 | name: "one-dark", 7 | muiThemeOptions: { 8 | palette: { 9 | common: { black: "#000", white: "#fff" }, 10 | background: { 11 | paper: lighten("#282c34", 0.05), 12 | default: "#282c34", 13 | }, 14 | primary: cyan, 15 | secondary: blueGrey, 16 | error: { 17 | light: "#e57373", 18 | main: "#f44336", 19 | dark: "#d32f2f", 20 | contrastText: "rgba(197, 197, 197, 1)", 21 | }, 22 | divider: "#222", 23 | text: { 24 | primary: "#ccc", 25 | secondary: "rgba(180, 180, 180, 1)", 26 | disabled: "rgba(121, 7, 7, 0.38)", 27 | hint: "rgba(0, 0, 0, 0.38)", 28 | }, 29 | action: { 30 | active: "rgba(180, 180, 180, 1)", 31 | disabled: "#353535", 32 | }, 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/views/themes/solarized-light.ts: -------------------------------------------------------------------------------- 1 | import { CrossnoteTheme } from "./theme"; 2 | import { orange, amber } from "@material-ui/core/colors"; 3 | import { lighten } from "@material-ui/core"; 4 | 5 | export const SolarizedLight: CrossnoteTheme = new CrossnoteTheme({ 6 | name: "solarized-light", 7 | muiThemeOptions: { 8 | palette: { 9 | primary: orange, 10 | secondary: amber, 11 | background: { 12 | paper: lighten("#fdf6e3", 0.05), 13 | default: "#fdf6e3", 14 | }, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/views/themes/theme.ts: -------------------------------------------------------------------------------- 1 | import { ThemeOptions, Theme, createMuiTheme } from "@material-ui/core"; 2 | import { ThemeName } from "vickymd/theme"; 3 | 4 | interface CrossnoteThemeProps { 5 | name: ThemeName; 6 | muiThemeOptions: ThemeOptions; 7 | } 8 | 9 | export class CrossnoteTheme { 10 | public name: ThemeName; 11 | public muiTheme: Theme; 12 | constructor({ name, muiThemeOptions }: CrossnoteThemeProps) { 13 | this.name = name; 14 | this.muiTheme = createMuiTheme(muiThemeOptions); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/views/util/markdown.ts: -------------------------------------------------------------------------------- 1 | export const TagStopRegExp = /[@#,.!$%^&*()[\]-_+=~`<>?\\,。]/g; 2 | export function getTags(markdown: string): string[] { 3 | const tags = new Set( 4 | markdown.match( 5 | /(#([^#]+?)#[\s@#,.!$%^&*()[\]-_+=~`<>?\\,。])|(#[^\s@#,.!$%^&*()[\]-_+=~`<>?\\,。]+)/g 6 | ) || [] 7 | ); 8 | return Array.from(tags).map( 9 | (tag) => tag.replace(TagStopRegExp, "").trim() // Don't remove \s here 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/views/util/preview.ts: -------------------------------------------------------------------------------- 1 | import { Note } from "../../lib/note"; 2 | import { SelectedSection, CrossnoteSectionType } from "../../lib/section"; 3 | import { Message, MessageAction } from "../../lib/message"; 4 | import { vscode, resolveNoteImageSrc, setSelectedSection } from "../util/util"; 5 | 6 | export function openURL(url: string, note: Note) { 7 | if (!note) { 8 | return; 9 | } 10 | const message: Message = { 11 | action: MessageAction.OpenURL, 12 | data: { 13 | note, 14 | url: decodeURIComponent(url), 15 | }, 16 | }; 17 | vscode.postMessage(message); 18 | } 19 | 20 | export function postprocessPreview( 21 | previewElement: HTMLElement, 22 | note: Note, 23 | isPresentationCallback?: (isPresentation: boolean) => void 24 | ) { 25 | if (!previewElement) { 26 | return; 27 | } 28 | const handleLinksClickEvent = (preview: HTMLElement) => { 29 | // Handle link click event 30 | const links = preview.getElementsByTagName("A"); 31 | for (let i = 0; i < links.length; i++) { 32 | const link = links[i] as HTMLAnchorElement; 33 | link.onclick = (event) => { 34 | event.preventDefault(); 35 | if (link.hasAttribute("data-topic")) { 36 | const tag = link.getAttribute("data-topic"); 37 | if (tag.length) { 38 | setSelectedSection({ 39 | type: CrossnoteSectionType.Tag, 40 | path: tag, 41 | notebook: { 42 | dir: note.notebookPath, 43 | name: "", 44 | }, 45 | }); 46 | } 47 | } else { 48 | openURL(link.getAttribute("href"), note); 49 | } 50 | }; 51 | } 52 | }; 53 | const resolveImages = async (preview: HTMLElement) => { 54 | const images = preview.getElementsByTagName("IMG"); 55 | for (let i = 0; i < images.length; i++) { 56 | const image = images[i] as HTMLImageElement; 57 | const imageSrc = image.getAttribute("src"); 58 | image.setAttribute( 59 | "src", 60 | resolveNoteImageSrc(note, decodeURIComponent(imageSrc)) 61 | ); 62 | } 63 | }; 64 | 65 | if ( 66 | previewElement.childElementCount && 67 | previewElement.children[0].tagName.toUpperCase() === "IFRAME" 68 | ) { 69 | // presentation 70 | previewElement.style.maxWidth = "100%"; 71 | previewElement.style.height = "100%"; 72 | previewElement.style.overflow = "hidden !important"; 73 | handleLinksClickEvent( 74 | (previewElement.children[0] as HTMLIFrameElement).contentDocument 75 | .body as HTMLElement 76 | ); 77 | resolveImages( 78 | (previewElement.children[0] as HTMLIFrameElement).contentDocument 79 | .body as HTMLElement 80 | ); 81 | if (isPresentationCallback) { 82 | isPresentationCallback(true); 83 | } 84 | } else { 85 | // normal 86 | // previewElement.style.maxWidth = `${EditorPreviewMaxWidth}px`; 87 | previewElement.style.height = "100%"; 88 | previewElement.style.overflow = "hidden !important"; 89 | handleLinksClickEvent(previewElement); 90 | resolveImages(previewElement); 91 | if (isPresentationCallback) { 92 | isPresentationCallback(false); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/views/util/util.ts: -------------------------------------------------------------------------------- 1 | import { Message, MessageAction } from "../../lib/message"; 2 | import { Note } from "../../lib/note"; 3 | import { CrossnoteSettings } from "../../lib/settings"; 4 | import * as path from "path"; 5 | import slash from "slash"; 6 | import { SelectedSection } from "../../lib/section"; 7 | 8 | interface VSCodeWebviewAPI { 9 | postMessage: (message: Message) => void; 10 | } 11 | 12 | // @ts-ignore 13 | export const vscode: VSCodeWebviewAPI = acquireVsCodeApi(); 14 | 15 | export function resolveNoteImageSrc(note: Note, imageSrc: string) { 16 | if (!note) { 17 | return imageSrc; 18 | } 19 | if (imageSrc.startsWith("https://") || imageSrc.startsWith("data:")) { 20 | return imageSrc; 21 | } else if (imageSrc.startsWith("http://")) { 22 | return ""; 23 | } else if (imageSrc.startsWith("/")) { 24 | return `vscode-resource://file//${slash( 25 | path.resolve(note.notebookPath, "." + imageSrc) 26 | )}`; 27 | } else { 28 | return `vscode-resource://file//${slash( 29 | path.join(note.notebookPath, path.dirname(note.filePath), imageSrc) 30 | )}`; 31 | } 32 | } 33 | 34 | export function setSelectedSection(selectedSection: SelectedSection) { 35 | const message: Message = { 36 | action: MessageAction.SetSelectedSection, 37 | data: selectedSection, 38 | }; 39 | vscode.postMessage(message); 40 | } 41 | 42 | export const crossnoteSettings: CrossnoteSettings = window[ 43 | "crossnoteSettings" 44 | ] as CrossnoteSettings; 45 | 46 | export const extensionPath: string = window["extensionPath"] as string; 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | // Target latest version of ECMAScript. 5 | "target": "es6", 6 | // path to output directory 7 | "outDir": "./out/", 8 | // enable strict null checks as a best practice 9 | "strictNullChecks": true, 10 | // Search under node_modules for non-relative imports. 11 | "moduleResolution": "node", 12 | // Process & infer types from .js files. 13 | "allowJs": true, 14 | // Don't emit; allow Babel to transform files. 15 | // "noEmit": true, 16 | // Enable strictest settings like strictNullChecks & noImplicitAny. 17 | "strict": true, 18 | // Import non-ES modules as default imports. 19 | "esModuleInterop": true, 20 | // use typescript to transpile jsx to js 21 | "jsx": "react", 22 | // "baseUrl": "./src", 23 | "sourceMap": true, 24 | "lib": [ 25 | "es2015", 26 | "dom.iterable", 27 | "es2016.array.include", 28 | "es2017.object", 29 | "dom" 30 | ], 31 | "removeComments": true, 32 | "alwaysStrict": true, 33 | "allowUnreachableCode": false, 34 | "noImplicitAny": true, 35 | "noImplicitThis": true, 36 | "noUnusedLocals": true, 37 | "noUnusedParameters": true, 38 | "noImplicitReturns": true, 39 | "noFallthroughCasesInSwitch": true, 40 | "forceConsistentCasingInFileNames": true, 41 | "importHelpers": true, 42 | "skipLibCheck": true, // HACK: Seems like @types/react causes issue because of conflicts with @types/react-dom 43 | }, 44 | "include": ["./src/**/*"], 45 | "exclude": ["node_modules", ".vscode-test", "webpack.config.js", "./src/views"] 46 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [true, "always"], 9 | "triple-equals": true 10 | }, 11 | "defaultSeverity": "warning" 12 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | entry: { 5 | EditorPanelWebview: path.resolve( 6 | __dirname, 7 | "./src/views/EditorPanelWebview.tsx" 8 | ), 9 | NotesPanelWebview: path.resolve( 10 | __dirname, 11 | "./src/views/NotesPanelWebview.tsx" 12 | ), 13 | }, 14 | output: { 15 | path: path.resolve(__dirname, "./out/views"), 16 | filename: "[name].bundle.js", 17 | }, 18 | resolve: { 19 | extensions: [".ts", ".tsx", ".js"], 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(ts|js)x?$/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: "babel-loader", 28 | }, 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: [ 33 | { 34 | loader: "css-to-string-loader", 35 | }, 36 | { 37 | loader: "style-loader", // Creates style nodes from JS strings 38 | }, 39 | { 40 | loader: "css-loader", // Translates CSS into CommonJS 41 | }, 42 | ], 43 | }, 44 | { 45 | test: /\.less$/, 46 | use: [ 47 | { 48 | loader: "style-loader", 49 | }, 50 | { 51 | loader: "css-loader", 52 | }, 53 | { 54 | loader: "less-loader", 55 | options: { 56 | lessOptions: { 57 | strictMath: true, 58 | noIeCompat: true, 59 | }, 60 | }, 61 | }, 62 | ], 63 | }, 64 | { 65 | test: /\.(png|woff|woff2|eot|ttf|svg|gif)$/, 66 | loader: "url-loader?limit=1000000", 67 | }, 68 | ], 69 | }, 70 | node: { 71 | fs: "empty", 72 | }, 73 | }; 74 | --------------------------------------------------------------------------------