├── .editorconfig ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .vscode ├── .debug.script.mjs ├── extensions.json ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── electron-builder.json5 ├── electron ├── electron-env.d.ts ├── main │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── global │ │ ├── global.module.ts │ │ └── win.module.ts │ ├── index.ts │ └── transport │ │ ├── decorators.ts │ │ ├── dispatcher.ts │ │ ├── filter.ts │ │ └── index.ts ├── preload │ └── index.ts └── resources │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── iconset │ └── 256x256.png │ ├── installerIcon.ico │ └── uninstallerIcon.ico ├── index.html ├── package.json ├── public ├── Muli.ttf ├── favicon.svg ├── search-index.js └── twitter-emoji-32.png ├── src ├── App.tsx ├── Initializer.tsx ├── assets │ ├── categories.json │ ├── electron.png │ ├── emoji.json │ ├── error.png │ ├── favicon.svg │ ├── jieba_rs_wasm.js │ ├── jieba_rs_wasm_bg.wasm │ ├── locales │ │ ├── en-US │ │ │ └── translation.json │ │ └── zh-CN │ │ │ └── translation.json │ ├── pdf.worker.min.js │ └── twitter-emoji-32.png ├── common.less ├── common │ ├── configs │ │ └── promiseErrorHandler.ts │ └── types │ │ ├── index.ts │ │ └── noop.ts ├── components │ ├── base │ │ ├── AnimatePopover.tsx │ │ ├── Content.tsx │ │ ├── FullSizeLink.tsx │ │ ├── Icon.tsx │ │ ├── IconWithPopover.tsx │ │ ├── Image.tsx │ │ ├── Panel.tsx │ │ ├── ReadMore.tsx │ │ ├── Spinner.tsx │ │ ├── Switch.tsx │ │ ├── Tooltip.tsx │ │ ├── WithBorder.tsx │ │ ├── animation │ │ │ └── SwitchAnimation.tsx │ │ ├── authenticatedLayout │ │ │ └── index.tsx │ │ ├── breadcrumb │ │ │ ├── Breadcrumb.tsx │ │ │ ├── BreadcrumbItem.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── container │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── dock │ │ │ ├── Algorithm.ts │ │ │ ├── Divider.tsx │ │ │ ├── DockBox.tsx │ │ │ ├── DockData.ts │ │ │ ├── DockDropEdge.tsx │ │ │ ├── DockDropLayer.tsx │ │ │ ├── DockLayout.tsx │ │ │ ├── DockPanel.tsx │ │ │ ├── DockTabBar.tsx │ │ │ ├── DockTabPane.tsx │ │ │ ├── DockTabs.tsx │ │ │ ├── LICENSE │ │ │ ├── Serializer.ts │ │ │ ├── bar.less │ │ │ ├── dragdrop │ │ │ │ ├── DragDropDiv.tsx │ │ │ │ ├── DragManager.ts │ │ │ │ └── GestureManager.ts │ │ │ ├── index.module.less │ │ │ ├── index.ts │ │ │ └── util │ │ │ │ └── Compare.ts │ │ ├── errorFallback │ │ │ └── index.tsx │ │ ├── footer │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── form │ │ │ ├── FormItem.tsx │ │ │ ├── index.module.less │ │ │ ├── index.tsx │ │ │ ├── useCanSubmit.ts │ │ │ └── useIsSubmitted.ts │ │ ├── heading │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── layout │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── list │ │ │ └── index.tsx │ │ ├── menu │ │ │ ├── MenuGroup.tsx │ │ │ ├── MenuItem.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── modal │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── navLink │ │ │ └── index.tsx │ │ ├── select │ │ │ ├── Select.tsx │ │ │ ├── SelectOption.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── sidebar │ │ │ ├── Item.tsx │ │ │ ├── Nav.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── tabs │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── timeline │ │ │ ├── TimelineItem.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ └── toolbar │ │ │ ├── Toolbar.tsx │ │ │ ├── ToolbarItem.tsx │ │ │ ├── ToolbarSeparator.tsx │ │ │ ├── index.module.less │ │ │ └── index.tsx │ ├── collection │ │ ├── CollectionItem.tsx │ │ ├── Details.tsx │ │ ├── DocumentItem.tsx │ │ ├── NoteItem.tsx │ │ ├── index.module.less │ │ ├── index.tsx │ │ ├── useCurrentSelect.ts │ │ └── utils.ts │ ├── contextMenu │ │ ├── ContextMenuProvider.tsx │ │ └── index.tsx │ ├── custom │ │ ├── iconButton │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── icons │ │ │ ├── CustomIcon.tsx │ │ │ ├── MoveBlock.tsx │ │ │ └── map.ts │ │ ├── input │ │ │ ├── autosizeTextarea │ │ │ │ ├── calculateNodeHeight.ts │ │ │ │ ├── forceHiddenStyles.ts │ │ │ │ ├── getSizingData.ts │ │ │ │ ├── hooks.ts │ │ │ │ └── index.tsx │ │ │ ├── pretty │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── standard │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── withPrefix │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ └── rippleButton │ │ │ ├── index.module.less │ │ │ └── index.tsx │ ├── dialogs │ │ ├── ChooseCollectionDialog.tsx │ │ ├── ChooseNoteDialog.tsx │ │ ├── DeleteDocumentDialog.tsx │ │ ├── NewDocumentDialog.tsx │ │ ├── NewItemDialog.tsx │ │ ├── SaveNoteDialog.tsx │ │ ├── SearchDialog │ │ │ ├── AutoComplete.tsx │ │ │ ├── Result.tsx │ │ │ └── SearchDialog.tsx │ │ ├── SettingDialog │ │ │ └── index.tsx │ │ ├── switchAnimation.ts │ │ └── type.ts │ ├── documentDetail │ │ ├── index.tsx │ │ ├── overview │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ └── tsest.ts │ ├── dragReposition │ │ ├── DragRepositionItem.tsx │ │ ├── DragRepositionWrapper.tsx │ │ ├── RepositionTest.tsx │ │ ├── calculateItemCollide.ts │ │ ├── index.module.less │ │ ├── types.ts │ │ └── utils │ │ │ ├── algorithm.ts │ │ │ ├── element.ts │ │ │ ├── position.ts │ │ │ └── scroll.ts │ ├── dragSelector │ │ ├── components │ │ │ └── SelectionContainer │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── useSelectionContainer.tsx │ │ │ └── useSelectionLogic.ts │ │ ├── index.ts │ │ ├── typings.d.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ └── boxes.test.ts │ │ │ ├── boxes.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ ├── draggableBox │ │ ├── index.module.less │ │ └── index.tsx │ ├── editor │ │ ├── Divider.tsx │ │ ├── Editor.tsx │ │ ├── EditorManager.tsx │ │ ├── InnerEditor.tsx │ │ ├── MindmapLayer.tsx │ │ ├── components │ │ │ ├── dragHandle │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── index.ts │ │ │ ├── menu │ │ │ │ ├── DragHandleMenu.tsx │ │ │ │ ├── MindMapMenu.tsx │ │ │ │ └── SlashMenu.tsx │ │ │ ├── mindmap │ │ │ │ ├── InsertNodeForm.tsx │ │ │ │ ├── Toolbar.tsx │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── outline │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── overlay │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── toolbar │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── consts │ │ │ ├── hotkeys.ts │ │ │ └── shortcuts.ts │ │ ├── customTypes.ts │ │ ├── dock.less │ │ ├── elements │ │ │ ├── Emoji.tsx │ │ │ ├── Spacer.tsx │ │ │ ├── block │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── elementBuilder.ts │ │ │ ├── heading │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── highlight │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── index.module.less │ │ │ ├── index.ts │ │ │ ├── paragraph │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── subPage │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── thought │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── useEditor.ts │ │ │ ├── useEditorValue.ts │ │ │ ├── useEditorVisible.ts │ │ │ ├── useIsDragging.ts │ │ │ └── useSelectedEditorRef.ts │ │ ├── index.module.less │ │ ├── plugins │ │ │ ├── withCustomComponent.ts │ │ │ ├── withEmitter.ts │ │ │ ├── withHtml.ts │ │ │ ├── withShortcuts.ts │ │ │ └── withVoid.ts │ │ ├── types │ │ │ ├── baseElementTypes.ts │ │ │ ├── baseTypes.ts │ │ │ ├── commonElementTypes.ts │ │ │ ├── index.ts │ │ │ └── outerElementTypes.ts │ │ └── utils │ │ │ ├── database │ │ │ ├── deserialize.ts │ │ │ └── serialize.ts │ │ │ ├── outline │ │ │ └── outliner.ts │ │ │ └── positions │ │ │ ├── block.ts │ │ │ ├── caret.ts │ │ │ ├── index.ts │ │ │ ├── scroll.ts │ │ │ └── spacer.ts │ ├── form │ │ ├── NewCollectionForm.tsx │ │ └── newDocumentForm │ │ │ └── index.tsx │ ├── header │ │ └── scrollable │ │ │ ├── index.module.less │ │ │ └── index.tsx │ ├── index.ts │ ├── menus │ │ ├── CollectionMenu.tsx │ │ ├── EditorManagerMenu.tsx │ │ ├── NewDocumentMenu.tsx │ │ ├── PdfContextMenu.tsx │ │ └── PdfViewerToolMenu.tsx │ ├── pdf │ │ ├── PdfViewer.tsx │ │ ├── components │ │ │ ├── abstract │ │ │ │ └── index.tsx │ │ │ ├── annotation │ │ │ │ ├── Link.tsx │ │ │ │ └── index.module.less │ │ │ ├── label │ │ │ │ ├── HighlightLabel.tsx │ │ │ │ ├── ThoughtLabel.tsx │ │ │ │ └── index.module.less │ │ │ ├── page │ │ │ │ ├── Page.tsx │ │ │ │ ├── index.module.less │ │ │ │ └── mouseSelection.tsx │ │ │ ├── sidebar │ │ │ │ ├── NoteInfo.tsx │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── statusbar │ │ │ │ └── index.tsx │ │ │ └── toolbar │ │ │ │ ├── LabelToolbar.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── TextSelectionToolbar.tsx │ │ │ │ ├── Translate.tsx │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── useCleanMode.ts │ │ │ ├── useCollapsed.ts │ │ │ ├── useContextMenu.ts │ │ │ ├── useCurrentTextSelection.ts │ │ │ ├── usePdfInfo.ts │ │ │ ├── usePdfNote.ts │ │ │ └── useWidth.ts │ │ └── index.module.less │ ├── picker │ │ ├── colorPicker │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ ├── emojiPicker │ │ │ ├── index.module.less │ │ │ └── index.tsx │ │ └── iconPicker │ │ │ ├── index.module.less │ │ │ └── index.tsx │ ├── titlebar │ │ ├── index.module.less │ │ └── index.tsx │ └── treeCollection │ │ ├── Item.tsx │ │ ├── draggingState.ts │ │ ├── index.module.less │ │ └── index.tsx ├── config │ └── axiosClient.ts ├── consts │ └── pagination.ts ├── events │ ├── editorEvent.ts │ ├── eventEmitter.ts │ ├── eventHandler.ts │ ├── pdfEvent.ts │ └── useEventEmitter.ts ├── global.d.ts ├── global.less ├── hooks │ ├── components │ │ ├── useCurrentCollection.ts │ │ ├── useCurrentDocument.ts │ │ ├── useCurrentViewingPdf.ts │ │ ├── useEditor.ts │ │ ├── useEditorManagerVisible.ts │ │ ├── useSidebarVisible.ts │ │ └── useSidebarWidth.ts │ ├── index.ts │ ├── stores │ │ └── useDb.ts │ ├── styles │ │ ├── index.ts │ │ └── useTheme.ts │ └── utils │ │ ├── index.ts │ │ ├── useCallbackRef.ts │ │ ├── useClickOutside.ts │ │ ├── useDebounce.ts │ │ ├── useDebounceEffect.ts │ │ ├── useDebounceFn.ts │ │ ├── useEventListener.ts │ │ ├── useLatest.ts │ │ ├── useMeasure.ts │ │ ├── useMemoizedFn.ts │ │ ├── useMergedRef.ts │ │ ├── useMount.ts │ │ ├── useMousePositionRef.ts │ │ ├── usePdfjs.ts │ │ ├── usePrevious.ts │ │ ├── useRafFn.ts │ │ ├── useRafState.ts │ │ ├── useRefCallback.ts │ │ ├── useRefDepsEffect.ts │ │ ├── useRefEffect.ts │ │ ├── useThrottleFn.ts │ │ ├── useTime.ts │ │ ├── useToggle.ts │ │ ├── useTransition.tsx │ │ ├── useUnmount.ts │ │ ├── useUpdateEffect.ts │ │ ├── useUpdateIsomorphicLayoutEffect.ts │ │ └── useWhyUpdate.ts ├── i18n │ └── index.ts ├── jotai │ └── jotaiScope.ts ├── main.tsx ├── pdfViewer │ └── core │ │ ├── Loader.tsx │ │ ├── Provider.tsx │ │ ├── Viewer.tsx │ │ ├── annotation │ │ └── Annotation.tsx │ │ ├── hooks │ │ └── useScrollMode.ts │ │ ├── layers │ │ ├── AnnotationLayer.tsx │ │ ├── CanvasLayer.tsx │ │ ├── PageLayer.tsx │ │ ├── TextLayer.tsx │ │ └── index.module.less │ │ ├── layouts │ │ ├── Layout.tsx │ │ └── LayoutWrapper.tsx │ │ ├── types │ │ ├── annotation.ts │ │ ├── index.ts │ │ ├── layout.ts │ │ ├── page.ts │ │ ├── pdfJsApi.ts │ │ ├── point.ts │ │ └── text.ts │ │ └── utils │ │ ├── canvas.ts │ │ ├── pages.ts │ │ ├── pan.tsx │ │ ├── text.ts │ │ └── zoom.ts ├── plugins │ ├── index.ts │ └── ipc.ts ├── routes │ ├── TransitionRoute.tsx │ ├── globalLocation.ts │ └── index.tsx ├── scenes │ ├── collections │ │ └── index.tsx │ ├── editor │ │ └── index.tsx │ ├── home │ │ ├── Card.tsx │ │ ├── Header.tsx │ │ ├── animation.ts │ │ ├── index.module.less │ │ └── index.tsx │ └── pdf │ │ ├── Header.tsx │ │ ├── index.module.less │ │ └── index.tsx ├── stores │ ├── base.store.ts │ ├── block.store.ts │ ├── collection.store.ts │ ├── document.store.ts │ ├── editor.service.ts │ ├── label.service.ts │ ├── label.store.ts │ ├── note.store.ts │ ├── task.store.ts │ └── word.service.ts ├── styles │ ├── border.less │ ├── common.module.less │ ├── default.less │ ├── font.less │ ├── icon.less │ ├── scrollbar.less │ └── tooltip.less ├── types.ts ├── utils │ ├── array.ts │ ├── color.ts │ ├── db.ts │ ├── emoji.ts │ ├── event.ts │ ├── id.ts │ ├── img.ts │ ├── localstorage.ts │ ├── request.ts │ ├── searchIndex.ts │ ├── snowflakeIdv1.ts │ ├── snowflakeIdv1Option.ts │ └── window.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── typings └── data │ ├── base.d.ts │ ├── collection.d.ts │ ├── detail-index.d.ts │ ├── document.d.ts │ ├── editor.d.ts │ ├── foo.d.ts │ ├── index.d.ts │ ├── label.d.ts │ ├── note.d.ts │ ├── page-index.d.ts │ ├── pdf.d.ts │ ├── result.d.ts │ ├── task.d.ts │ ├── user.d.ts │ └── word.d.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | { 3 | "root": true, 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "sourceType": "module", 7 | "extraFileExtensions": [".json"], 8 | "ecmaFeatures": { 9 | "jsx": true 10 | } 11 | }, 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:prettier/recommended" 16 | ], 17 | "plugins": [ 18 | "@typescript-eslint", 19 | "eslint-plugin-react", 20 | "@emotion" 21 | ], 22 | "rules": { 23 | "semi": ["error","never"], 24 | "prettier/prettier": [ 25 | "error", 26 | { 27 | "printWidth": 140, 28 | "endOfLine": "auto", 29 | "semi": false, 30 | "singleQuote": true, 31 | "trailingComma": "none", 32 | "arrowParens": "avoid" 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | release 25 | .vscode/.debug.env 26 | package-lock.json 27 | pnpm-lock.yaml 28 | yarn.lock 29 | dist-electron 30 | files 31 | imgs -------------------------------------------------------------------------------- /.vscode/.debug.script.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { createRequire } from 'module' 5 | import { spawn } from 'child_process' 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 8 | const require = createRequire(import.meta.url) 9 | const pkg = require('../package.json') 10 | 11 | // write .debug.env 12 | const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`) 13 | fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n')) 14 | 15 | // bootstrap 16 | spawn( 17 | // TODO: terminate `npm run dev` when Debug exits. 18 | process.platform === 'win32' ? 'npm.cmd' : 'npm', 19 | ['run', 'dev'], 20 | { 21 | stdio: 'inherit', 22 | env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), 23 | }, 24 | ) 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "editorconfig.editorconfig", 6 | "mrmlnc.vscode-json5", 7 | "rbbit.typescript-hero", 8 | "syler.sass-indented", 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 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 | "version": "0.2.0", 6 | "compounds": [ 7 | { 8 | "name": "Debug App", 9 | "preLaunchTask": "start .debug.script.mjs", 10 | "configurations": [ 11 | "Debug Main Process", 12 | "Debug Renderer Process" 13 | ], 14 | "presentation": { 15 | "hidden": false, 16 | "group": "", 17 | "order": 1 18 | }, 19 | "stopAll": true 20 | } 21 | ], 22 | "configurations": [ 23 | { 24 | "name": "Debug Main Process", 25 | "type": "pwa-node", 26 | "request": "launch", 27 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 28 | "windows": { 29 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 30 | }, 31 | "runtimeArgs": [ 32 | "--no-sandbox", 33 | "--remote-debugging-port=9229", 34 | "." 35 | ], 36 | "envFile": "${workspaceFolder}/.vscode/.debug.env", 37 | "console": "integratedTerminal" 38 | }, 39 | { 40 | "name": "Debug Renderer Process", 41 | "port": 9229, 42 | "request": "attach", 43 | "type": "pwa-chrome", 44 | "timeout": 60000 45 | }, 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "start .debug.script.mjs", 8 | "type": "shell", 9 | "command": "node .vscode/.debug.script.mjs", 10 | "isBackground": true, 11 | "problemMatcher": { 12 | "owner": "typescript", 13 | "fileLocation": "relative", 14 | "pattern": { 15 | // TODO: correct "regexp" 16 | "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", 17 | "file": 1, 18 | "line": 3, 19 | "column": 4, 20 | "code": 5, 21 | "message": 6 22 | }, 23 | "background": { 24 | "activeOnStart": true, 25 | "endsPattern": "^.*[startup] Electron App.*$", 26 | } 27 | } 28 | } 29 | ] 30 | } 31 | 32 | // https://code.visualstudio.com/docs/editor/tasks#_operating-system-specific-properties 33 | // https://code.visualstudio.com/docs/editor/tasks#_background-watching-tasks 34 | // https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson 35 | -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | { 2 | appId: "Rendevoz", 3 | productName: "Rendevoz", 4 | copyright: "Copyright © 2022 RealRong", 5 | asar: true, 6 | directories: { 7 | output: "release/${version}", 8 | buildResources: "electron/resources", 9 | }, 10 | files: [ 11 | "dist-electron", 12 | "dist" 13 | ], 14 | win: { 15 | target: [ 16 | { 17 | target: "nsis", 18 | arch: ["x64"], 19 | }, 20 | ], 21 | artifactName: "${productName}-Windows-${version}-Setup.${ext}", 22 | }, 23 | nsis: { 24 | oneClick: false, 25 | perMachine: false, 26 | allowToChangeInstallationDirectory: true, 27 | deleteAppDataOnUninstall: false, 28 | }, 29 | mac: { 30 | target: ["dmg"], 31 | artifactName: "${productName}-Mac-${version}-Installer.${ext}", 32 | }, 33 | linux: { 34 | icon: "electron/resources/iconset", 35 | target: ["AppImage", "deb"], 36 | artifactName: "${productName}-Linux-${version}.${ext}", 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /electron/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: 'true' 6 | DIST_ELECTRON: string 7 | DIST: string 8 | /** /dist/ or /public/ */ 9 | PUBLIC: string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /electron/main/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { AppController } from './app.controller' 3 | import { AppService } from './app.service' 4 | import { GlobalModule } from './global/global.module' 5 | 6 | @Module({ 7 | imports: [GlobalModule], 8 | controllers: [AppController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule { } 12 | -------------------------------------------------------------------------------- /electron/main/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AppService { 5 | public getTime(): number { 6 | return new Date().getTime() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /electron/main/global/global.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common' 2 | import { app } from 'electron' 3 | import { WinModule } from './win.module' 4 | 5 | @Global() 6 | @Module({ 7 | providers: [{ 8 | provide: 'IS_DEV', 9 | useValue: !app.isPackaged, 10 | }], 11 | imports: [WinModule], 12 | exports: [WinModule, 'IS_DEV'], 13 | }) 14 | export class GlobalModule { } 15 | -------------------------------------------------------------------------------- /electron/main/index.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, shell } from 'electron' 2 | import { release } from 'os' 3 | import { join } from 'path' 4 | import { NestFactory } from '@nestjs/core' 5 | import { MicroserviceOptions } from '@nestjs/microservices' 6 | import { AppModule } from './app.module' 7 | import { ElectronIpcTransport } from './transport' 8 | 9 | async function bootstrap() { 10 | try { 11 | const nestApp = await NestFactory.createMicroservice(AppModule, { 12 | strategy: new ElectronIpcTransport() 13 | }) 14 | await nestApp.listen() 15 | } catch (error) { 16 | console.log(error) 17 | app.quit() 18 | } 19 | } 20 | // Disable GPU Acceleration for Windows 7 21 | if (release().startsWith('6.1')) app.disableHardwareAcceleration() 22 | 23 | // Set application name for Windows 10+ notifications 24 | if (process.platform === 'win32') app.setAppUserModelId(app.getName()) 25 | 26 | if (!app.requestSingleInstanceLock()) { 27 | app.quit() 28 | process.exit(0) 29 | } 30 | 31 | bootstrap() -------------------------------------------------------------------------------- /electron/main/transport/decorators.ts: -------------------------------------------------------------------------------- 1 | import { UseFilters, applyDecorators } from '@nestjs/common' 2 | import { MessagePattern } from '@nestjs/microservices' 3 | import { ipcMain } from 'electron' 4 | import { ipcMessageDispatcher } from './dispatcher' 5 | import { AllExceptionsFilter } from './filter' 6 | 7 | function GetParamsFromMessageChannel() { 8 | return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { 9 | const method = descriptor.value 10 | descriptor.value = function (args: any[]) { 11 | const [ipcMainEventObject, ...payload] = args 12 | const newArgs = [ 13 | ...payload, 14 | { 15 | evt: ipcMainEventObject, 16 | }, 17 | ] 18 | return method.apply(this, newArgs) 19 | } 20 | return descriptor 21 | } 22 | } 23 | 24 | export function IpcInvoke(messageChannel: string) { 25 | ipcMain.handle(messageChannel, (...args) => ipcMessageDispatcher.emit(messageChannel, ...args)) 26 | 27 | // Do not modify the order! 28 | return applyDecorators( 29 | GetParamsFromMessageChannel(), 30 | MessagePattern(messageChannel), 31 | UseFilters(new AllExceptionsFilter()), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /electron/main/transport/dispatcher.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | 3 | class IPCMessageDispatcher extends EventEmitter { 4 | emit(messageChannel: string, ...args: any[]): any { 5 | const [ipcHandler] = this.listeners('ipc-message') 6 | 7 | if (ipcHandler) 8 | return Reflect.apply(ipcHandler, this, [messageChannel, ...args]) 9 | } 10 | } 11 | 12 | export const ipcMessageDispatcher = new IPCMessageDispatcher() 13 | -------------------------------------------------------------------------------- /electron/main/transport/filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ExceptionFilter } from '@nestjs/common' 2 | 3 | @Catch() 4 | export class AllExceptionsFilter implements ExceptionFilter { 5 | catch(exception: any) { 6 | throw exception 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /electron/main/transport/index.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common' 2 | import { CustomTransportStrategy, MessageHandler, Server } from '@nestjs/microservices' 3 | import { ipcMessageDispatcher } from './dispatcher' 4 | 5 | export class ElectronIpcTransport extends Server implements CustomTransportStrategy { 6 | protected readonly logger = new Logger(ElectronIpcTransport.name) 7 | 8 | async onMessage(messageChannel: string, ...args: any[]): Promise { 9 | const handler: MessageHandler | undefined = this.messageHandlers.get(messageChannel) 10 | if (!handler) 11 | return this.logger.warn(`No handlers for message ${messageChannel}`) 12 | 13 | try { 14 | this.logger.debug(`Process message ${messageChannel}`) 15 | 16 | const result = await handler(args) 17 | 18 | return { 19 | data: result, 20 | } 21 | } 22 | catch (error) { 23 | this.logger.error(error) 24 | return { 25 | error, 26 | } 27 | } 28 | } 29 | 30 | close(): any { 31 | } 32 | 33 | listen(callback: () => void): any { 34 | ipcMessageDispatcher.on('ipc-message', this.onMessage.bind(this)) 35 | callback() 36 | } 37 | } 38 | 39 | export * from './decorators' 40 | -------------------------------------------------------------------------------- /electron/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/electron/resources/icon.icns -------------------------------------------------------------------------------- /electron/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/electron/resources/icon.ico -------------------------------------------------------------------------------- /electron/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/electron/resources/icon.png -------------------------------------------------------------------------------- /electron/resources/iconset/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/electron/resources/iconset/256x256.png -------------------------------------------------------------------------------- /electron/resources/installerIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/electron/resources/installerIcon.ico -------------------------------------------------------------------------------- /electron/resources/uninstallerIcon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/electron/resources/uninstallerIcon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Rendevoz 9 | 10 | 11 |
12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/Muli.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/public/Muli.ttf -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/twitter-emoji-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/public/twitter-emoji-32.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2022 RealRong 2001 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as 6 | published by the Free Software Foundation, either version 3 of the 7 | License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | import { Provider as JotaiProvider } from 'jotai' 19 | import { BrowserRouter as Router } from 'react-router-dom' 20 | import Routes from './routes' 21 | import { GlobalScope } from './jotai/jotaiScope' 22 | import '@icon-park/react/styles/index.less' 23 | import './common.less' 24 | import { Toaster } from 'react-hot-toast' 25 | import TitleBar from './components/titlebar' 26 | import Initializer from './Initializer' 27 | import './global.less' 28 | 29 | const App = () => { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | export default App 43 | -------------------------------------------------------------------------------- /src/Initializer.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useNoteStore from './stores/note.store' 3 | 4 | const Initializer = () => { 5 | const { getAllNotes } = useNoteStore() 6 | useEffect(() => { 7 | getAllNotes() 8 | }, []) 9 | return null 10 | } 11 | 12 | export default Initializer 13 | -------------------------------------------------------------------------------- /src/assets/electron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/assets/electron.png -------------------------------------------------------------------------------- /src/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/assets/error.png -------------------------------------------------------------------------------- /src/assets/jieba_rs_wasm_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/assets/jieba_rs_wasm_bg.wasm -------------------------------------------------------------------------------- /src/assets/locales/en-US/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home":{ 3 | "greeting": "Good {{time}}, welcome back", 4 | "recently visit": "Recently visit", 5 | "recently add": "Recently add" 6 | }, 7 | "sidebar":{ 8 | "home": "Home", 9 | "editor": "Editor", 10 | "collections": "Collections", 11 | "search": "Search", 12 | "new": "New", 13 | "setting": "Settings" 14 | }, 15 | "editor": { 16 | "untitled": "Untitled", 17 | "save": "Save", 18 | "redo": "Redo", 19 | "undo": "Undo", 20 | "font color": "Font color", 21 | "bold": "Bold", 22 | "italic": "Italic", 23 | "underline": "Underline", 24 | "more": "More", 25 | "block elements": "Block elements", 26 | "inline elements": "Inline elements", 27 | "use emoji to express your ideas": "Use emoji to express your ideas!", 28 | "sub page": "Sub page", 29 | "add sub page": "Add sub page", 30 | "add new page": "Add new page", 31 | "open note in collections": "Open note in collections", 32 | "close panel": "Close panel", 33 | "keyword": "Keyword", 34 | "use keyword to organize your ideas": "Use keywor to organize your ideas!" 35 | } 36 | } -------------------------------------------------------------------------------- /src/assets/locales/zh-CN/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home":{ 3 | "greeting": "欢迎回来", 4 | "recently visit": "最近访问", 5 | "recently add": "最近添加" 6 | }, 7 | "sidebar":{ 8 | "home": "主页", 9 | "editor": "编辑器", 10 | "collections": "收藏夹", 11 | "search": "搜索", 12 | "new": "新建", 13 | "setting": "设置" 14 | }, 15 | "editor": { 16 | "untitled": "未命名", 17 | "save": "保存", 18 | "redo": "重做", 19 | "undo": "撤销", 20 | "font color": "文字颜色", 21 | "bold": "加粗", 22 | "italic": "斜体", 23 | "underline": "下划线", 24 | "more": "更多", 25 | "block elements": "块元素", 26 | "inline elements": "行内元素", 27 | "use emoji to express your ideas": "使用Emoji来表达想法吧!", 28 | "sub page": "子页面", 29 | "add sub page": "添加子页面", 30 | "add new page": "添加新页面", 31 | "open note in collections": "打开收藏夹内的笔记", 32 | "close panel": "关闭面版", 33 | "keyword": "关键词", 34 | "use keyword to organize your ideas": "使用关键词来组织思路!" 35 | } 36 | } -------------------------------------------------------------------------------- /src/assets/twitter-emoji-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/assets/twitter-emoji-32.png -------------------------------------------------------------------------------- /src/common/configs/promiseErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import toast from 'react-hot-toast' 2 | 3 | const globalPromiseErrorHandler = e => { 4 | toast.error(e.reason) 5 | } 6 | 7 | window.onunhandledrejection = globalPromiseErrorHandler 8 | -------------------------------------------------------------------------------- /src/common/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { default as Noop } from './noop' -------------------------------------------------------------------------------- /src/common/types/noop.ts: -------------------------------------------------------------------------------- 1 | type Noop = () => void 2 | 3 | export default Noop 4 | -------------------------------------------------------------------------------- /src/components/base/FullSizeLink.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Link } from 'react-router-dom' 3 | import { LinkProps } from 'react-router-dom' 4 | 5 | const FullSizeLink: FC = props => { 6 | return 7 | } 8 | export default FullSizeLink -------------------------------------------------------------------------------- /src/components/base/Image.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ImgHTMLAttributes, ReactNode, useState } from 'react' 2 | import Icon from './Icon' 3 | 4 | const Image: FC & { placeHolderSize?: number; placeHolder?: ReactNode }> = ({ 5 | placeHolderSize = 20, 6 | placeHolder, 7 | ...rest 8 | }) => { 9 | const [loaded, setLoaded] = useState(false) 10 | return ( 11 | <> 12 | {!loaded && (placeHolder || )} 13 | setLoaded(true)} onError={() => setLoaded(false)}/> 14 | 15 | ) 16 | } 17 | 18 | export default Image 19 | -------------------------------------------------------------------------------- /src/components/base/ReadMore.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme, useToggle } from '@/hooks' 2 | import { CSSProperties, FC, useEffect, useState } from 'react' 3 | import Icon from './Icon' 4 | import { Content } from '.' 5 | 6 | interface ReadMoreProps { 7 | textLength?: number 8 | children: React.ReactNode 9 | style?: CSSProperties 10 | } 11 | const ReadMore: FC = ({ children, textLength = 150, style }) => { 12 | const text = children as string 13 | const [isReadMore, toggleReadMore] = useToggle(text.length > textLength) 14 | const { primaryColor } = useTheme('primaryColor') 15 | return ( 16 | 17 | 25 | {isReadMore ? text.slice(0, textLength).concat('...') : text} 26 | 27 | {text.length > textLength && ( 28 | { 31 | e.stopPropagation() 32 | toggleReadMore() 33 | }} 34 | > 35 | {isReadMore ? 'Read more' : 'Show less'} 36 | 37 | )} 38 | 39 | ) 40 | } 41 | 42 | export default ReadMore 43 | -------------------------------------------------------------------------------- /src/components/base/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { Icon, Content } from '..' 3 | 4 | interface SpinnerProps { 5 | size?: number 6 | color?: string 7 | } 8 | const Spinner: FC = ({ size = 20, color = '#8590ae' }) => { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | export default Spinner 17 | -------------------------------------------------------------------------------- /src/components/base/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { useSetTheme, useTheme, useToggle } from '@/hooks' 2 | import { CSSProperties, FC } from 'react' 3 | import { motion } from 'framer-motion' 4 | 5 | interface SwitchProps { 6 | onChange?: (value: boolean) => void 7 | defaultValue?: boolean 8 | width?: number 9 | height?: number 10 | } 11 | const spring = { 12 | type: 'spring', 13 | stiffness: 700, 14 | damping: 30 15 | } 16 | const Switch: FC = ({ onChange, defaultValue = false }) => { 17 | const [on, toggleOn] = useToggle(defaultValue) 18 | const { primaryColor } = useTheme('primaryColor') 19 | const switchStyle: CSSProperties = { 20 | width: 44, 21 | flexShrink: 0, 22 | height: 22, 23 | backgroundColor: on ? primaryColor : '#00000040', 24 | display: 'flex', 25 | justifyContent: on ? 'flex-end' : 'flex-start', 26 | borderRadius: 999, 27 | padding: 2, 28 | cursor: 'pointer', 29 | transition: 'background 0.3s ease', 30 | position: 'relative' 31 | } 32 | const handleStyle: CSSProperties = { 33 | width: 18, 34 | height: 18, 35 | backgroundColor: 'white', 36 | borderRadius: 999 37 | } 38 | return ( 39 |
{ 41 | toggleOn() 42 | onChange?.(!on) 43 | }} 44 | style={switchStyle} 45 | > 46 | 47 |
48 | ) 49 | } 50 | 51 | export default Switch 52 | -------------------------------------------------------------------------------- /src/components/base/WithBorder.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, forwardRef, MouseEventHandler, PropsWithChildren } from 'react' 2 | 3 | interface WithBorderProps { 4 | boxShadowBorder?: boolean 5 | solidBorder?: boolean 6 | borderRadius?: number 7 | style?: CSSProperties 8 | className?: string 9 | onClick?: MouseEventHandler 10 | contentEditable?: boolean 11 | } 12 | 13 | const WithBorder: FC> = ( 14 | { borderRadius = 5, boxShadowBorder = true, solidBorder = false, children, style, onClick, className, contentEditable, ...rest }, 15 | ref 16 | ) => { 17 | const DefaultBorder: CSSProperties = { 18 | boxShadow: boxShadowBorder ? 'rgba(0, 0, 0, 0.16) 0px 1px 4px' : undefined, 19 | border: solidBorder ? '1px solid grey' : undefined, 20 | borderRadius: borderRadius, 21 | backgroundColor: 'white', 22 | ...style 23 | } 24 | const handleClick = (e) => { 25 | // e.stopPropagation() 26 | onClick?.(e) 27 | } 28 | return ( 29 |
30 | {children} 31 |
32 | ) 33 | } 34 | 35 | export default forwardRef(WithBorder) 36 | -------------------------------------------------------------------------------- /src/components/base/animation/SwitchAnimation.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren, ReactNode } from 'react' 2 | import { AnimatePresence, motion } from 'framer-motion' 3 | import Content from '../Content' 4 | const transition = { 5 | x: { type: 'spring', stiffness: 300, damping: 30 }, 6 | opacity: { duration: 0.2 } 7 | } 8 | const variants = { 9 | enter: (direction: number) => { 10 | return { 11 | x: direction > 0 ? '100%' : '-100%', 12 | opacity: 0 13 | } 14 | }, 15 | center: { 16 | zIndex: 1, 17 | x: 0, 18 | opacity: 1 19 | }, 20 | exit: (direction: number) => { 21 | return { 22 | zIndex: 0, 23 | x: direction > 0 ? '100%' : '-100%', 24 | opacity: 0 25 | } 26 | } 27 | } 28 | const SwitchAnimation: FC<{ 29 | children: (switchWrapper: (children: ReactNode, key: string) => JSX.Element) => ReactNode 30 | direction: boolean 31 | }> = ({ children, direction }) => { 32 | const switchWrapper = (children: ReactNode, key: string) => { 33 | return ( 34 | 44 | {children} 45 | 46 | ) 47 | } 48 | return {children ? children(switchWrapper) : null} 49 | } 50 | 51 | export default SwitchAnimation 52 | -------------------------------------------------------------------------------- /src/components/base/authenticatedLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import Layout from '../layout' 3 | import { Sidebar } from '../sidebar' 4 | 5 | interface Props { 6 | children?: React.ReactNode 7 | } 8 | const AuthenticatedLayout: FC = ({ children }) => { 9 | return }>{children} 10 | } 11 | 12 | export default AuthenticatedLayout 13 | -------------------------------------------------------------------------------- /src/components/base/breadcrumb/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, FC } from 'react' 2 | import BreadcrumbItem from './BreadcrumbItem' 3 | import classNames from 'classnames' 4 | import styles from './index.module.less' 5 | import { toArray } from 'lodash' 6 | 7 | export interface BreadcrumbProps { 8 | separator?: React.ReactNode 9 | style?: React.CSSProperties 10 | className?: string 11 | children: React.ReactNode 12 | } 13 | 14 | interface IBreadcrumb extends FC { 15 | Item: typeof BreadcrumbItem 16 | } 17 | 18 | const Breadcrumb: IBreadcrumb = ({ separator = '/', style, className, children }) => { 19 | const clazzName = classNames(styles.breadcrumb, className) 20 | let crumbs 21 | if (children) { 22 | crumbs = Children.toArray(children).map((item, index) => { 23 | if (!item) { 24 | return item 25 | } 26 | return cloneElement(item, { 27 | separator: item.props?.separator ?? separator, 28 | key: index 29 | }) 30 | }) 31 | } 32 | return ( 33 | 36 | ) 37 | } 38 | 39 | Breadcrumb.Item = BreadcrumbItem 40 | 41 | export default Breadcrumb 42 | -------------------------------------------------------------------------------- /src/components/base/breadcrumb/BreadcrumbItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import NavLinkWithChildren from '../navLink' 3 | import styles from './index.module.less' 4 | import classnames from 'classnames' 5 | import Content from '../Content' 6 | interface CommonItem { 7 | style?: React.CSSProperties 8 | className?: string 9 | separator?: React.ReactNode 10 | } 11 | interface ButtonItem { 12 | type: 'button' 13 | to?: never 14 | children: React.ReactNode 15 | onClick: () => void 16 | } 17 | interface RouteItem { 18 | type: 'route' 19 | onClick?: never 20 | to: string 21 | children: (match: boolean) => React.ReactNode 22 | } 23 | 24 | export type BreadcrumbItemProps = CommonItem & (ButtonItem | RouteItem) 25 | 26 | const BreadcrumbItem: FC = ({ type, to, onClick, children, separator = '/', style, className }) => { 27 | const clazzName = classnames(className, styles.item) 28 | let item = null 29 | switch (type) { 30 | case 'button': 31 | item = ( 32 | 33 | {children} 34 | 35 | ) 36 | break 37 | case 'route': 38 | item = {match => <>{children(match)}} 39 | break 40 | default: 41 | break 42 | } 43 | 44 | return ( 45 | 46 | {item} 47 | {separator && {separator}} 48 | 49 | ) 50 | } 51 | 52 | export default BreadcrumbItem 53 | -------------------------------------------------------------------------------- /src/components/base/breadcrumb/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../styles/common.module.less"; 2 | 3 | .breadcrumb{ 4 | color: @font-light-blue-color; 5 | font-size: 12px; 6 | display: flex; 7 | align-items: center; 8 | ol{ 9 | display: flex; 10 | align-items: center; 11 | flex-wrap: wrap; 12 | margin: 0; 13 | padding: 0; 14 | list-style: none; 15 | > div{ 16 | margin-right: 5px; 17 | &:last-child{ 18 | .separator{ 19 | display: none; 20 | } 21 | } 22 | } 23 | } 24 | 25 | } 26 | .separator{ 27 | margin-left: 4px; 28 | color: @font-light-blue-color; 29 | } 30 | .item{ 31 | color: @font-light-blue-color; 32 | transition: @default-transition; 33 | cursor: pointer; 34 | &:hover{ 35 | color: @font-light-grey-color; 36 | } 37 | } -------------------------------------------------------------------------------- /src/components/base/breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import Breadcrumb from './Breadcrumb' 2 | 3 | export type { BreadcrumbProps } from './Breadcrumb' 4 | export default Breadcrumb 5 | -------------------------------------------------------------------------------- /src/components/base/container/index.module.less: -------------------------------------------------------------------------------- 1 | .container{ 2 | background: rgb(255,255,255); 3 | position: relative; 4 | width: 100%; 5 | height: calc( 100% - 30px); 6 | display: flex; 7 | flex-grow: 1; 8 | flex-basis: auto; 9 | } -------------------------------------------------------------------------------- /src/components/base/container/index.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames' 2 | import { FC } from 'react' 3 | import styles from './index.module.less' 4 | 5 | interface ContainerProps { 6 | className?: string 7 | column?: boolean 8 | auto?: boolean 9 | levelCenter?: boolean 10 | verticalCenter?: boolean 11 | children: React.ReactNode 12 | style?: React.CSSProperties 13 | } 14 | 15 | const Container: FC = ({ column, auto, children, levelCenter, verticalCenter, style,className }) => { 16 | return ( 17 |
27 | {children} 28 |
29 | ) 30 | } 31 | export default Container 32 | -------------------------------------------------------------------------------- /src/components/base/dock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DockTabs' 2 | export * from './DockData' 3 | export * from './DockPanel' 4 | export * from './DockBox' 5 | export * from './DockLayout' 6 | export * from './dragdrop/DragManager' 7 | export * from './dragdrop/GestureManager' 8 | export * from './dragdrop/DragDropDiv' 9 | export * from './Divider' 10 | 11 | import { DockLayout } from './DockLayout' 12 | 13 | export default DockLayout 14 | -------------------------------------------------------------------------------- /src/components/base/dock/util/Compare.ts: -------------------------------------------------------------------------------- 1 | export function compareKeys(a: { [key: string]: any }, b: { [key: string]: any }, keys: string[]) { 2 | if (a === b) { 3 | return true 4 | } 5 | if (a && b && typeof a === 'object' && typeof b === 'object') { 6 | for (const key of keys) { 7 | if (a[key] !== b[key]) { 8 | return false 9 | } 10 | } 11 | return true 12 | } 13 | return false 14 | } 15 | 16 | const isArray = Array.isArray 17 | 18 | export function compareArray(a: { [key: string]: any }[], b: { [key: string]: any }[]) { 19 | if (a === b) { 20 | return true 21 | } 22 | if (isArray(a) && isArray(b)) { 23 | const len = a.length 24 | if (len !== b.length) { 25 | return false 26 | } 27 | for (let i = 0; i < len; ++i) { 28 | if (a[i] !== b[i]) { 29 | return false 30 | } 31 | } 32 | return true 33 | } 34 | return false 35 | } 36 | -------------------------------------------------------------------------------- /src/components/base/footer/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../styles/common.module.less"; 2 | 3 | .back{ 4 | padding: 6px 18px 6px 0px ; 5 | background:transparent; 6 | box-shadow: none !important; 7 | color: @default-grey-link-color; 8 | margin-right: 30px; 9 | } 10 | .submit{ 11 | border-radius: 8px; 12 | padding: 6px 18px; 13 | background: #8590ae; 14 | } -------------------------------------------------------------------------------- /src/components/base/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import { RippleButton } from '@/components/custom/rippleButton' 3 | import { FC } from 'react' 4 | import Content from '../Content' 5 | import styles from './index.module.less' 6 | 7 | interface FooterProps { 8 | submitText?: string 9 | backText?: string 10 | onSubmit?: Noop 11 | onBack?: Noop 12 | } 13 | const Footer: FC = ({ onBack, onSubmit,submitText = 'Submit',backText = 'Back' }) => { 14 | return ( 15 | 16 | 17 | {backText} 18 | 19 | 20 | {submitText} 21 | 22 | 23 | ) 24 | } 25 | 26 | export default Footer 27 | -------------------------------------------------------------------------------- /src/components/base/form/useCanSubmit.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from "jotai"; 2 | 3 | const canSubmitAtom = atom(false) 4 | 5 | export const useCanSubmit = () => useAtom(canSubmitAtom) -------------------------------------------------------------------------------- /src/components/base/form/useIsSubmitted.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | 3 | const isSubmittedAtom = atom(false) 4 | 5 | export const useIsSubmitted = () => useAtom(isSubmittedAtom) 6 | -------------------------------------------------------------------------------- /src/components/base/heading/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../../styles/common.module.less'; 2 | 3 | .heading{ 4 | color: @font-light-blue-color; 5 | } -------------------------------------------------------------------------------- /src/components/base/heading/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import styles from './index.module.less' 3 | 4 | interface HeadingProps { 5 | fontSize?: string | number 6 | children: React.ReactNode 7 | } 8 | const Heading: FC = ({ fontSize, children }) => { 9 | return ( 10 |
11 |
12 | {children} 13 |
14 |
15 | ) 16 | } 17 | export default Heading 18 | -------------------------------------------------------------------------------- /src/components/base/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Content } from './Content' 2 | export { default as WithBorder } from './WithBorder' 3 | export { default as Toolbar } from './toolbar' 4 | export { default as Panel } from './Panel' 5 | export { default as AnimatePopover } from './AnimatePopover' 6 | export { Tabs, TabPane } from './tabs' 7 | export { default as Form } from './form' 8 | export { default as Breadcrumb } from './breadcrumb' 9 | export { default as Footer } from './footer' 10 | export { default as Image } from './Image' 11 | export { default as IconWithPopover } from './IconWithPopover' 12 | export { default as Tooltip } from './Tooltip' 13 | export { default as Icon } from './Icon' 14 | export { default as Spinner } from './Spinner' 15 | -------------------------------------------------------------------------------- /src/components/base/layout/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/base/layout/index.module.less -------------------------------------------------------------------------------- /src/components/base/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import useSidebarWidth from '@/hooks/components/useSidebarWidth' 2 | import { FC } from 'react' 3 | import { Helmet } from 'react-helmet' 4 | import Container from '../container' 5 | 6 | interface LayoutProps { 7 | sidebar?: React.ReactNode 8 | children?: React.ReactNode 9 | } 10 | 11 | const Layout: FC = ({ sidebar, children }) => { 12 | const [sidebarWidth] = useSidebarWidth() 13 | return ( 14 | 15 | 16 | Rendevoz 17 | 18 | 19 | 20 | {sidebar} 21 | 22 | {children} 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default Layout 30 | -------------------------------------------------------------------------------- /src/components/base/menu/MenuGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from '@/components/base' 2 | import { FC, ReactNode } from 'react' 3 | import styles from './index.module.less' 4 | 5 | interface MenuGroupProps { 6 | title: string 7 | children: ReactNode 8 | } 9 | const MenuGroup: FC = ({ children, title }) => { 10 | return ( 11 | 12 |
{title}
13 | {children} 14 |
15 | ) 16 | } 17 | export default MenuGroup 18 | -------------------------------------------------------------------------------- /src/components/base/menu/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../styles/common.module.less"; 2 | 3 | .menu{ 4 | min-width: 120px; 5 | pointer-events: all; 6 | border-radius: 6px; 7 | padding: 0; 8 | position: relative; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | .item{ 13 | display: flex; 14 | cursor: pointer; 15 | width: 100%; 16 | align-items: center; 17 | min-height: 32px; 18 | color: @default-grey-link-color; 19 | justify-content: left; 20 | padding: 4px 12px; 21 | transition: @default-transition; 22 | &:hover{ 23 | background-color: @default-btn-hover-bg-color; 24 | } 25 | &.selected{ 26 | background-color: @default-btn-hover-bg-color; 27 | } 28 | } 29 | .text{ 30 | overflow: hidden; 31 | white-space: nowrap; 32 | text-overflow: ellipsis; 33 | } 34 | .title{ 35 | font-size: 20px; 36 | line-height: 24px; 37 | font-weight: 600; 38 | } 39 | .icon{ 40 | display: flex; 41 | width: 24px; 42 | height: 24px; 43 | color: @default-grey-link-color; 44 | margin-right: 8px; 45 | font-size: 24px; 46 | } 47 | .group{ 48 | user-select: none; 49 | min-width: 280px; 50 | &:not(:first-child){ 51 | margin-top: 10px; 52 | } 53 | } 54 | .groupTitle{ 55 | display: flex; 56 | padding: 0px 14px; 57 | margin-bottom: 8px; 58 | font-size: 12px; 59 | font-weight: 500; 60 | line-height: 20px; 61 | user-select: none; 62 | } -------------------------------------------------------------------------------- /src/components/base/menu/index.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC, MouseEventHandler, ReactNode } from 'react' 2 | import Content from '../Content' 3 | import styles from './index.module.less' 4 | import MenuGroup from './MenuGroup' 5 | import MenuItem from './MenuItem' 6 | 7 | interface MenuProps { 8 | title?: string 9 | extra?: ReactNode 10 | footer?: ReactNode 11 | children: ReactNode 12 | style?: CSSProperties 13 | onClick?: MouseEventHandler 14 | } 15 | interface IMenu extends FC { 16 | Item: typeof MenuItem 17 | Group: typeof MenuGroup 18 | } 19 | const Menu: IMenu = ({ title, children, extra, footer, style,onClick }) => { 20 | return ( 21 |
22 | 23 | {title &&
{title}
} 24 | {extra} 25 |
26 | {children} 27 | {footer && {footer}} 28 |
29 | ) 30 | } 31 | Menu.Item = MenuItem 32 | Menu.Group = MenuGroup 33 | export default Menu 34 | -------------------------------------------------------------------------------- /src/components/base/modal/index.module.less: -------------------------------------------------------------------------------- 1 | .modal{ 2 | margin: auto; 3 | z-index: 1106; 4 | padding: 16px 24px; 5 | border-radius: 8px; 6 | background-color: white; 7 | overflow: hidden; 8 | } 9 | .backdrop{ 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | height: 100vh; 14 | width: 100vw; 15 | background-color: rgba(0,0,0,0.7); 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | overflow-y: hidden; 20 | z-index: 1105; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/base/navLink/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { NavLink, NavLinkProps, Route } from 'react-router-dom' 3 | type NavLinkWithChildrenProps = Omit & { 4 | exact?: boolean 5 | to: string 6 | activeStyle?: React.CSSProperties 7 | children: (isActive: boolean) => React.ReactNode 8 | } 9 | const NavLinkWithChildren: FC = ({ to, children, activeStyle, ...rest }) => { 10 | return ( 11 | (isActive ? activeStyle : undefined)}> 12 | {({ isActive }) => (children ? children(isActive) : null)} 13 | 14 | ) 15 | } 16 | export default NavLinkWithChildren 17 | -------------------------------------------------------------------------------- /src/components/base/select/SelectOption.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactElement, ReactNode } from 'react' 2 | 3 | export interface SelectOptionProps { 4 | children: string 5 | value: string 6 | } 7 | 8 | const SelectOption: FC = () => { 9 | return null 10 | } 11 | export default SelectOption 12 | -------------------------------------------------------------------------------- /src/components/base/select/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../styles/common.module.less"; 2 | .selectContainer{ 3 | padding: 4px 0px; 4 | cursor: pointer; 5 | max-width: 300px; 6 | flex-grow: 1; 7 | flex-shrink: 0; 8 | overflow: hidden; 9 | border-bottom: @default-border; 10 | transition: @default-transition; 11 | height: 34px; 12 | &:hover{ 13 | border-bottom: 1px solid @default-link-color; 14 | } 15 | &.focused{ 16 | border-bottom: 1px solid @default-link-color; 17 | } 18 | &.disabled{ 19 | cursor: not-allowed; 20 | border-bottom: none; 21 | } 22 | } 23 | .placeholder{ 24 | color: @font-light-blue-color; 25 | } 26 | .tag{ 27 | padding: 3px 0px; 28 | color: @font-light-blue-color; 29 | // background-color: #A7BBC7; 30 | border-radius: 4px; 31 | margin-right: 7px; 32 | > span{ 33 | line-height: 1; 34 | margin: 3px 4px 3px 0px; 35 | } 36 | } -------------------------------------------------------------------------------- /src/components/base/select/index.tsx: -------------------------------------------------------------------------------- 1 | import Select from './Select' 2 | 3 | export default Select -------------------------------------------------------------------------------- /src/components/base/sidebar/Item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { motion } from 'framer-motion' 3 | import styles from './index.module.less' 4 | import { NavLink } from 'react-router-dom' 5 | import NavLinkWithChildren from '../navLink' 6 | 7 | const variants = { 8 | open: { 9 | y: 0, 10 | opacity: 1, 11 | transition: { 12 | y: { stiffness: 1000, velocity: -100 } 13 | } 14 | }, 15 | closed: { 16 | y: 50, 17 | opacity: 0, 18 | transition: { 19 | y: { stiffness: 1000 } 20 | } 21 | } 22 | } 23 | 24 | export const MenuItem = ({ children, to, onClick }: { children: React.ReactNode; to?: string; onClick?: React.MouseEventHandler }) => { 25 | return to ? ( 26 | 27 | {match => ( 28 | 29 | {children} 30 | 31 | )} 32 | 33 | ) : ( 34 | 35 | {children} 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/base/tabs/index.module.less: -------------------------------------------------------------------------------- 1 | .tabs{ 2 | overflow-y: auto; 3 | white-space: nowrap; 4 | border-bottom: 1px solid grey; 5 | scrollbar-width: none; 6 | &::-webkit-scrollbar{ 7 | display: none; 8 | } 9 | } 10 | .tab{ 11 | position: relative; 12 | display: inline-flex; 13 | align-items: center; 14 | font-weight: 500; 15 | font-size: 14px; 16 | margin-right: 24px; 17 | padding: 6px 0; 18 | color: blue; 19 | user-select: none; 20 | &:hover{ 21 | color: black 22 | } 23 | } 24 | .underline{ 25 | position: absolute; 26 | bottom: 0; 27 | left: 0; 28 | right: 0; 29 | height: 3px; 30 | width: 100%; 31 | border-radius: 7px; 32 | background-color: #8590ae; 33 | } 34 | .tabNav{ 35 | padding: 10px; 36 | margin-right: 5px; 37 | cursor: pointer; 38 | position: relative; 39 | user-select: none; 40 | font-family: Muli,sans-serif; 41 | font-weight: 600; 42 | } 43 | .tabPane{ 44 | position: relative; 45 | height: 100%; 46 | transition: opacity 0.5s ease-in-out; 47 | } -------------------------------------------------------------------------------- /src/components/base/timeline/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/base/timeline/index.module.less -------------------------------------------------------------------------------- /src/components/base/timeline/index.tsx: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, FC, ReactElement, ReactNode } from 'react' 2 | import TimelineItem from './TimelineItem' 3 | 4 | interface TimelineProps { 5 | children?: ReactNode 6 | iconSize?: number 7 | iconColor?: string 8 | } 9 | 10 | interface ITimeline extends FC { 11 | Item: typeof TimelineItem 12 | } 13 | 14 | const Timeline: ITimeline = ({ children, iconSize }) => { 15 | const items = Children.toArray(children) 16 | const childrenItems = items 17 | .filter(i => !!i) 18 | .map((i: ReactElement, idx, array) => { 19 | const length = array.length 20 | const isLast = idx === length - 1 21 | return cloneElement(i, { 22 | iconSize, 23 | hideTail: isLast 24 | }) 25 | }) 26 | return
    {childrenItems}
27 | } 28 | Timeline.Item = TimelineItem 29 | 30 | export default Timeline 31 | -------------------------------------------------------------------------------- /src/components/base/toolbar/ToolbarSeparator.tsx: -------------------------------------------------------------------------------- 1 | import { CSSProperties, FC } from 'react' 2 | 3 | interface ToolbarSeparatorProps { 4 | direction?: 'vertical' | 'horizontal' 5 | } 6 | 7 | const ToolbarSeparator: FC = ({ direction }) => { 8 | const verticalSeparator: CSSProperties = { 9 | width: '1px', 10 | height: '60%', 11 | margin: '0px 6px', 12 | background: '#d9d9d9' 13 | } 14 | const horizontalSeparator: CSSProperties = { 15 | borderRight: '1px solid #CCC' 16 | } 17 | return
18 | } 19 | 20 | export default ToolbarSeparator 21 | -------------------------------------------------------------------------------- /src/components/base/toolbar/index.module.less: -------------------------------------------------------------------------------- 1 | .icon{ 2 | background-color: white; 3 | padding: 4px; 4 | border-radius: 5px; 5 | position: relative; 6 | transition: 0.2s all ease-out; 7 | &:hover{ 8 | background-color: rgba(0,0,0,0.08); 9 | } 10 | &[data-active="true"]{ 11 | background-color: rgba(0,0,0,0.08); 12 | } 13 | &[data-disabled="true"]{ 14 | background-color: white !important; 15 | } 16 | } 17 | .toolbar{ 18 | padding: 0px 6px; 19 | height: 34px; 20 | } 21 | .title{ 22 | padding: 0 20px; 23 | margin-top: 12px; 24 | margin-bottom: 4px; 25 | font-size: 12px; 26 | line-height: 20px; 27 | color: #8096ae; 28 | } -------------------------------------------------------------------------------- /src/components/base/toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import Toolbar from './Toolbar' 2 | 3 | export type { ToolbarProps } from './Toolbar' 4 | export default Toolbar 5 | -------------------------------------------------------------------------------- /src/components/collection/DocumentItem.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from 'react' 2 | import { IPdfDocument } from '~/typings/data' 3 | import Icon from '../base/Icon' 4 | import MenuItem from '../base/menu/MenuItem' 5 | 6 | interface DocumentItemProps { 7 | document: IPdfDocument 8 | onClick: () => void 9 | } 10 | const DocumentItem: FC = ({ document, onClick }) => { 11 | return ( 12 | } type="button"> 13 | {document?.name || document?.metadata?.title} 14 | 15 | ) 16 | } 17 | 18 | export default DocumentItem 19 | -------------------------------------------------------------------------------- /src/components/collection/useCurrentSelect.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { ICollection, IPdfDocument } from '~/typings/data' 3 | 4 | export const CurrentSelectContext = createContext(null) 5 | 6 | export const useCurrentSelect = () => { 7 | const ctx = useContext(CurrentSelectContext) 8 | return ctx 9 | } 10 | -------------------------------------------------------------------------------- /src/components/collection/utils.ts: -------------------------------------------------------------------------------- 1 | export const isDocument = (item: any) => { 2 | return 'fileUrl' in item && 'metadata' in item 3 | } 4 | 5 | export const isNote = (item: any) => { 6 | return 'allBlockIds' in item 7 | } 8 | -------------------------------------------------------------------------------- /src/components/contextMenu/ContextMenuProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | 3 | export interface ContextMenuContext { 4 | docX: number 5 | docY: number 6 | eleX: number 7 | eleY: number 8 | relX: number 9 | relY: number 10 | } 11 | 12 | const ContextMenuContext = createContext(null) 13 | export const ContextMenuProvider = ContextMenuContext.Provider 14 | 15 | export const useContextMenu = () => { 16 | const value = useContext(ContextMenuContext) 17 | if (value === null) { 18 | throw new Error('Not used in Context Menu') 19 | } 20 | return value 21 | } 22 | -------------------------------------------------------------------------------- /src/components/custom/iconButton/index.module.less: -------------------------------------------------------------------------------- 1 | .defaultClickableIcon{ 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | cursor: pointer; 6 | justify-content: center; 7 | width: 30px; 8 | height: 30px; 9 | font-size: 20px; 10 | transition: all 0.4s ease-in; 11 | border-radius: 50%; 12 | span { 13 | position: relative; 14 | } 15 | // &:hover{ 16 | // background-color: rgba(0,0,0,0.05); 17 | // } 18 | &:before { 19 | content: ''; 20 | border: 1px black solid; 21 | background-color: transparent; 22 | border-radius: 50%; 23 | display: block; 24 | position: absolute; 25 | top: 0; 26 | right: 0; 27 | bottom: 0; 28 | left: 0; 29 | transform: scale(0); 30 | } 31 | &.active { 32 | outline: 0; 33 | &:before { 34 | animation: effect_dylan 0.8s ease-out; 35 | } 36 | } 37 | } 38 | @keyframes effect_dylan { 39 | 50% { 40 | transform: scale(1.2, 1.2); 41 | opacity: 0; 42 | } 43 | 99% { 44 | transform: scale(0.001, 0.001); 45 | opacity: 0; 46 | } 47 | 100% { 48 | transform: scale(0.001, 0.001); 49 | opacity: 1; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/custom/iconButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import Icon from '../../base/Icon' 3 | import styles from './index.module.less' 4 | 5 | interface Props extends React.DetailedHTMLProps, HTMLDivElement> { 6 | name: string 7 | iconClassName?: string 8 | } 9 | 10 | export const IconButton: React.FC = props => { 11 | const [ripple, setRipple] = useState(false) 12 | const handleClick = (e: React.MouseEvent) => { 13 | if (!ripple) { 14 | setRipple(true) 15 | props.onClick?.(e) 16 | setTimeout(() => setRipple(false), 800) 17 | } 18 | } 19 | return ( 20 |
27 | 28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/components/custom/icons/CustomIcon.tsx: -------------------------------------------------------------------------------- 1 | import { IIconAllProps } from '@icon-park/react/es/all' 2 | import React from 'react' 3 | import * as IconMap from './map' 4 | function toPascalCase(val: string): string { 5 | return val.replace(/(^\w|-\w)/g, c => c.slice(-1).toUpperCase()) 6 | } 7 | type IconType = keyof typeof IconMap 8 | export default function CustomIcon(props: IIconAllProps) { 9 | const { type, ...extra } = props 10 | const realType = toPascalCase(type) 11 | return React.createElement(IconMap[realType as IconType], extra) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/custom/icons/MoveBlock.tsx: -------------------------------------------------------------------------------- 1 | import { IconWrapper, ISvgIconProps } from '@icon-park/react/es/runtime' 2 | 3 | export default IconWrapper('move-block', true, (props: ISvgIconProps) => ( 4 | 5 | 6 | 13 | 14 | )) 15 | -------------------------------------------------------------------------------- /src/components/custom/icons/map.ts: -------------------------------------------------------------------------------- 1 | export { default as MoveBlock } from './MoveBlock' 2 | -------------------------------------------------------------------------------- /src/components/custom/input/autosizeTextarea/forceHiddenStyles.ts: -------------------------------------------------------------------------------- 1 | const HIDDEN_TEXTAREA_STYLE = { 2 | 'min-height': '0', 3 | 'max-height': 'none', 4 | height: '0', 5 | visibility: 'hidden', 6 | overflow: 'hidden', 7 | position: 'absolute', 8 | 'z-index': '-1000', 9 | top: '0', 10 | right: '0', 11 | } as const; 12 | 13 | const forceHiddenStyles = (node: HTMLElement) => { 14 | Object.keys(HIDDEN_TEXTAREA_STYLE).forEach((key) => { 15 | node.style.setProperty( 16 | key, 17 | HIDDEN_TEXTAREA_STYLE[key as keyof typeof HIDDEN_TEXTAREA_STYLE], 18 | 'important', 19 | ); 20 | }); 21 | }; 22 | 23 | export default forceHiddenStyles; -------------------------------------------------------------------------------- /src/components/custom/input/standard/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/default.less"; 2 | 3 | .CommentInput { 4 | position: relative; 5 | display: flex; 6 | width: 100%; 7 | transition: @default-transition; 8 | &.Collapse { 9 | padding-right: 86px; 10 | } 11 | :global { 12 | .ant-input { 13 | height: 36px; 14 | padding: 6px 12px; 15 | border-radius: 4px; 16 | &:hover { 17 | border-color: @default-link-color !important; 18 | } 19 | &:focus { 20 | border-color: @default-link-color !important; 21 | box-shadow: 0 0 0 2px rgba(138, 87, 235, 0.2) !important; 22 | } 23 | } 24 | } 25 | } 26 | .SubReplySubmitButton { 27 | position: absolute; 28 | right: 0; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | min-width: 72px; 33 | height: 36px; 34 | padding: 0 10px; 35 | color: @default-link-color; 36 | font-weight: bold; 37 | border-radius: 4px; 38 | cursor: pointer; 39 | transition: @default-transition; 40 | &:hover { 41 | background-color: @default-btn-hover-bg-color; 42 | transition: @default-transition; 43 | } 44 | &.Collapse { 45 | transform: scale(0); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/custom/input/standard/index.tsx: -------------------------------------------------------------------------------- 1 | import { InputProps, Input } from 'antd' 2 | import React, { useRef, useState } from 'react' 3 | 4 | import styles from './index.module.less' 5 | 6 | interface Props { 7 | input: InputProps 8 | buttonText?: string 9 | onSubmit?: () => void 10 | ref?: React.MutableRefObject 11 | } 12 | export const StandardInput: React.FC = (props) => { 13 | const { input, buttonText, onSubmit } = props 14 | const [replyButton, setReplyButton] = useState(false) 15 | const ref = useRef(null) 16 | return ( 17 | <> 18 |
{ if (ref.current.state.value === '' || !ref.current.state.value) { setReplyButton(false) }; }} className={`${replyButton ? `${styles.CommentInput} ${styles.Collapse}` : `${styles.CommentInput}`}`}> 19 | { input.onChange && input.onChange(e) }} value={input?.value || undefined} onFocus={() => { setReplyButton(true) }} placeholder={input?.placeholder}/> 20 |
{ onSubmit && onSubmit() }} className={`${replyButton ? `${styles.SubReplySubmitButton}` : `${styles.SubReplySubmitButton} ${styles.Collapse}`}`}>{buttonText}
21 |
22 | 23 | ) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/components/custom/input/withPrefix/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/common.module.less'; 2 | 3 | .prefix{ 4 | color: @font-light-blue-color; 5 | flex-shrink: 0; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | } 10 | .input{ 11 | outline: none; 12 | width: 100%; 13 | min-width: 0; 14 | border-radius: 6px; 15 | transition: @default-transition; 16 | height: 100%; 17 | border: @default-border; 18 | &:hover { 19 | border-color: @default-link-color !important; 20 | box-shadow: 0 0 0 2px rgba(138, 87, 235, 0.2) !important; 21 | } 22 | &:focus { 23 | border-color: @default-link-color !important; 24 | box-shadow: 0 0 0 2px rgba(138, 87, 235, 0.2) !important; 25 | } 26 | } -------------------------------------------------------------------------------- /src/components/custom/input/withPrefix/index.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'antd' 2 | import { FC } from 'react' 3 | import styles from './index.module.less' 4 | interface WithPrefixInputProps{ 5 | prefix: string 6 | enableTextArea?: boolean 7 | onChange?: (e: string) => void 8 | className?: string 9 | style?: React.CSSProperties 10 | } 11 | export const WithPrefixInput: FC = (props) => { 12 | const { prefix, enableTextArea, onChange, className, style } = props 13 | return ( 14 |
15 |
{prefix}
16 |
{enableTextArea ? : onChange?.(e.target.value)}>}
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/components/custom/rippleButton/index.module.less: -------------------------------------------------------------------------------- 1 | .RippleButton { 2 | position: relative; 3 | box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.3); 4 | outline: 0; 5 | overflow: hidden; 6 | color: #fff; 7 | background-color: #8590ae; 8 | border: none; 9 | border-radius: 4px; 10 | cursor: pointer; 11 | &:focus{ 12 | box-shadow: 0 0 1rem rgba(0, 0, 0, 0.3); 13 | } 14 | } 15 | .ripple{ 16 | position: absolute; 17 | display: block; 18 | transform: scale(0); 19 | width: 20px; 20 | height: 20px; 21 | background-color: rgba(255, 255, 255, 0.4); 22 | border-radius: 50%; 23 | opacity: 1; 24 | animation: ripple 600ms linear; 25 | } 26 | .content{ 27 | position: relative; 28 | z-index: 10; 29 | } 30 | @keyframes ripple { 31 | to { 32 | transform: scale(30); 33 | opacity: 0; 34 | } 35 | } -------------------------------------------------------------------------------- /src/components/custom/rippleButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './index.module.less' 3 | 4 | interface Props { 5 | children: any 6 | onClick?: () => void 7 | className?: string 8 | tabIndex?: number 9 | } 10 | export const RippleButton: React.FC = (props) => { 11 | const { children, onClick, className, tabIndex } = props 12 | const [coords, setCoords] = React.useState({ x: -1, y: -1 }) 13 | const [isRippling, setIsRippling] = React.useState(false) 14 | 15 | React.useEffect(() => { 16 | if (coords.x !== -1 && coords.y !== -1 && !isRippling) { 17 | setIsRippling(true) 18 | setTimeout(() => setIsRippling(false), 600) 19 | } else setIsRippling(false) 20 | }, [coords]) 21 | 22 | React.useEffect(() => { 23 | if (!isRippling) setCoords({ x: -1, y: -1 }) 24 | }, [isRippling]) 25 | 26 | return ( 27 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/dialogs/ChooseNoteDialog.tsx: -------------------------------------------------------------------------------- 1 | const ChooseNoteDialog = () => { 2 | 3 | } -------------------------------------------------------------------------------- /src/components/dialogs/DeleteDocumentDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import { Content, Footer } from '../base' 3 | 4 | const DeleteDocumentDialog = ({ onBack, onSubmit }: { onBack: Noop; onSubmit: Noop }) => { 5 | return ( 6 | 7 |
Delete file
8 |
Are you sure you want to delete this file?
9 |
You can recover it from bin later
10 | 11 |
12 |
13 |
14 | ) 15 | } 16 | 17 | export default DeleteDocumentDialog 18 | -------------------------------------------------------------------------------- /src/components/dialogs/NewDocumentDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import useCollectionStore from '@/stores/collection.store' 3 | import { useState } from 'react' 4 | import toast from 'react-hot-toast' 5 | import { ICollection } from '~/typings/data' 6 | import NewDocumentForm from '../form/newDocumentForm' 7 | import ChooseCollectionDialog from './ChooseCollectionDialog' 8 | 9 | const NewDocumentDialog = ({ onSubmit, onBack }: { onSubmit?: Noop; onBack?: Noop }) => { 10 | const [chosenCollection, setChosenCollection] = useState() 11 | const { addItemToCollection } = useCollectionStore() 12 | return ( 13 | <> 14 | {chosenCollection ? ( 15 | setChosenCollection(undefined)} 17 | onSubmit={doc => { 18 | addItemToCollection( 19 | { 20 | type: 'document', 21 | id: doc.id 22 | }, 23 | chosenCollection.id 24 | ).then( 25 | () => { 26 | onSubmit?.() 27 | toast.success('Successfully add document !') 28 | }, 29 | reject => { 30 | toast.error(reject) 31 | } 32 | ) 33 | }} 34 | collection={chosenCollection} 35 | /> 36 | ) : ( 37 | setChosenCollection(collection)} /> 38 | )} 39 | 40 | ) 41 | } 42 | 43 | export default NewDocumentDialog 44 | -------------------------------------------------------------------------------- /src/components/dialogs/NewItemDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useNavigate } from 'react-router-dom' 3 | import { Content, Icon } from '../base' 4 | import SwitchAnimation from '../base/animation/SwitchAnimation' 5 | import Menu from '../base/menu' 6 | import NewDocumentDialog from './NewDocumentDialog' 7 | 8 | const NewItemDialog = () => { 9 | const [item, setItem] = useState<'document' | null>(null) 10 | const nav = useNavigate() 11 | const renderChooseItemMenu = () => { 12 | return ( 13 | 14 | } onClick={() => setItem('document')}> 15 | Pdf 16 | 17 | } 20 | onClick={() => { 21 | nav('/editor') 22 | setItem(null) 23 | }} 24 | > 25 | Note 26 | 27 | 28 | ) 29 | } 30 | const renderItem = () => { 31 | switch (item) { 32 | case 'document': 33 | return setItem(null)} /> 34 | } 35 | } 36 | return ( 37 | 38 | 39 | {switchWrapper => (item ? switchWrapper(renderItem(), 'item') : switchWrapper(renderChooseItemMenu(), 'menu'))} 40 | 41 | 42 | ) 43 | } 44 | 45 | export default NewItemDialog 46 | -------------------------------------------------------------------------------- /src/components/dialogs/SearchDialog/AutoComplete.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from '@/components/base' 2 | import { css } from '@emotion/css' 3 | import { CSSProperties, FC, MouseEventHandler } from 'react' 4 | 5 | export const AutoComplete: FC<{ 6 | value: string 7 | onClick: MouseEventHandler 8 | }> = ({ value, onClick }) => { 9 | return ( 10 |
24 | {value} 25 |
26 | ) 27 | } 28 | const AutoCompleteList: FC<{ values: string[]; onClick: (value: string) => void; style?: CSSProperties }> = ({ 29 | values, 30 | onClick, 31 | style 32 | }) => { 33 | return ( 34 | 35 | {values.map(i => ( 36 | onClick(i)} /> 37 | ))} 38 | 39 | ) 40 | } 41 | 42 | export default AutoCompleteList 43 | -------------------------------------------------------------------------------- /src/components/dialogs/SettingDialog/index.tsx: -------------------------------------------------------------------------------- 1 | import { Content, TabPane, Tabs } from '@/components/base' 2 | import Select from '@/components/base/select' 3 | import { useTranslation } from 'react-i18next' 4 | import { useLocalStorage } from 'react-use' 5 | 6 | const SettingDialog = () => { 7 | const { i18n } = useTranslation() 8 | const [value, setValue, remove] = useLocalStorage('lang', undefined, { 9 | raw: true 10 | }) 11 | return ( 12 | 13 |
Language:
14 | 25 |
26 | ) 27 | } 28 | 29 | export default SettingDialog 30 | -------------------------------------------------------------------------------- /src/components/dialogs/switchAnimation.ts: -------------------------------------------------------------------------------- 1 | export const transition = { 2 | x: { type: 'spring', stiffness: 300, damping: 30 }, 3 | opacity: { duration: 0.2 } 4 | } 5 | export const variants = { 6 | enter: (direction: number) => { 7 | return { 8 | x: direction > 0 ? '100%' : '-100%', 9 | opacity: 0 10 | } 11 | }, 12 | center: { 13 | zIndex: 1, 14 | x: 0, 15 | opacity: 1 16 | }, 17 | exit: (direction: number) => { 18 | return { 19 | zIndex: 0, 20 | x: direction > 0 ? '100%' : '-100%', 21 | opacity: 0 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/dialogs/type.ts: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | 3 | export interface IDialog { 4 | onCancel?: Noop 5 | onSubmit?: Noop 6 | } 7 | -------------------------------------------------------------------------------- /src/components/documentDetail/index.tsx: -------------------------------------------------------------------------------- 1 | import useDocumentStore from '@/stores/document.store' 2 | import { useEffect, useState } from 'react' 3 | import { useParams } from 'react-router-dom' 4 | import { IPdfDocument } from '~/typings/data' 5 | import { Content } from '../base' 6 | import { TabPane, Tabs } from '../base/tabs' 7 | import Overview from './overview' 8 | 9 | const DocumentDetails = () => { 10 | const { id } = useParams() 11 | const [document, setDocument] = useState() 12 | const { getDocumentById } = useDocumentStore() 13 | useEffect(() => { 14 | const Id = parseInt(id) 15 | const d = getDocumentById(Id) 16 | setDocument(d) 17 | }, []) 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default DocumentDetails 30 | -------------------------------------------------------------------------------- /src/components/documentDetail/tsest.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/documentDetail/tsest.ts -------------------------------------------------------------------------------- /src/components/dragReposition/DragRepositionItem.tsx: -------------------------------------------------------------------------------- 1 | const DragRepositionItem = () =>{ 2 | return ( 3 |
4 | 5 |
6 | ) 7 | } -------------------------------------------------------------------------------- /src/components/dragReposition/DragRepositionWrapper.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/dragReposition/DragRepositionWrapper.tsx -------------------------------------------------------------------------------- /src/components/dragReposition/calculateItemCollide.ts: -------------------------------------------------------------------------------- 1 | import { Coordinates } from "./types" 2 | 3 | export const calculatePointerCollide = (pointer: Coordinates, objectCoord: Coordinates, tolerance = 0) => { 4 | if ( 5 | pointer.top > objectCoord.top && 6 | pointer.top < objectCoord.top + objectCoord.height && 7 | pointer.left > objectCoord.left && 8 | pointer.left < objectCoord.left + objectCoord.width 9 | ) { 10 | if ((pointer.left - objectCoord.left) / objectCoord.width < 0.1) { 11 | return 'left' 12 | } 13 | if ((objectCoord.left + objectCoord.width - pointer.left) / objectCoord.width < 0.1) { 14 | return 'right' 15 | } 16 | if ((pointer.top - objectCoord.top) / objectCoord.height < 0.5) { 17 | return 'top' 18 | } 19 | return 'bottom' 20 | } 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /src/components/dragReposition/index.module.less: -------------------------------------------------------------------------------- 1 | .ColumnListBlock{ 2 | width: 100%; 3 | align-self: center; 4 | max-width: 800px; 5 | display: flex; 6 | position: relative; 7 | } 8 | .ParagraphBlock{ 9 | width: 100%; 10 | max-width: 800px; 11 | padding-top: 1.4em; 12 | position: relative; 13 | } 14 | .ColumnBlock{ 15 | position: relative; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | .ColumnDivider{ 20 | position: relative; 21 | width: 40px; 22 | flex-grow: 0; 23 | flex-shrink: 0; 24 | &:first-child{ 25 | display: none; 26 | } 27 | } 28 | .DividerWrapper{ 29 | user-select: none; 30 | position: relative; 31 | width: 46px; 32 | flex-grow: 0; 33 | flex-shrink: 0; 34 | opacity: 0; 35 | transition: opacity 0.2s ease-in-out; 36 | &:first-child{ 37 | display: none; 38 | } 39 | &:hover{ 40 | opacity: 1; 41 | cursor: col-resize; 42 | } 43 | } 44 | .Divider{ 45 | position: absolute; 46 | top: 0px; 47 | left: 21px; 48 | width: 4px; 49 | height: 100%; 50 | background-color: rgba(55, 53, 47, 0.16); 51 | } -------------------------------------------------------------------------------- /src/components/dragReposition/types.ts: -------------------------------------------------------------------------------- 1 | export type Coordinates = { 2 | top: number 3 | left: number 4 | width?: number 5 | height?: number 6 | } 7 | -------------------------------------------------------------------------------- /src/components/dragReposition/utils/algorithm.ts: -------------------------------------------------------------------------------- 1 | // algorithm based on interpolation search 2 | // only search twice for 500 nodes 3 | export const searchBlock = (entries: [globalThis.Element, { height: number; top: number }][], distance: number) => { 4 | let left = 0 5 | let right = entries.length - 1 6 | let index = -1 7 | let searchCount = 0 8 | const isIn = ({ height, top }) => { 9 | if (distance > top && distance < height + top) { 10 | return true 11 | } 12 | return false 13 | } 14 | while (left <= right) { 15 | searchCount += 1 16 | // prevent dead loop 17 | if (searchCount > 1000) break 18 | const rangeDelta = entries[right][1].height + entries[right][1].top - entries[left][1].top 19 | const indexDelta = right - left 20 | const valueDelta = distance - entries[left][1].top 21 | if (valueDelta < 0) { 22 | index = -1 23 | break 24 | } 25 | if (!rangeDelta) { 26 | const entry = entries[left][1] 27 | index = isIn(entry) ? left : -1 28 | break 29 | } 30 | const middleIndex = left + Math.floor((valueDelta * indexDelta) / rangeDelta) 31 | if (!entries[middleIndex]) { 32 | return -1 33 | } 34 | if (isIn(entries[middleIndex][1])) { 35 | index = middleIndex 36 | break 37 | } 38 | if (entries[middleIndex][1].top + entries[middleIndex][1].height <= distance) { 39 | left = middleIndex + 1 40 | } else { 41 | right = middleIndex - 1 42 | } 43 | } 44 | if (index !== -1) { 45 | return entries[index][0] 46 | } 47 | return -1 48 | } 49 | -------------------------------------------------------------------------------- /src/components/dragReposition/utils/element.ts: -------------------------------------------------------------------------------- 1 | export const getElementId = (element: Element | null): number | undefined => { 2 | if (!element) { 3 | return 4 | } 5 | const id = element.getAttribute('id') 6 | if (id) { 7 | return parseInt(id) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/dragReposition/utils/position.ts: -------------------------------------------------------------------------------- 1 | import { MouseEvent } from "react" 2 | 3 | const determineDirection = () => {} 4 | 5 | export const getDistanceBetweenPointAndElement = (e: MouseEvent, element: Element) => { 6 | const { clientX: pointX, clientY: pointY } = e 7 | const rect = element.getBoundingClientRect() 8 | const { top, left, width, height } = rect 9 | return { 10 | top: pointY - top, 11 | left: pointX - left, 12 | right: left + width - pointX, 13 | bottom: top + height - pointY 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/dragReposition/utils/scroll.ts: -------------------------------------------------------------------------------- 1 | export const isScrollable = (element: HTMLElement) => { 2 | const hasScrollableContent = element.scrollHeight > element.clientHeight 3 | const overflowYStyle = window.getComputedStyle(element).overflowY 4 | const isOverflowHidden = overflowYStyle.indexOf('hidden') !== -1 5 | 6 | return hasScrollableContent && !isOverflowHidden 7 | } 8 | export const isFullyScrolled = (element: HTMLElement) => { 9 | if (element.scrollTop === 0) { 10 | return 'top' 11 | } 12 | if (Math.floor(element.scrollTop + element.offsetHeight) >= element.scrollHeight) { 13 | return 'bottom' 14 | } 15 | return false 16 | } 17 | export const getDistanceBetweenPointAndScrollableElement = (point: { x: number; y: number }, element: HTMLElement) => { 18 | const rect = element.getBoundingClientRect() 19 | return { 20 | left: point.x + element.scrollLeft - rect.left, 21 | top: point.y + element.scrollTop - rect.top 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/dragSelector/index.ts: -------------------------------------------------------------------------------- 1 | import { MouseSelectionProps } from './components/SelectionContainer' 2 | import { useSelectionContainer } from './hooks/useSelectionContainer' 3 | import { boxesIntersect } from './utils/boxes' 4 | import { 5 | Point, 6 | Box, 7 | SelectionBox, 8 | OnSelectionChange, 9 | MouseSelectionRef, 10 | } from './utils/types' 11 | import { isElementDraggable, isSelectionDisabled } from './utils/utils' 12 | 13 | export type { 14 | Point, 15 | Box, 16 | SelectionBox, 17 | OnSelectionChange, 18 | MouseSelectionRef, 19 | MouseSelectionProps, 20 | } 21 | 22 | export { 23 | useSelectionContainer, 24 | boxesIntersect, 25 | isElementDraggable, 26 | isSelectionDisabled, 27 | } 28 | -------------------------------------------------------------------------------- /src/components/dragSelector/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module '*.css' { 6 | const content: { [className: string]: string }; 7 | export default content; 8 | } 9 | 10 | interface SvgrComponent extends React.StatelessComponent> {} 11 | 12 | declare module '*.svg' { 13 | const svgUrl: string; 14 | const svgComponent: SvgrComponent; 15 | export default svgUrl; 16 | export { svgComponent as ReactComponent } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/dragSelector/utils/boxes.ts: -------------------------------------------------------------------------------- 1 | import { Box, Point } from './types'; 2 | 3 | /** This method returns true if two boxes intersects 4 | * @param boxA 5 | * @param boxB 6 | */ 7 | export const boxesIntersect = (boxA: Box, boxB: Box) => 8 | boxA.left <= boxB.left + boxB.width && 9 | boxA.left + boxA.width >= boxB.left && 10 | boxA.top <= boxB.top + boxB.height && 11 | boxA.top + boxA.height >= boxB.top; 12 | 13 | export const calculateSelectionBox = ({ startPoint, endPoint }: { startPoint: Point; endPoint: Point }) => ({ 14 | left: Math.min(startPoint.x, endPoint.x), 15 | top: Math.min(startPoint.y, endPoint.y), 16 | width: Math.abs(startPoint.x - endPoint.x), 17 | height: Math.abs(startPoint.y - endPoint.y), 18 | }); 19 | 20 | export const calculateBoxArea = (box: Box) => box.width * box.height; 21 | -------------------------------------------------------------------------------- /src/components/dragSelector/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number 3 | y: number 4 | } 5 | 6 | export interface Box { 7 | left: number 8 | top: number 9 | width: number 10 | height: number 11 | } 12 | 13 | export interface SelectionBox extends Box {} 14 | 15 | export type OnSelectionChange = (box: SelectionBox) => void 16 | 17 | export interface MouseSelectionRef { 18 | drawSelectionBox: OnSelectionChange 19 | clearSelectionBox: () => void 20 | getBoundingClientRect: () => DOMRect | undefined 21 | getParentBoundingClientRect: () => DOMRect | undefined 22 | } 23 | -------------------------------------------------------------------------------- /src/components/dragSelector/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // Check if current element is in draggable parent 2 | /** 3 | * @deprecated - use isSelectionDisabled 4 | * This method checks if current element is draggable - it's 'draggable' prop is true 5 | * It checks element itself and all its parent until draggable is true or element tagName is body or main 6 | * @param target 7 | */ 8 | export const isElementDraggable = (target: HTMLElement) => { 9 | let el = target; 10 | while (el.parentElement && el.tagName !== 'BODY' && el.tagName !== 'MAIN' && !el.dataset.draggable) { 11 | el = el.parentElement; 12 | } 13 | 14 | return el.dataset.draggable === 'true'; 15 | }; 16 | 17 | /** 18 | * This method checks if current element is draggable - it's 'draggable' prop is true 19 | * It checks element itself and all its parent until draggable is true or element tagName is body or main 20 | * @param target 21 | */ 22 | export const isSelectionDisabled = (target: HTMLElement) => { 23 | let el = target; 24 | while ( 25 | el.parentElement && 26 | el.tagName !== 'BODY' && 27 | el.tagName !== 'MAIN' && 28 | !el.dataset.draggable && 29 | !el.dataset.disableselect 30 | ) { 31 | el = el.parentElement; 32 | } 33 | 34 | return el.dataset.draggable === 'true' || el.dataset.disableselect === 'true'; 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/draggableBox/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../styles/common.module.less'; 2 | 3 | .rnd{ 4 | transition: opacity 0.2s linear,visibility 0.2s linear; 5 | z-index: 10000; 6 | } 7 | .draggableBoxWrapper{ 8 | .verticalCenterFlex; 9 | background-color: white; 10 | flex-direction: column; 11 | height: 100%; 12 | width: 100%; 13 | min-height: inherit; 14 | .defaultBorder; 15 | } 16 | .title{ 17 | font-size: 20px; 18 | padding-left: 10px; 19 | font-weight: 600; 20 | font-family: 'Times New Roman', Times, serif; 21 | } 22 | .draggableBoxHeader{ 23 | padding: 5px; 24 | display: flex; 25 | justify-content: space-between; 26 | width: 100%; 27 | border-bottom: @default-border; 28 | } 29 | .draggableBoxBody{ 30 | overflow: auto; 31 | padding: 10px; 32 | .iosStyleScrollBar; 33 | font-size: 16px; 34 | width: 100%; 35 | flex-grow: 1; 36 | transition: color 0.5s ease-in-out; 37 | color: rgba(31, 31, 31, 0.3); 38 | &::-webkit-scrollbar-thumb{ 39 | background: none; 40 | box-shadow: inset 0 0 0 7px; 41 | } 42 | &.hideScrollBar{ 43 | color: rgba(0, 0, 0 , 0); 44 | } 45 | } 46 | .body{ 47 | color: initial !important; 48 | height: 100%; 49 | } 50 | .icon{ 51 | display: flex !important; 52 | align-items: center; 53 | justify-content: center; 54 | } -------------------------------------------------------------------------------- /src/components/editor/components/dragHandle/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/editor/components/dragHandle/index.module.less -------------------------------------------------------------------------------- /src/components/editor/components/dragHandle/index.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import { Icon } from '@/components' 3 | import { AnimatePresence, motion } from 'framer-motion' 4 | import { FC, memo } from 'react' 5 | 6 | type DragHandleProps = { 7 | left?: number 8 | top?: number 9 | targetHeight?: number 10 | targetId?: number 11 | } 12 | 13 | type IDragHandle = FC< 14 | DragHandleProps & { 15 | visible: boolean 16 | onMouseDown: Noop 17 | onClick: Noop 18 | } 19 | > 20 | const DragHandle: IDragHandle = ({ left, top, targetHeight, visible, onMouseDown, onClick }) => { 21 | return ( 22 | 23 | {targetHeight && visible && ( 24 | 40 |
41 | 42 |
43 |
44 | )} 45 |
46 | ) 47 | } 48 | 49 | export default memo(DragHandle) -------------------------------------------------------------------------------- /src/components/editor/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Outline } from './outline' 2 | export { default as Mindmap } from './mindmap' 3 | -------------------------------------------------------------------------------- /src/components/editor/components/menu/DragHandleMenu.tsx: -------------------------------------------------------------------------------- 1 | import Menu from '@/components/base/menu' 2 | import useEventEmitter from '@/events/useEventEmitter' 3 | 4 | const DragHandleMenu = () => { 5 | const emitter = useEventEmitter() 6 | return ( 7 | 8 | emitter.emit('editor', { type: 'toggleThoughtLayer' })}> 9 | Add to mindmap 10 | 11 | emitter.emit('editor', { type: 'toggleThoughtLayer' })}> 12 | Add to mindmap 13 | 14 | 15 | ) 16 | } 17 | 18 | export default DragHandleMenu 19 | -------------------------------------------------------------------------------- /src/components/editor/components/menu/MindMapMenu.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/editor/components/menu/MindMapMenu.tsx -------------------------------------------------------------------------------- /src/components/editor/components/menu/SlashMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import { AnimatePopover, WithBorder, Content } from '@/components' 3 | import { PosHelper } from '@/components/contextMenu' 4 | import { PrettyInput } from '@/components/custom/input/pretty' 5 | import { FC, useEffect, useState } from 'react' 6 | 7 | interface SlashMenuProps { 8 | visible: boolean 9 | onClose: Noop 10 | } 11 | 12 | const SlashMenu: FC = ({ visible, onClose }) => { 13 | const [position, setPosition] = useState() 14 | const [search, setSearch] = useState('') 15 | 16 | useEffect(() => { 17 | if (visible) { 18 | setPosition(window.getSelection()?.getRangeAt(0)?.getBoundingClientRect()) 19 | } 20 | }, [visible]) 21 | return ( 22 | 28 | 29 | { 32 | setSearch(e.target.value) 33 | }} 34 | /> 35 | 36 | 37 | } 38 | visible={visible} 39 | onClickOutside={onClose} 40 | > 41 | 42 | 43 | ) 44 | } 45 | 46 | export default SlashMenu 47 | -------------------------------------------------------------------------------- /src/components/editor/components/mindmap/InsertNodeForm.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import Form from '@/components/base/form' 3 | import { FC, useState } from 'react' 4 | 5 | const InsertNodeForm: FC<{ 6 | onBack: Noop 7 | onSubmit: (node: { id: number; type: 'common'; content: string; backgroundColor: string }) => void 8 | }> = ({ onBack, onSubmit }) => { 9 | const [node, setNode] = useState({ 10 | id: 123, 11 | type: 'common', 12 | backgroundColor: 'white', 13 | content: '' 14 | }) 15 | return ( 16 |
{ 20 | onSubmit(node) 21 | }} 22 | > 23 | setNode({ ...node, content: value })}> 24 | setNode({ ...node, backgroundColor: color })} /> 25 | 26 | ) 27 | } 28 | 29 | export default InsertNodeForm 30 | -------------------------------------------------------------------------------- /src/components/editor/components/mindmap/Toolbar.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import { Toolbar } from '@/components/base' 3 | import { FC } from 'react' 4 | import styles from './index.module.less' 5 | 6 | const MindmapToolbar: FC<{ 7 | onZoomIn: Noop 8 | onZoomOut: Noop 9 | onFullScreen: Noop 10 | onOffScreen: Noop 11 | onAddNode: Noop 12 | onDelete: Noop 13 | onClose: Noop 14 | }> = ({ onZoomIn, onZoomOut, onFullScreen, onOffScreen, onAddNode, onDelete, onClose }) => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default MindmapToolbar 29 | -------------------------------------------------------------------------------- /src/components/editor/components/mindmap/index.module.less: -------------------------------------------------------------------------------- 1 | .icon{ 2 | background-color: transparent; 3 | } -------------------------------------------------------------------------------- /src/components/editor/components/mindmap/index.tsx: -------------------------------------------------------------------------------- 1 | import { DragEventHandler, FC } from 'react' 2 | import ReactFlow, { Node, Edge, OnNodesChange, OnEdgesChange, ReactFlowProps } from 'react-flow-renderer' 3 | 4 | type FlowProps = ReactFlowProps 5 | 6 | const ThoughtFlow: FC = ({ ...rest }) => { 7 | return 8 | } 9 | 10 | export default ThoughtFlow 11 | -------------------------------------------------------------------------------- /src/components/editor/components/outline/index.module.less: -------------------------------------------------------------------------------- 1 | .outline{ 2 | cursor: pointer; 3 | margin-bottom: 6px; 4 | border-radius: 5px; 5 | transition: background 0.1s ease-in-out; 6 | &.headingOne{ 7 | padding-left: 8px; 8 | font-weight: 600; 9 | } 10 | &.headingTwo{ 11 | padding-left: 25px; 12 | font-weight: 500; 13 | } 14 | &.headingThree{ 15 | padding-left: 42px; 16 | font-weight: 500; 17 | } 18 | &.headingFour{ 19 | padding-left: 76px; 20 | font-weight: 400; 21 | } 22 | &:hover{ 23 | background-color: rgba(55,54,47,0.08); 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/editor/components/overlay/index.module.less: -------------------------------------------------------------------------------- 1 | .OverlayContainer{ 2 | position: fixed; 3 | inset: 0px; 4 | z-index: 999; 5 | pointer-events: none; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /src/components/editor/components/toolbar/index.module.less: -------------------------------------------------------------------------------- 1 | .toolbar{ 2 | display: flex; 3 | position: sticky !important; 4 | top: 0; 5 | background-color: white; 6 | z-index: 999; 7 | } 8 | .image{ 9 | background-size: 6000% 6000%; 10 | } -------------------------------------------------------------------------------- /src/components/editor/consts/hotkeys.ts: -------------------------------------------------------------------------------- 1 | const HOTKEYS = { 2 | 'mod+b': 'bold', 3 | 'mod+i': 'italic', 4 | 'mod+u': 'underline', 5 | 'mod+`': 'code' 6 | } 7 | 8 | export default HOTKEYS 9 | -------------------------------------------------------------------------------- /src/components/editor/consts/shortcuts.ts: -------------------------------------------------------------------------------- 1 | const SHORTCUTS = { 2 | '*': 'list-item', 3 | '-': 'list-item', 4 | '+': 'list-item', 5 | '>': 'block-quote', 6 | '#': 'heading-one', 7 | '##': 'heading-two', 8 | '###': 'heading-three', 9 | '####': 'heading-four', 10 | '#####': 'heading-five', 11 | '######': 'heading-six' 12 | } 13 | 14 | export default SHORTCUTS 15 | -------------------------------------------------------------------------------- /src/components/editor/elements/Emoji.tsx: -------------------------------------------------------------------------------- 1 | import getEmoji from '@/utils/emoji' 2 | import { FC, useEffect, useState } from 'react' 3 | import { RenderElementProps, useFocused, useSelected } from 'slate-react' 4 | 5 | const Emoji: FC = ({ element, children, attributes }) => { 6 | const [emoji, setEmoji] = useState() 7 | useEffect(() => { 8 | getEmoji(element.name).then(data => setEmoji(data)) 9 | }, []) 10 | const InlineChromiumBugfix = () => ( 11 | 12 | ${String.fromCodePoint(160) /* Non-breaking space */} 13 | 14 | ) 15 | return ( 16 | 17 | 18 | 31 | {children} 32 | 33 | 34 | ) 35 | } 36 | 37 | export default Emoji 38 | -------------------------------------------------------------------------------- /src/components/editor/elements/Spacer.tsx: -------------------------------------------------------------------------------- 1 | import { RenderElementProps, useFocused, useSelected } from 'slate-react' 2 | import styles from './index.module.less' 3 | export const Spacer: React.FC = props => { 4 | const selected = useSelected() 5 | const focused = useFocused() 6 | return ( 7 | 8 | {props.children} 9 |

10 |
11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/components/editor/elements/block/index.module.less: -------------------------------------------------------------------------------- 1 | .ColumnListBlock{ 2 | width: 100%; 3 | align-self: center; 4 | max-width: 800px; 5 | display: flex; 6 | position: relative; 7 | } 8 | .Selected{ 9 | background: #8590ae2d; 10 | border-radius: 4px; 11 | } 12 | .ParagraphBlock{ 13 | width: 100%; 14 | max-width: 800px; 15 | position: relative; 16 | word-break: break-all; 17 | padding: 3px 2px; 18 | } 19 | .HeadingBlock{ 20 | width: 100%; 21 | max-width: 800px; 22 | position: relative; 23 | margin-top: 2em; 24 | margin-bottom: 4px; 25 | &:first-child{ 26 | margin-top: 0; 27 | } 28 | } 29 | .ColumnBlock{ 30 | position: relative; 31 | display: flex; 32 | flex-direction: column; 33 | transition: width 1s ease-out; 34 | } 35 | .ColumnDivider{ 36 | position: relative; 37 | width: 40px; 38 | flex-grow: 0; 39 | flex-shrink: 0; 40 | &:first-child{ 41 | display: none; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/editor/elements/elementBuilder.ts: -------------------------------------------------------------------------------- 1 | export interface ElementBuilder{ 2 | build: (source: any) => T 3 | } 4 | -------------------------------------------------------------------------------- /src/components/editor/elements/heading/index.module.less: -------------------------------------------------------------------------------- 1 | .fold{ 2 | outline: none; 3 | } 4 | .extra{ 5 | opacity: 0; 6 | position: absolute; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | left: -5px; 11 | transform: translateX(-100%); 12 | top: 0; 13 | bottom: 0; 14 | font-size: 14px; 15 | transition: all 0.5s; 16 | font-weight: normal; 17 | &.folded{ 18 | opacity: 1; 19 | } 20 | } 21 | .heading{ 22 | position: relative; 23 | margin: 0; 24 | &:hover{ 25 | .extra{ 26 | opacity: 1; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/components/editor/elements/highlight/index.module.less: -------------------------------------------------------------------------------- 1 | .selectedText{ 2 | font-size: 12px; 3 | } 4 | .selectedTextContainer{ 5 | background-color: #f3f5f6; 6 | padding: 6px 10px; 7 | border-radius: 5px; 8 | } -------------------------------------------------------------------------------- /src/components/editor/elements/highlight/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReadMore, Content } from '@/components' 2 | import { FC, useEffect, useState } from 'react' 3 | import { RenderElementProps, useFocused, useSelected } from 'slate-react' 4 | import { HighlighElement } from '../../types/outerElementTypes' 5 | import styles from './index.module.less' 6 | import { AnimatePresence, motion } from 'framer-motion' 7 | const Highlight: FC = ({ element, children, attributes }) => { 8 | const highlightElement = element as HighlighElement 9 | const [showToolbar, setShowToolbar] = useState(false) 10 | const selected = useSelected() 11 | useEffect(() => { 12 | if (!selected) { 13 | setShowToolbar(false) 14 | } 15 | }, [selected]) 16 | return ( 17 |
18 | { 20 | // setShowToolbar(true) 21 | }} 22 | className={styles.selectedTextContainer} 23 | > 24 | {highlightElement.selectedText} 25 | 26 | {showToolbar ? ( 27 | 33 | 34 | ) : undefined} 35 | 36 | 37 | {children} 38 |
39 | ) 40 | } 41 | export default Highlight 42 | -------------------------------------------------------------------------------- /src/components/editor/elements/index.module.less: -------------------------------------------------------------------------------- 1 | .spacer{ 2 | min-width: 1px; 3 | font-size: 1em; 4 | height: 1em; 5 | display: inline; 6 | text-indent: 0; 7 | vertical-align: baseline; 8 | &:first-child{ 9 | align-self: flex-start; 10 | } 11 | &:last-child{ 12 | align-self: flex-end; 13 | } 14 | } -------------------------------------------------------------------------------- /src/components/editor/elements/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/editor/elements/index.ts -------------------------------------------------------------------------------- /src/components/editor/elements/paragraph/index.module.less: -------------------------------------------------------------------------------- 1 | .selectedEmpty{ 2 | position: relative; 3 | &::after{ 4 | content: 'Input something'; 5 | color: #aaa; 6 | cursor: text; 7 | position: absolute; 8 | top: 50%; 9 | transform: translateY(-50%); 10 | user-select: none; 11 | pointer-events: none; 12 | white-space: nowrap; 13 | } 14 | } -------------------------------------------------------------------------------- /src/components/editor/elements/subPage/index.module.less: -------------------------------------------------------------------------------- 1 | .subPageLinkWrapper{ 2 | padding: 3px 4px; 3 | font-size: 15px; 4 | font-weight: 500; 5 | background-color: rgba(0,0,0,0.04); 6 | transition: background-color 0.3s ease; 7 | cursor: pointer; 8 | border-radius: 3px; 9 | &:hover{ 10 | background-color: rgba(0,0,0,0.08); 11 | > span{ 12 | text-decoration: underline; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/components/editor/elements/subPage/index.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from '@/components/base' 2 | import Icon from '@/components/base/Icon' 3 | import useEventEmitter from '@/events/useEventEmitter' 4 | import useNoteStore from '@/stores/note.store' 5 | import { useEffect, useState } from 'react' 6 | import { RenderElementProps } from 'slate-react' 7 | import { INote } from '~/typings/data' 8 | import { SubPageElement } from '../../types/outerElementTypes' 9 | import styles from './index.module.less' 10 | 11 | const SubPage = ({ children, attributes, element }: RenderElementProps) => { 12 | const ele = element as SubPageElement 13 | const { getNoteById } = useNoteStore() 14 | const [subPageNote, setSubPageNote] = useState() 15 | const emitter = useEventEmitter() 16 | useEffect(() => { 17 | getNoteById(ele.originId).then(note => setSubPageNote(note)) 18 | }, [getNoteById]) 19 | const handleLinkClick = () => { 20 | emitter.emit('editor',{ 21 | type: 'insertTab', 22 | data: { 23 | isNew: false, 24 | noteId: ele.originId 25 | } 26 | }) 27 | } 28 | return ( 29 |
30 | 31 | 32 | {subPageNote?.title || 'Untitled'} 33 | {children} 34 | 35 |
36 | ) 37 | } 38 | 39 | export default SubPage 40 | -------------------------------------------------------------------------------- /src/components/editor/elements/thought/index.module.less: -------------------------------------------------------------------------------- 1 | .thoughtContainer{ 2 | padding: 4px 6px; 3 | display: flex; 4 | flex: auto; 5 | box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; 6 | background-color: white; 7 | border-radius: 8px; 8 | &.selected{ 9 | box-shadow: rgba(3, 102, 214, 0.3) 0px 0px 0px 3px; 10 | } 11 | } 12 | .title{ 13 | font-size: 20px; 14 | font-weight: bold; 15 | margin-right: 6px; 16 | } 17 | .thoughtInnerContainer{ 18 | margin: 4px; 19 | } -------------------------------------------------------------------------------- /src/components/editor/hooks/useEditor.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | import { ReactEditor } from 'slate-react' 3 | 4 | const editor = atom(null) 5 | export const useEditor = () => { 6 | return useAtom(editor) 7 | } 8 | -------------------------------------------------------------------------------- /src/components/editor/hooks/useEditorValue.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | import { Descendant } from 'slate' 3 | 4 | const markdownEditorValue = atom([{ type: 'paragraph', children: [{ text: '' }] }]) 5 | 6 | const useEditorValue = () => { 7 | return useAtom(markdownEditorValue) 8 | } 9 | export default useEditorValue 10 | -------------------------------------------------------------------------------- /src/components/editor/hooks/useEditorVisible.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | 3 | const editorVisible = atom(false) 4 | export const useEditorVisible = () => { 5 | return useAtom(editorVisible) 6 | } 7 | -------------------------------------------------------------------------------- /src/components/editor/hooks/useIsDragging.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | 3 | const isDraggingAtom = atom(false) 4 | 5 | const useIsDragging = () => useAtom(isDraggingAtom) 6 | 7 | export default useIsDragging 8 | -------------------------------------------------------------------------------- /src/components/editor/hooks/useSelectedEditorRef.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtomValue } from 'jotai' 3 | import React from 'react' 4 | import { MutableRefObject } from 'react' 5 | import { IEditorRef } from '../Editor' 6 | 7 | const selectedEditorRefAtom = atom>(React.createRef()) 8 | 9 | export const useSelectedEditorRef = () => useAtomValue(selectedEditorRefAtom, GlobalScope) 10 | -------------------------------------------------------------------------------- /src/components/editor/plugins/withEmitter.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from '@/events/eventEmitter' 2 | import { CustomEditor } from '../types' 3 | 4 | const withEmitter = (editor: CustomEditor, emitter: EventEmitter) => { 5 | editor.emitter = emitter 6 | return editor 7 | } 8 | 9 | export default withEmitter -------------------------------------------------------------------------------- /src/components/editor/plugins/withVoid.ts: -------------------------------------------------------------------------------- 1 | const withVoid = (editor: CustomEditor) => { 2 | const { isVoid,isInline } = editor 3 | const checkIfVoidComponents = (element: CustomElement) => { 4 | switch (element.type) { 5 | case 'textSelectionCard': 6 | return true 7 | case 'spacer': 8 | return false 9 | case 'highlight': 10 | return true 11 | case 'emoji': 12 | return true 13 | case 'image': 14 | return true 15 | case 'thought': 16 | return true 17 | case 'subPage': 18 | return true 19 | default: 20 | return false 21 | } 22 | } 23 | editor.isInline = element => ['emoji'].includes(element.type) || isInline(element) 24 | editor.isVoid = element => { 25 | return checkIfVoidComponents(element) ? true : isVoid(element) 26 | } 27 | return editor 28 | } 29 | 30 | export default withVoid 31 | -------------------------------------------------------------------------------- /src/components/editor/types/baseElementTypes.ts: -------------------------------------------------------------------------------- 1 | import { Descendant } from 'slate' 2 | import { EmptyText } from './baseTypes' 3 | 4 | export interface HoleElement { 5 | type: 'hole' 6 | children: Descendant[] 7 | } 8 | export interface SpacerElement { 9 | type: 'spacer' 10 | children: EmptyText[] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/editor/types/baseTypes.ts: -------------------------------------------------------------------------------- 1 | export interface ElementWithId { 2 | id: number 3 | } 4 | export interface EmptyText { 5 | text: string 6 | } 7 | export interface CustomText { 8 | bold?: boolean 9 | italic?: boolean 10 | code?: boolean 11 | text: string 12 | } 13 | -------------------------------------------------------------------------------- /src/components/editor/types/index.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from '@/events/eventEmitter' 2 | import { BaseEditor } from 'slate' 3 | import { HistoryEditor } from 'slate-history' 4 | import { ReactEditor } from 'slate-react' 5 | import { CustomText, EmptyText } from './baseTypes' 6 | import { CommonElement } from './commonElementTypes' 7 | import { OuterElement } from './outerElementTypes' 8 | 9 | export type CustomEditor = BaseEditor & 10 | ReactEditor & 11 | HistoryEditor & { 12 | emitter?: EventEmitter 13 | } 14 | 15 | export type CustomElement = CommonElement | OuterElement 16 | 17 | declare module 'slate' { 18 | interface CustomTypes { 19 | Editor: CustomEditor 20 | Element: CustomElement & { hide: boolean; id: number } 21 | Text: CustomText | EmptyText 22 | } 23 | } 24 | 25 | export type { OuterElement, CommonElement } 26 | -------------------------------------------------------------------------------- /src/components/editor/types/outerElementTypes.ts: -------------------------------------------------------------------------------- 1 | import { Descendant } from 'slate' 2 | import { ElementWithId, EmptyText } from './baseTypes' 3 | 4 | export interface IOuterElement extends ElementWithId { 5 | /** 6 | * Origin id represents the id which the outer element has in its original place 7 | * For example: The id highlight has in pdf note 8 | */ 9 | originId: number 10 | } 11 | 12 | export interface ThoughtElement extends IOuterElement { 13 | type: 'thought' 14 | title: string 15 | content: string 16 | children: EmptyText[] 17 | } 18 | 19 | export interface HighlighElement extends IOuterElement { 20 | type: 'highlight' 21 | selectedText: string 22 | title: string 23 | content: string 24 | children: EmptyText[] 25 | } 26 | export interface SubPageElement extends IOuterElement { 27 | type: 'subPage' 28 | iconName?: string 29 | } 30 | export type OuterElement = ThoughtElement | HighlighElement | SubPageElement 31 | -------------------------------------------------------------------------------- /src/components/editor/utils/database/deserialize.ts: -------------------------------------------------------------------------------- 1 | export const deserializeEditor = (root, arr: []) => { 2 | const res = [] 3 | const rootNodesId = root.subBlockIds 4 | const map = arr.reduce((res, v) => ((res[v.id] = v), res), {}) 5 | for (const item of arr) { 6 | delete item.noteId 7 | delete item.plain 8 | if (!item.parentId) { 9 | const idx = rootNodesId.findIndex(i => i === item.id) 10 | if (idx >= 0) { 11 | res[idx] = item 12 | } 13 | 14 | continue 15 | } 16 | if (item.parentId in map) { 17 | const parent = map[item.parentId] 18 | parent.children = parent.children || [] 19 | const subBlockIds = parent.subBlockIds 20 | const idx = subBlockIds.findIndex(i => i === item.id) 21 | if (idx >= 0) { 22 | parent.children[idx] = item 23 | } 24 | } 25 | } 26 | return res 27 | } 28 | -------------------------------------------------------------------------------- /src/components/editor/utils/database/serialize.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Element, Node } from 'slate' 2 | import { CustomEditor } from '../../types' 3 | 4 | export const serializeEditor = (node: Element, editor: CustomEditor, result: []) => { 5 | if (node.children?.length && !Editor.isEditor(node) && !Editor.isInline(editor, node)) { 6 | result.push(node) 7 | } 8 | if (node.type === 'hole') { 9 | return 10 | } 11 | node.children?.forEach(i => { 12 | node.subBlockIds = node.subBlockIds || [] 13 | i.id && node.subBlockIds.push(i.id) 14 | const cloned = structuredClone(i) 15 | cloned.plain = Node.string(cloned) 16 | if (node.id) { 17 | cloned.parentId = node.id 18 | } 19 | serializeEditor(cloned, editor, result) 20 | }) 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /src/components/editor/utils/positions/caret.ts: -------------------------------------------------------------------------------- 1 | export const getCaretGlobalPosition = () => { 2 | if(window.getSelection()?.rangeCount === 0){ 3 | return 4 | } 5 | const r = window.getSelection()?.getRangeAt(0) 6 | const node = r?.startContainer 7 | const offset = r?.startOffset 8 | const scrollOffset = { x: window.scrollX, y: window.scrollY } 9 | let rect, r2 10 | if (offset > 0) { 11 | r2 = document.createRange() 12 | r2.setStart(node, offset - 1) 13 | r2.setEnd(node, offset) 14 | rect = r2.getBoundingClientRect() 15 | return { left: rect.right + scrollOffset.x, top: rect.bottom + scrollOffset.y } 16 | }else{ 17 | r2 = document.createRange() 18 | r2.setStart(node,offset) 19 | r2.setEnd(node,offset) 20 | rect = r2.getBoundingClientRect() 21 | return {left: rect.right + scrollOffset.x,top: rect.bottom + scrollOffset.y} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/editor/utils/positions/index.ts: -------------------------------------------------------------------------------- 1 | import { BlockPosition } from './block' 2 | import { SpacerPosition } from './spacer' 3 | export const Positions = { 4 | ...BlockPosition, 5 | ...SpacerPosition 6 | } 7 | -------------------------------------------------------------------------------- /src/components/editor/utils/positions/scroll.ts: -------------------------------------------------------------------------------- 1 | export function scrollToTarget(target: HTMLElement | number, containerEl: HTMLElement) { 2 | // Moved up here for readability: 3 | const isElement = target && target.nodeType === 1, 4 | isNumber = Object.prototype.toString.call(target) === '[object Number]' 5 | 6 | if (isElement) { 7 | containerEl.scrollTop = target.offsetTop 8 | } else if (isNumber) { 9 | containerEl.scrollTop = target 10 | } else if (target === 'bottom') { 11 | containerEl.scrollTop = containerEl.scrollHeight - containerEl.offsetHeight 12 | } else if (target === 'top') { 13 | containerEl.scrollTop = 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/editor/utils/positions/spacer.ts: -------------------------------------------------------------------------------- 1 | import { Editor, Path, Range } from 'slate' 2 | import { CustomEditor } from '../../customTypes' 3 | 4 | export interface ISpacerPosition { 5 | isStartSpacer: (editor: CustomEditor) => boolean | undefined 6 | isEndSpacer: (editor: CustomEditor) => boolean | undefined 7 | } 8 | 9 | export const SpacerPosition: ISpacerPosition = { 10 | isStartSpacer: editor => { 11 | const { selection } = editor 12 | const match = Editor.above(editor, { 13 | match: n => Editor.isBlock(editor, n) 14 | }) 15 | if (match && selection && Range.isCollapsed(selection)) { 16 | const [block, path] = match 17 | if (block.type === 'spacer') { 18 | if (Editor.isStart(editor, selection.anchor, Path.parent(path))) { 19 | return true 20 | } 21 | } 22 | } 23 | }, 24 | isEndSpacer: editor => { 25 | const { selection } = editor 26 | const match = Editor.above(editor, { 27 | match: n => Editor.isBlock(editor, n) 28 | }) 29 | if (match && selection && Range.isCollapsed(selection)) { 30 | const [block, path] = match 31 | if (block.type === 'spacer') { 32 | if (Editor.isEnd(editor, selection.anchor, Path.parent(path))) { 33 | return true 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/form/NewCollectionForm.tsx: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import useCollectionStore from '@/stores/collection.store' 3 | import { FC, useEffect, useRef } from 'react' 4 | import { ICollection } from '~/typings/data' 5 | import { Form } from '../base' 6 | import CollectionItem from '../collection/CollectionItem' 7 | 8 | interface NewCollectionFormProps { 9 | parentCollection?: ICollection | null 10 | onAddCollection?: (collection: ICollection) => void 11 | onBack?: Noop 12 | } 13 | const NewCollectionForm: FC = ({ parentCollection, onAddCollection, onBack }) => { 14 | const collection = useRef({ parentId: parentCollection?.id }) 15 | collection.current.parentId = parentCollection?.id 16 | const { addCollection } = useCollectionStore() 17 | 18 | return ( 19 |
{ 23 | const c = addCollection(collection.current) 24 | onAddCollection?.(c) 25 | }} 26 | onBack={onBack} 27 | > 28 | (collection.current.name = value)} 34 | > 35 | Under: {parentCollection ? : 'Root'} 36 |
37 | ) 38 | } 39 | 40 | export default NewCollectionForm 41 | -------------------------------------------------------------------------------- /src/components/header/scrollable/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/header/scrollable/index.module.less -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Icon } from './base/Icon' 2 | export * from './base' 3 | export { default as ColorPicker } from './picker/colorPicker' 4 | export { default as IconPicker } from './picker/iconPicker' 5 | export { default as Switch } from './base/Switch' 6 | export { default as ReadMore } from './base/ReadMore' -------------------------------------------------------------------------------- /src/components/menus/CollectionMenu.tsx: -------------------------------------------------------------------------------- 1 | const CollectionMenu = () =>{ 2 | 3 | } -------------------------------------------------------------------------------- /src/components/menus/NewDocumentMenu.tsx: -------------------------------------------------------------------------------- 1 | import Icon from '@/components/base/Icon' 2 | import Modal from '@/components/base/modal' 3 | import { useRef, useState } from 'react' 4 | import { useNavigate } from 'react-router-dom' 5 | import Menu from '../base/menu' 6 | import NewDocumentDialog from '../dialogs/NewDocumentDialog' 7 | 8 | const NewDocumentMenu = () => { 9 | const [newPdfModalOpen, setNewPdfModalOpen] = useState(false) 10 | return ( 11 | <> 12 | setNewPdfModalOpen(false)}> 13 | setNewPdfModalOpen(false)} onBack={() => setNewPdfModalOpen(false)} /> 14 | 15 | 16 | setNewPdfModalOpen(true)} type="button" icon={}> 17 | Pdf 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | export default NewDocumentMenu 25 | -------------------------------------------------------------------------------- /src/components/menus/PdfContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { FC, MouseEvent } from 'react' 2 | import { useContextMenu } from '../contextMenu/ContextMenuProvider' 3 | import Menu from '../base/menu' 4 | 5 | interface Props { 6 | onAddThought?: (mousePos) => void 7 | onShouldCloseContextMenu: () => void 8 | } 9 | const PdfContextMenu: FC = ({ onShouldCloseContextMenu, onAddThought }) => { 10 | const mousePos = useContextMenu() 11 | const handleAddThought = (e: MouseEvent) => { 12 | onAddThought?.(mousePos) 13 | onShouldCloseContextMenu() 14 | e.stopPropagation() 15 | } 16 | return ( 17 | 18 | 19 | Thought 20 | 21 | 22 | ) 23 | } 24 | export default PdfContextMenu 25 | -------------------------------------------------------------------------------- /src/components/pdf/components/abstract/index.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/pdf/components/abstract/index.tsx -------------------------------------------------------------------------------- /src/components/pdf/components/annotation/Link.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from '@/components/base' 2 | import { MouseEventHandler } from 'react' 3 | import styles from './index.module.less' 4 | 5 | const Link = ({ onClick }: { onClick: MouseEventHandler }) => { 6 | return 7 | } 8 | 9 | export default Link 10 | -------------------------------------------------------------------------------- /src/components/pdf/components/annotation/index.module.less: -------------------------------------------------------------------------------- 1 | .link{ 2 | transition: background 0.3s ease; 3 | cursor: pointer; 4 | &:hover{ 5 | background: #8590ae43; 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/pdf/components/label/index.module.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/components/pdf/components/label/index.module.less -------------------------------------------------------------------------------- /src/components/pdf/components/page/index.module.less: -------------------------------------------------------------------------------- 1 | .page{ 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | margin: 20px; 6 | } -------------------------------------------------------------------------------- /src/components/pdf/components/page/mouseSelection.tsx: -------------------------------------------------------------------------------- 1 | import { OnSelectionChange, useSelectionContainer, OnSelectionEnd } from '@/components/dragSelector' 2 | import React, { FC } from 'react' 3 | 4 | export interface MouseSelectionProps { 5 | onSelectionChange: OnSelectionChange 6 | eventsElement: HTMLElement | Window | null 7 | onSelectionEnd: () => void 8 | } 9 | 10 | const DragSelection: FC = ({ onSelectionChange, eventsElement, onSelectionEnd }) => { 11 | const { DragSelection } = useSelectionContainer({ 12 | eventsElement, 13 | onSelectionChange, 14 | onSelectionEnd, 15 | selectionProps: { 16 | style: { 17 | border: '2px dashed purple', 18 | borderRadius: 2, 19 | opacity: 0.5, 20 | position: 'absolute', 21 | zIndex: 999999 22 | } 23 | } 24 | }) 25 | 26 | return 27 | } 28 | 29 | export default React.memo(DragSelection) 30 | -------------------------------------------------------------------------------- /src/components/pdf/components/sidebar/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../../styles/common.module.less"; 2 | 3 | .searchContainer{ 4 | background-color: #f3f5f6; 5 | padding: 4px 12px; 6 | flex-basis: 60%; 7 | flex-shrink: 0; 8 | flex-grow: 1; 9 | border-radius: 4px; 10 | margin-right: 20px; 11 | transition: all 0.2s ease-in-out; 12 | &.focused{ 13 | box-shadow: 0 0 3px 1px #8590A6; 14 | } 15 | } 16 | .statusBar{ 17 | color: #8590A6; 18 | font-size: 12px; 19 | >div{ 20 | margin-right: 6px; 21 | } 22 | >span{ 23 | margin-right: 10px; 24 | } 25 | } 26 | .labelItem{ 27 | position: relative; 28 | padding: 10px 16px 10px 0; 29 | &::after{ 30 | position: absolute; 31 | left: 0; 32 | top: 0; 33 | left: 0; 34 | right: 0; 35 | margin: 0 20px; 36 | display: block; 37 | border-bottom: 1px solid #f6f6f6; 38 | content: ''; 39 | } 40 | } 41 | .noteInfoContainer{ 42 | height: 100%; 43 | overflow: hidden; 44 | padding-right: 20px; 45 | } 46 | .resultContainer{ 47 | height: 100%; 48 | overflow-x: hidden; 49 | overflow-y: auto; 50 | .iosStyleScrollBar; 51 | padding: 10px 0 50px 0; 52 | } -------------------------------------------------------------------------------- /src/components/pdf/components/statusbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from 'react' 2 | 3 | interface Props { 4 | style?: React.CSSProperties 5 | className?: string 6 | } 7 | export const StatusBar: FC = ({ progress = 0, ...rest }) => { 8 | return
9 | } 10 | -------------------------------------------------------------------------------- /src/components/pdf/components/toolbar/Translate.tsx: -------------------------------------------------------------------------------- 1 | import { googleTranslate } from '@/utils/translate/googleTranslate' 2 | import { Content } from '@/components' 3 | import { FC } from 'react' 4 | import styles from './index.module.less' 5 | import useSWRImmutable from 'swr/immutable' 6 | 7 | const Translate: FC<{ translateInput?: string }> = ({ translateInput }) => { 8 | const normalizedInput = translateInput?.replace(/\n/g, '') 9 | const fetcher = () => { 10 | if (normalizedInput) { 11 | return googleTranslate(normalizedInput, 'en', 'zh-CN') 12 | } 13 | } 14 | const { data, error } = useSWRImmutable(`/api/translate/${normalizedInput}`, fetcher) 15 | console.log(data) 16 | return ( 17 | 18 | 19 | Provided By Google Translate 20 | 21 | 22 |
{translateInput}
23 |
24 | 25 | TranslateResult 26 | 27 | 28 | {data ? ( 29 |
{data.data.trans.paragraphs[0]}
30 | ) : error ? ( 31 |
Some errors happened
32 | ) : ( 33 |
loading
34 | )} 35 |
36 |
37 | ) 38 | } 39 | 40 | export default Translate -------------------------------------------------------------------------------- /src/components/pdf/components/toolbar/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/common.module.less'; 2 | .title{ 3 | font-size: 22px; 4 | font-weight: 500; 5 | margin-bottom: 10px; 6 | color: @font-black-color; 7 | } 8 | .subtitle{ 9 | color: @font-light-grey-color; 10 | font-weight: 500; 11 | font-size: 12px; 12 | } 13 | .thumbnail{ 14 | width: 180px; 15 | height: 210px; 16 | margin-right: 20px; 17 | border-radius: 12px; 18 | } 19 | .extract{ 20 | color: @font-black-color; 21 | font-weight: 500; 22 | font-size: 16px; 23 | overflow-y: auto; 24 | overflow-x: hidden; 25 | min-width: 500px; 26 | } 27 | .longText{ 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | display: block; 31 | -webkit-box-orient: vertical; 32 | display: -webkit-box; 33 | -webkit-line-clamp: 4; 34 | word-break: break-all; 35 | color: @font-black-color; 36 | } 37 | .translateDivider{ 38 | margin: 10px 0; 39 | font-size: 16px; 40 | font-weight: 500; 41 | color: @font-light-grey-color; 42 | } 43 | .disambiguationContainer{ 44 | max-height: 300px; 45 | width: 800px; 46 | overflow-y: auto; 47 | color: black; 48 | .iosStyleScrollBar; 49 | & a{ 50 | color: #8590ae; 51 | &:hover{ 52 | text-decoration: underline; 53 | } 54 | } 55 | .line{ 56 | color: black; 57 | font-weight: 600; 58 | font-size: 18px; 59 | } 60 | } -------------------------------------------------------------------------------- /src/components/pdf/components/toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEditorVisible } from '@/components/editor/hooks/useEditorVisible' 2 | import { FC } from 'react' 3 | 4 | interface ToolbarProps { 5 | onZoomInClick?: () => void 6 | onZoomOutClick?: () => void 7 | onSwitchWatchModeClick?: () => void 8 | onOpenEditorClick?: () => void 9 | onOpenTranslateClick?: () => void 10 | onExtractOutline?: () => void 11 | } 12 | export const Toolbar: FC = props => { 13 | const [editorVisible, setEditorVisible] = useEditorVisible() 14 | return ( 15 |
16 | 17 | {editorVisible && } 18 | 19 | 20 | 21 | 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/useCleanMode.ts: -------------------------------------------------------------------------------- 1 | import { PdfScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | 4 | const cleanMode = atom(false) 5 | 6 | const useCleanMode = () => useAtom(cleanMode, PdfScope) 7 | 8 | export default useCleanMode 9 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/useCollapsed.ts: -------------------------------------------------------------------------------- 1 | import { PdfScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | 4 | const collapsedAtom = atom(false) 5 | 6 | const useCollapsed = () => useAtom(collapsedAtom, PdfScope) 7 | 8 | export default useCollapsed 9 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/useContextMenu.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | 3 | const contextMenuOpen = atom(false) 4 | export const useContextMenu = () => { 5 | return useAtom(contextMenuOpen) 6 | } 7 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/useCurrentTextSelection.ts: -------------------------------------------------------------------------------- 1 | import { ITextSelectionLabel } from '@/../typings/data' 2 | import { atom, useAtom } from 'jotai' 3 | 4 | const textSelection = atom(null) 5 | export const useCurrentTextSelection = () => useAtom(textSelection) 6 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/usePdfInfo.ts: -------------------------------------------------------------------------------- 1 | import { PdfScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | import { IPdfDocument } from '~/typings/data' 4 | 5 | const pdfInfoAtom = atom(null) 6 | 7 | const usePdfInfo = () => useAtom(pdfInfoAtom, PdfScope) 8 | 9 | export default usePdfInfo 10 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/usePdfNote.ts: -------------------------------------------------------------------------------- 1 | // A React context for sharing the Pdf note object 2 | 3 | import { CustomLabel, ILabel, PdfNoteData } from '@/../typings/data' 4 | import { atom, PrimitiveAtom, useAtom, useAtomValue, useSetAtom } from 'jotai' 5 | import { focusAtom } from 'jotai/optics' 6 | import { splitAtom } from 'jotai/utils' 7 | import { useMemo } from 'react' 8 | 9 | export type SplitAtomAction = 10 | | { 11 | type: 'remove' 12 | atom: PrimitiveAtom 13 | } 14 | | { 15 | type: 'insert' 16 | value: Item 17 | before?: PrimitiveAtom 18 | } 19 | | { 20 | type: 'move' 21 | atom: PrimitiveAtom 22 | before?: PrimitiveAtom 23 | } 24 | export const PdfNoteAtom = atom({ labels: [] }) 25 | 26 | export const PdfLabelsAtoms = splitAtom( 27 | focusAtom(PdfNoteAtom, optic => optic.prop('labels')), 28 | item => item.id 29 | ) 30 | 31 | export const usePdfNote = () => useAtom(PdfNoteAtom) 32 | 33 | 34 | 35 | export const usePdfLabels = ( 36 | pageIndex: number, 37 | type: string 38 | ): [PrimitiveAtom[], (update: SplitAtomAction) => void] => { 39 | const dispatch = useSetAtom(PdfLabelsAtoms) 40 | const labelAtomsAtom = useMemo( 41 | () => atom(get => get(PdfLabelsAtoms).filter(i => get(i).type === type && get(i).page === pageIndex)), 42 | [pageIndex] 43 | ) 44 | const atoms = useAtomValue(labelAtomsAtom) 45 | return [atoms, dispatch] 46 | } 47 | -------------------------------------------------------------------------------- /src/components/pdf/hooks/useWidth.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | 3 | const widthAtom = atom({ 4 | viewerWidth: '60%', 5 | sidebarWidth: '40%' 6 | }) 7 | 8 | const useWidth = () => useAtom(widthAtom) 9 | 10 | export default useWidth 11 | -------------------------------------------------------------------------------- /src/components/pdf/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../styles/common.module.less"; 2 | .contextMenu{ 3 | font-size: 14px; 4 | // background-color: #fff; 5 | border-radius: 2px; 6 | padding: 5px; 7 | width: 150px; 8 | height: auto; 9 | margin: 0; 10 | position: absolute; 11 | list-style: none; 12 | opacity: 1; 13 | transition: opacity 0.5s linear; 14 | z-index: 9999; 15 | } 16 | .translateResultBox{ 17 | width: 400px; 18 | } 19 | .viewerBox{ 20 | width: 100%; 21 | height: calc( 100vh - 100px); 22 | font-size: 16px; 23 | position: relative; 24 | } 25 | .viewerLayout{ 26 | .iosStyleScrollBar; 27 | } 28 | .careEyes{ 29 | position: absolute; 30 | top: 0; 31 | right: 0; 32 | bottom: 0; 33 | left: 0; 34 | pointer-events: none; 35 | background-color: rgba(204,232,207,0.2); 36 | z-index: 9998; 37 | } 38 | .scrollableHeader{ 39 | flex-grow: 1; 40 | } 41 | .statusBar{ 42 | margin: 0 20px; 43 | height: 100%; 44 | } 45 | .headerWrapper{ 46 | padding: 5px; 47 | .defaultBorder 48 | } 49 | .flexColumnWrapper{ 50 | flex-grow: 0; 51 | flex-shrink: 1; 52 | } -------------------------------------------------------------------------------- /src/components/picker/colorPicker/index.module.less: -------------------------------------------------------------------------------- 1 | @import '../../../styles/common.module.less'; 2 | .colors{ 3 | max-width: 240px; 4 | overflow-y: hidden; 5 | overflow-x: scroll; 6 | padding: 4px 0; 7 | .slimIosStyleScrollBar; 8 | } 9 | .colorPickerContainer{ 10 | width: 24px; 11 | height: 24px; 12 | padding: 2px; 13 | border-radius: 3px; 14 | cursor: pointer; 15 | border: 1px solid transparent; 16 | &:hover{ 17 | border: 1px solid grey; 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/picker/emojiPicker/index.module.less: -------------------------------------------------------------------------------- 1 | @import "../../../styles/common.module.less"; 2 | 3 | .emojiContainer{ 4 | user-select: none; 5 | transition: background 0.05s ease-in-out; 6 | border-radius: 4px; 7 | cursor: pointer; 8 | width: 32px; 9 | height: 32px; 10 | &:hover{ 11 | background: rgba(55,53,47,0.08); 12 | } 13 | } 14 | .emojisContainer{ 15 | width: 400px; 16 | min-height: 200px; 17 | max-height: 270px; 18 | overflow: hidden auto; 19 | display: flex; 20 | flex-flow: wrap; 21 | padding: 4; 22 | .iosStyleScrollBar; 23 | } 24 | .categories{ 25 | overflow: auto hidden; 26 | flex-wrap: nowrap; 27 | padding: 4px; 28 | width: 400px; 29 | .iosStyleScrollBar; 30 | } 31 | .category{ 32 | height: 26px; 33 | user-select: none; 34 | cursor: pointer; 35 | padding: 0 8px; 36 | font-size: 12px; 37 | transition: background 0.15s ease-in-out; 38 | border-radius: 6px; 39 | font-weight: 500; 40 | &:hover{ 41 | background: rgba(55,53,47,0.08); 42 | } 43 | &.selected{ 44 | background: rgba(55,53,47,0.08); 45 | } 46 | &:not(:first-child){ 47 | margin-left: 4px; 48 | } 49 | } -------------------------------------------------------------------------------- /src/components/picker/iconPicker/index.module.less: -------------------------------------------------------------------------------- 1 | .iconPickerContainer{ 2 | padding: 2px; 3 | border-radius: 2px; 4 | } -------------------------------------------------------------------------------- /src/components/picker/iconPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import { Content, Icon, Toolbar } from '@/components' 2 | import _ from 'lodash' 3 | import { FC } from 'react' 4 | import styles from './index.module.less' 5 | 6 | interface IconPickerProps { 7 | icons: string[] 8 | rowSize?: number 9 | onIconPick: (icon: string) => void 10 | } 11 | const IconPicker: FC = ({ icons, onIconPick, rowSize = 5 }) => { 12 | return ( 13 | 14 | {_.chunk(icons, rowSize).map(i => ( 15 | 16 | {i.map(ii => ( 17 | onIconPick(ii)} iconName={ii}> 18 | ))} 19 | 20 | ))} 21 | 22 | ) 23 | } 24 | export default IconPicker 25 | -------------------------------------------------------------------------------- /src/components/titlebar/index.module.less: -------------------------------------------------------------------------------- 1 | .titleBar{ 2 | -webkit-app-region: drag; 3 | -webkit-user-select: none; 4 | user-select: none; 5 | z-index: 100000; 6 | height: 30px; 7 | padding-right: 20px; 8 | > div{ 9 | -webkit-app-region: no-drag; 10 | } 11 | } 12 | .icon{ 13 | padding: 6px; 14 | border-radius: 4px; 15 | transition: background 0.3s ease; 16 | &:hover{ 17 | background: rgba(0,0,0,0.08); 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/treeCollection/draggingState.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | 3 | const draggingStateAtom = atom({ 4 | isDragging: false, 5 | dragElementType: '', 6 | dragElementId: 0, 7 | dragOverElementType: '', 8 | dragOverElementName: '', 9 | canDrop: false 10 | }) 11 | const useDraggingState = () => useAtom(draggingStateAtom) 12 | 13 | export default useDraggingState 14 | -------------------------------------------------------------------------------- /src/components/treeCollection/index.module.less: -------------------------------------------------------------------------------- 1 | .item{ 2 | transition: background 0.3s ease, border 0.3s ease; 3 | border: 1px solid transparent; 4 | border-radius: 3px; 5 | padding: 4px; 6 | user-select: none; 7 | cursor: pointer; 8 | &.isOver{ 9 | border: 1px solid #8590ae; 10 | background: #8590aea8; 11 | } 12 | } 13 | .draggingElementContainer{ 14 | border-radius: 4px; 15 | background: white; 16 | opacity: 0.8; 17 | } 18 | .draggingIndicator{ 19 | 20 | } 21 | .icon{ 22 | position: absolute !important; 23 | left: -15px; 24 | top: 50%; 25 | transform: translateY(-50%); 26 | transition: all 0.2s ease; 27 | &.collapsed{ 28 | transform: translateY(-50%) rotate(90deg); 29 | } 30 | } -------------------------------------------------------------------------------- /src/config/axiosClient.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | axios.defaults.withCredentials = false; 4 | const fetchClient = () => { 5 | const instance = axios.create() 6 | instance.interceptors.request.use(request => { 7 | // request.headers.RendevozCsrf = Cookies.get('CSRF-TOKEN') 8 | return request 9 | }) 10 | return instance 11 | } 12 | export default fetchClient() 13 | -------------------------------------------------------------------------------- /src/consts/pagination.ts: -------------------------------------------------------------------------------- 1 | export const DefaultPaginationLimit = 20 -------------------------------------------------------------------------------- /src/events/eventHandler.ts: -------------------------------------------------------------------------------- 1 | 2 | class EventHandler { 3 | private readonly handlerMap = new Map() 4 | 5 | on(type: K, handler: (data: T[K]) => void) { 6 | this.handlerMap.set(type, handler) 7 | } 8 | handle(event) { 9 | const handler = this.handlerMap.get(event.type) 10 | if (handler) { 11 | handler(event.data) 12 | } 13 | } 14 | } 15 | 16 | export default EventHandler 17 | -------------------------------------------------------------------------------- /src/events/pdfEvent.ts: -------------------------------------------------------------------------------- 1 | import { Noop } from '@/common/types' 2 | import { HighlightArea } from '~/typings/data' 3 | import { Event } from './eventEmitter' 4 | import EventHandler from './eventHandler' 5 | 6 | interface JumpToHighlightEvent { 7 | area: HighlightArea 8 | } 9 | interface JumpToDestEvent { 10 | dest: string 11 | } 12 | interface AddThoughtEvent { 13 | id: number 14 | } 15 | interface JumpToPageEvent { 16 | pageIndex: number 17 | } 18 | export interface TextSelectRect { 19 | left?: number 20 | top?: number 21 | width?: number 22 | height?: number 23 | percentageLeft?: number 24 | percentageTop?: number 25 | percentageWidth?: number 26 | percentageHeight?: number 27 | } 28 | export interface TextSelectEvent { 29 | selectedText?: string 30 | pageIndex: number 31 | rects?: TextSelectRect[] 32 | isCancel: boolean 33 | } 34 | 35 | export interface PdfEventMap { 36 | jumpToHighlight: JumpToHighlightEvent 37 | jumpToDest: JumpToDestEvent 38 | addThought: AddThoughtEvent 39 | jumpToPage: JumpToPageEvent 40 | textSelect: TextSelectEvent 41 | addHighlight: Noop 42 | cancelTextSelect: Noop 43 | zoomIn: Noop 44 | zoomOut: Noop 45 | autoFit: Noop 46 | togglePan: Noop 47 | } 48 | export type PdfEvent = Event 49 | 50 | export class PdfEventHandler extends EventHandler {} 51 | -------------------------------------------------------------------------------- /src/events/useEventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { eventEmitter } from '@/events/eventEmitter' 3 | import { useAtomValue } from 'jotai' 4 | 5 | const useEventEmitter = () => useAtomValue(eventEmitter, GlobalScope) 6 | export default useEventEmitter 7 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export { } 3 | 4 | declare global { 5 | interface Window { 6 | // Expose some Api through preload script 7 | fs: typeof import('fs') 8 | ipcRenderer: import('electron').IpcRenderer 9 | removeLoading: () => void 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/components/useCurrentCollection.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | import { ICollection } from '~/typings/data' 4 | 5 | const currentCollection = atom(null) 6 | 7 | const useCurrentCollection = () => useAtom(currentCollection, GlobalScope) 8 | 9 | 10 | export default useCurrentCollection 11 | -------------------------------------------------------------------------------- /src/hooks/components/useCurrentDocument.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | import { IPdfDocument } from '~/typings/data' 4 | 5 | const currentDocumentAtom = atom(null) 6 | 7 | const useCurrentDocument = () => useAtom(currentDocumentAtom, GlobalScope) 8 | 9 | export default useCurrentDocument 10 | -------------------------------------------------------------------------------- /src/hooks/components/useCurrentViewingPdf.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | import { IPdfDocument } from '~/typings/data' 4 | 5 | const currentViewingPdf = atom(null) 6 | 7 | const useCurrentViewingPdf = () => useAtom(currentViewingPdf, GlobalScope) 8 | 9 | export default useCurrentViewingPdf 10 | -------------------------------------------------------------------------------- /src/hooks/components/useEditor.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/hooks/components/useEditor.ts -------------------------------------------------------------------------------- /src/hooks/components/useEditorManagerVisible.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/hooks/components/useEditorManagerVisible.ts -------------------------------------------------------------------------------- /src/hooks/components/useSidebarVisible.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | 4 | const sidebarVisible = atom(true) 5 | 6 | const useSidebarVisible = () => useAtom(sidebarVisible, GlobalScope) 7 | 8 | export default useSidebarVisible -------------------------------------------------------------------------------- /src/hooks/components/useSidebarWidth.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom } from 'jotai' 3 | 4 | const sidebarWidthAtom = atom(240) 5 | 6 | const useSidebarWidth = () => useAtom(sidebarWidthAtom, GlobalScope) 7 | 8 | export default useSidebarWidth 9 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils' 2 | export * from './styles' 3 | -------------------------------------------------------------------------------- /src/hooks/stores/useDb.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/utils/db' 2 | 3 | const useDb = (table: string) => { 4 | return db.table(table) 5 | } 6 | export default useDb 7 | -------------------------------------------------------------------------------- /src/hooks/styles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTheme' -------------------------------------------------------------------------------- /src/hooks/styles/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { GlobalScope } from '@/jotai/jotaiScope' 2 | import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' 3 | import { atomFamily } from 'jotai/utils' 4 | 5 | const defaultTheme = { 6 | primaryColor: 'rgb(162, 181, 200)', 7 | pa: 'asd' 8 | } 9 | 10 | const themeAtomFamily = atomFamily((key: keyof typeof defaultTheme) => atom(defaultTheme[key])) 11 | type Theme = keyof typeof defaultTheme 12 | type RemoveArrayRepeats = { 13 | [K in keyof T]: T[number] extends { [P in keyof T]: P extends K ? never : T[P] }[number] ? never : T[K] 14 | } 15 | export function useTheme(...themes: RemoveArrayRepeats & T) { 16 | const theme: Partial = {} 17 | themes.map(key => { 18 | const value = useAtomValue(themeAtomFamily(key), GlobalScope) 19 | theme[key] = value 20 | }) 21 | return theme 22 | } 23 | 24 | export const useSetTheme = (name: keyof typeof defaultTheme) => { 25 | const atom = themeAtomFamily(name) 26 | return useSetAtom(atom, GlobalScope) 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useToggle } from './useToggle' 2 | export { default as usePrevious } from './usePrevious' 3 | export { default as useRafState } from './useRafState' 4 | export { default as useUnMount } from './useUnmount' 5 | export { default as useMousePositionRef } from './useMousePositionRef' 6 | export { default as useMount } from './useMount' 7 | export { default as useMemoizedFn } from './useMemoizedFn' 8 | export { default as useTime } from './useTime' 9 | export { default as useTransition } from './useTransition' 10 | export { default as useMergedRef } from './useMergedRef' 11 | export { default as useDebounce } from './useDebounce' 12 | export { default as useDebounceFn } from './useDebounceFn' 13 | export { default as useRafFn } from './useRafFn' 14 | export { default as useUpdateEffect } from './useUpdateEffect' 15 | export { default as useDebounceEffect } from './useDebounceEffect' -------------------------------------------------------------------------------- /src/hooks/utils/useCallbackRef.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | 3 | function useCallbackRef() { 4 | const [ref, setRef] = useState(null) 5 | const fn = useCallback(node => { 6 | setRef(node) 7 | }, []) 8 | return [ref, fn] 9 | } 10 | 11 | export default useCallbackRef 12 | -------------------------------------------------------------------------------- /src/hooks/utils/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect } from 'react' 2 | 3 | const useClickOutside = (callback: () => void, ref: MutableRefObject | MutableRefObject[]) => { 4 | const targets = Array.isArray(ref) ? ref : [ref] 5 | const handleClick = e => { 6 | if (targets.some(i => i.current && !i.current.contains(e.target))) { 7 | callback() 8 | } 9 | } 10 | useEffect(() => { 11 | document.addEventListener('click', handleClick) 12 | return () => { 13 | document.removeEventListener('click', handleClick) 14 | } 15 | }) 16 | } 17 | export default useClickOutside 18 | -------------------------------------------------------------------------------- /src/hooks/utils/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import useDebounceFn from './useDebounceFn' 3 | 4 | function useDebounce(value: T, options?: { wait?: number; leading?: boolean; trailing?: boolean; maxWait?: number }) { 5 | const [debounced, setDebounced] = useState(value) 6 | 7 | const { run } = useDebounceFn(() => { 8 | setDebounced(value) 9 | }, options) 10 | 11 | useEffect(() => { 12 | run() 13 | }, [value]) 14 | 15 | return debounced 16 | } 17 | 18 | export default useDebounce 19 | -------------------------------------------------------------------------------- /src/hooks/utils/useDebounceEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { DependencyList, EffectCallback } from 'react'; 3 | import useDebounceFn from './useDebounceFn'; 4 | import useUpdateEffect from './useUpdateEffect'; 5 | 6 | function useDebounceEffect( 7 | effect: EffectCallback, 8 | deps?: DependencyList, 9 | options?: { wait?: number; leading?: boolean; trailing?: boolean; maxWait?: number }, 10 | ) { 11 | const [flag, setFlag] = useState({}); 12 | 13 | const { run } = useDebounceFn(() => { 14 | setFlag({}); 15 | }, options); 16 | 17 | useEffect(() => { 18 | return run(); 19 | }, deps); 20 | 21 | useUpdateEffect(effect, [flag]); 22 | } 23 | export default useDebounceEffect; -------------------------------------------------------------------------------- /src/hooks/utils/useDebounceFn.ts: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash/debounce' 2 | import { useMemo } from 'react' 3 | import useLatest from './useLatest' 4 | import useUnmount from './useUnmount' 5 | 6 | type noop = (...args: any) => any 7 | 8 | function useDebounceFn(fn: T, options?: { wait?: number; leading?: boolean; trailing?: boolean; maxWait?: number }) { 9 | const fnRef = useLatest(fn) 10 | 11 | const wait = options?.wait ?? 1000 12 | 13 | const debounced = useMemo(() => debounce((...args: Parameters): ReturnType => fnRef.current(...args), wait, options), []) 14 | 15 | useUnmount(() => { 16 | debounced.cancel() 17 | }) 18 | 19 | return { 20 | run: debounced, 21 | cancel: debounced.cancel, 22 | flush: debounced.flush 23 | } 24 | } 25 | 26 | export default useDebounceFn 27 | -------------------------------------------------------------------------------- /src/hooks/utils/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | /** 4 | * Helper to remove plumbing involved with adding and removing an event listener 5 | * in components. 6 | * 7 | * @param eventName The name of the event to listen to. 8 | * @param handler The handler to call when the event is triggered. 9 | * @param element The element to attach the event listener to. 10 | * @param options The options to pass to the event listener. 11 | */ 12 | export default function useEventListener( 13 | eventName: string, 14 | handler: T, 15 | element: Window | Node = window, 16 | options: AddEventListenerOptions = {} 17 | ) { 18 | const savedHandler = React.useRef() 19 | const { capture, passive, once } = options 20 | 21 | React.useEffect(() => { 22 | savedHandler.current = handler 23 | }, [handler]) 24 | 25 | React.useEffect(() => { 26 | const isSupported = element?.addEventListener 27 | if (!isSupported) { 28 | console.error('not support', element) 29 | return 30 | } 31 | const eventListener: EventListener = event => savedHandler.current?.(event) 32 | const opts = { capture, passive, once } 33 | element.addEventListener(eventName, eventListener, opts) 34 | return () => element.removeEventListener(eventName, eventListener, opts) 35 | }, [eventName, element, capture, passive, once]) 36 | } 37 | -------------------------------------------------------------------------------- /src/hooks/utils/useLatest.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | 3 | function useLatest (value: T) { 4 | const ref = useRef(value) 5 | ref.current = value 6 | return ref 7 | } 8 | export default useLatest 9 | -------------------------------------------------------------------------------- /src/hooks/utils/useMeasure.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import useCallbackRef from './useCallbackRef' 3 | import ResizeObserver from 'resize-observer-polyfill' 4 | const useMeasure = (ref: React.RefObject) => { 5 | const [element, attachRef] = useCallbackRef() 6 | const [bounds, setBounds] = useState({}) 7 | function onResize([entry]) { 8 | setBounds({ 9 | height: entry.contentRect.height 10 | }) 11 | } 12 | useEffect(() => { 13 | const observer = new ResizeObserver(onResize) 14 | if (element && element.current) { 15 | observer.observe(element.current) 16 | } 17 | return () => { 18 | observer.disconnect() 19 | } 20 | }, [element]) 21 | useEffect(() => { 22 | attachRef(ref) 23 | }, [attachRef, ref]) 24 | return bounds 25 | } 26 | 27 | export default useMeasure 28 | -------------------------------------------------------------------------------- /src/hooks/utils/useMemoizedFn.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useRef } from 'react' 2 | 3 | type noop = (this: any, ...args: any[]) => any 4 | 5 | type PickFunction = (this: ThisParameterType, ...args: Parameters) => ReturnType 6 | 7 | function useMemoizedFn(fn: T) { 8 | const fnRef = useRef(fn) 9 | 10 | fnRef.current = useMemo(() => fn, [fn]) 11 | 12 | const memoizedFn = useRef>() 13 | if (!memoizedFn.current) { 14 | memoizedFn.current = function (this, ...args) { 15 | return fnRef.current.apply(this, args) 16 | } 17 | } 18 | 19 | return memoizedFn.current as T 20 | } 21 | 22 | export default useMemoizedFn 23 | -------------------------------------------------------------------------------- /src/hooks/utils/useMergedRef.ts: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, Ref } from 'react' 2 | 3 | export function assignRef(ref: React.ForwardedRef, value: T | null) { 4 | if (typeof ref === 'function') { 5 | ref(value) 6 | } else if (typeof ref === 'object' && ref !== null && 'current' in ref) { 7 | // eslint-disable-next-line no-param-reassign 8 | ref.current = value 9 | } 10 | } 11 | export function mergeRefs(...refs: Ref[]) { 12 | return (node: T | null) => { 13 | refs.forEach(ref => assignRef(ref, node)) 14 | } 15 | } 16 | 17 | const useMergedRef = (...refs: MutableRefObject[]) => { 18 | return (node: any | null) => { 19 | refs.forEach(ref => { 20 | if (typeof ref === 'function') { 21 | ref(node) 22 | } else if (typeof ref === 'object' && ref !== null && 'current' in ref) { 23 | ref.current = node 24 | } 25 | }) 26 | } 27 | } 28 | 29 | export default useMergedRef 30 | -------------------------------------------------------------------------------- /src/hooks/utils/useMount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | 3 | const useMount = (fn: () => void) => { 4 | useEffect(() => { 5 | fn?.() 6 | }, []) 7 | } 8 | 9 | export default useMount 10 | -------------------------------------------------------------------------------- /src/hooks/utils/usePdfjs.ts: -------------------------------------------------------------------------------- 1 | import * as PdfJs from 'pdfjs-dist' 2 | 3 | const usePdfjs = () => { 4 | return PdfJs 5 | } 6 | export default usePdfjs 7 | -------------------------------------------------------------------------------- /src/hooks/utils/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export default function usePrevious (value: T): T | undefined { 4 | const ref = React.useRef() 5 | 6 | React.useEffect(() => { 7 | ref.current = value 8 | }) 9 | 10 | return ref.current 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/utils/useRafFn.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from "react" 2 | import useLatest from "./useLatest" 3 | 4 | const useRafFn = (fn: (...args) => void) => { 5 | const fnRef = useLatest(fn) 6 | const frameIdRef = useRef(null) 7 | let lastArgs 8 | 9 | const later = context => () => { 10 | frameIdRef.current = null 11 | const lfn = fnRef.current 12 | lfn.apply(context, lastArgs) 13 | } 14 | const throttled = function (...args) { 15 | lastArgs = args 16 | if (frameIdRef.current === null) { 17 | frameIdRef.current = requestAnimationFrame(later(this)) 18 | } 19 | } 20 | throttled.cancel = () => { 21 | cancelAnimationFrame(frameIdRef.current) 22 | frameIdRef.current = null 23 | } 24 | return throttled 25 | } 26 | 27 | export default useRafFn 28 | -------------------------------------------------------------------------------- /src/hooks/utils/useRafState.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react' 2 | import useUnmount from './useUnmount' 3 | 4 | const useRafState = (initialState: S | (() => S)): [S, Dispatch>] => { 5 | const frame = useRef(0) 6 | const [state, setState] = useState(initialState) 7 | const setRafState = useCallback((value: S | ((prev: S) => S)) => { 8 | cancelAnimationFrame(frame.current) 9 | frame.current = requestAnimationFrame(() => { 10 | setState(value) 11 | }) 12 | }, []) 13 | useUnmount(() => { 14 | cancelAnimationFrame(frame.current) 15 | }) 16 | return [state, setRafState] 17 | } 18 | 19 | export default useRafState 20 | -------------------------------------------------------------------------------- /src/hooks/utils/useRefCallback.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export const useRefCallback = () => { 4 | // github use-callback-ref 5 | // https://stackoverflow.com/a/52334964 6 | const [ref] = useState(() => ({ 7 | value: 1, 8 | callback: (newValue, lastValue) => { console.log(newValue, lastValue) }, 9 | get current () { 10 | return ref.value 11 | }, 12 | set current (value) { 13 | const last = ref.value 14 | ref.value = value 15 | ref.callback(value, last) 16 | } 17 | })) 18 | return ref; 19 | } 20 | -------------------------------------------------------------------------------- /src/hooks/utils/useRefDepsEffect.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, RefObject, EffectCallback, DependencyList } from 'react' 2 | 3 | export const useRefDepsEffect = createRefDepsHook(useEffect) 4 | 5 | type UseEffectLike = (effect: EffectCallback, deps?: DependencyList) => void 6 | 7 | export function createRefDepsHook (useEffectLike: UseEffectLike) { 8 | return (effect: EffectCallback, refDeps: DependencyList) => { 9 | const cleanupRef = useRef<(() => void) | undefined>() 10 | const prevDepsRef = useRef() 11 | 12 | useEffectLike(() => { 13 | const prevDeps = prevDepsRef.current 14 | if (prevDeps && refDeps.every((v, i) => Object.is(isRefObj(v) ? v.current : v, prevDeps[i]))) { 15 | return 16 | } 17 | 18 | cleanupRef.current?.() 19 | cleanupRef.current = effect()! 20 | prevDepsRef.current = refDeps.map(v => (isRefObj(v) ? v.current : v)) 21 | }) 22 | 23 | useEffectLike(() => () => cleanupRef.current?.(), []) 24 | } 25 | } 26 | 27 | function isRefObj (ref: any): ref is RefObject { 28 | return (ref !== null || ref !== undefined) && 'current' in ref 29 | } 30 | -------------------------------------------------------------------------------- /src/hooks/utils/useThrottleFn.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { useEffect, useMemo, useRef } from 'react' 3 | import useLatest from './useLatest' 4 | 5 | type noop = (...args: any) => any 6 | function useThrottleFn(fn: T, options) { 7 | const fnRef = useLatest(fn) 8 | const wait = options?.wait ?? 1000 9 | const throttled = useMemo( 10 | () => 11 | _.throttle( 12 | (...args: Parameters): ReturnType => { 13 | return fnRef.current(...args) 14 | }, 15 | wait, 16 | options 17 | ), 18 | [] 19 | ) 20 | useEffect( 21 | () => () => { 22 | throttled.cancel() 23 | }, 24 | [] 25 | ) 26 | return { 27 | run: throttled, 28 | cancel: throttled.cancel, 29 | flush: throttled.flush 30 | } 31 | } 32 | export default useThrottleFn 33 | -------------------------------------------------------------------------------- /src/hooks/utils/useTime.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns' 2 | 3 | function useTime(): number 4 | function useTime(time: number): string 5 | function useTime(time?: number) { 6 | if (time) { 7 | return format(time, 'd MMM yyyy') 8 | } 9 | return Date.now() 10 | } 11 | export default useTime 12 | -------------------------------------------------------------------------------- /src/hooks/utils/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, useReducer } from 'react' 2 | 3 | const toggleReducer = (state: boolean, nextValue?: unknown) => (typeof nextValue === 'boolean' ? nextValue : !state) 4 | const useToggle = (initialValue: boolean): [boolean, (nextValue?: unknown) => void] => { 5 | return useReducer>(toggleReducer, initialValue) 6 | } 7 | 8 | export default useToggle 9 | -------------------------------------------------------------------------------- /src/hooks/utils/useTransition.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react' 2 | import { AnimatePresence, motion } from 'framer-motion' 3 | 4 | type Animator = (children: ReactNode, key: string) => ReactNode 5 | 6 | const animator: Animator = (children, key) => ( 7 | 8 | {children} 9 | 10 | ) 11 | 12 | const useTransition = () => animator 13 | 14 | export default useTransition 15 | -------------------------------------------------------------------------------- /src/hooks/utils/useUnmount.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import useLatest from './useLatest' 3 | 4 | const useUnmount = (fn: () => void) => { 5 | // avoid outdated reference 6 | const fnRef = useLatest(fn) 7 | useEffect( 8 | () => () => { 9 | fnRef.current() 10 | }, 11 | [] 12 | ) 13 | } 14 | 15 | export default useUnmount 16 | -------------------------------------------------------------------------------- /src/hooks/utils/useUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect, useRef } from 'react' 2 | 3 | type EffectHookType = typeof useEffect | typeof useLayoutEffect 4 | 5 | export const createUpdateEffect: (hook: EffectHookType) => EffectHookType = hook => (effect, deps) => { 6 | const isMounted = useRef(false) 7 | 8 | // for react-refresh 9 | hook(() => { 10 | return () => { 11 | isMounted.current = false 12 | } 13 | }, []) 14 | 15 | hook(() => { 16 | if (!isMounted.current) { 17 | isMounted.current = true 18 | } else { 19 | return effect() 20 | } 21 | }, deps) 22 | } 23 | 24 | const useUpdateEffect = createUpdateEffect(useEffect) 25 | 26 | export default useUpdateEffect 27 | -------------------------------------------------------------------------------- /src/hooks/utils/useUpdateIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useIsomorphicLayoutEffect } from 'react-use' 2 | import { createUpdateEffect } from './useUpdateEffect' 3 | 4 | export default createUpdateEffect(useIsomorphicLayoutEffect) 5 | -------------------------------------------------------------------------------- /src/hooks/utils/useWhyUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react' 2 | 3 | export type IProps = Record 4 | 5 | export default function useWhyDidYouUpdate(componentName: string, props: IProps) { 6 | const prevProps = useRef({}) 7 | 8 | useEffect(() => { 9 | if (prevProps.current) { 10 | const allKeys = Object.keys({ ...prevProps.current, ...props }) 11 | const changedProps: IProps = {} 12 | 13 | allKeys.forEach(key => { 14 | if (!Object.is(prevProps.current[key], props[key])) { 15 | changedProps[key] = { 16 | from: prevProps.current[key], 17 | to: props[key] 18 | } 19 | } 20 | }) 21 | 22 | if (Object.keys(changedProps).length) { 23 | console.log('[why-did-you-update]', componentName, changedProps) 24 | } 25 | } 26 | 27 | prevProps.current = props 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import zhCN from '@/assets/locales/zh-CN/translation.json' 4 | import enUS from '@/assets/locales/en-US/translation.json' 5 | 6 | const initI18n = (defaultLang = 'en-US') => { 7 | i18n.use(initReactI18next).init({ 8 | resources: { 9 | 'en-US': { 10 | translation: enUS 11 | }, 12 | 'zh-CN': { 13 | translation: zhCN 14 | } 15 | }, 16 | interpolation: { 17 | escapeValue: false 18 | }, 19 | react: { 20 | useSuspense: false 21 | }, 22 | lng: defaultLang, 23 | fallbackLng: defaultLang 24 | }) 25 | return i18n 26 | } 27 | export default initI18n 28 | -------------------------------------------------------------------------------- /src/jotai/jotaiScope.ts: -------------------------------------------------------------------------------- 1 | export const GlobalScope = Symbol('global') 2 | 3 | export const PdfScope = Symbol('pdf') 4 | 5 | export const EditorScope = Symbol('editor') 6 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { enableMapSet } from 'immer' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | import initI18n from './i18n' 5 | import { initSearchIndex } from './utils/searchIndex' 6 | 7 | enableMapSet() 8 | initSearchIndex() 9 | const lang = localStorage.getItem('lang') || 'en-US' 10 | initI18n(lang) 11 | 12 | const root = createRoot(document.getElementById('root')!) 13 | 14 | root.render() 15 | 16 | postMessage({ payload: 'removeLoading' }, '*') 17 | -------------------------------------------------------------------------------- /src/pdfViewer/core/Loader.tsx: -------------------------------------------------------------------------------- 1 | import Spinner from '@/components/base/Spinner' 2 | import { PDFDocumentProxy } from 'pdfjs-dist' 3 | import { FC, useEffect, useRef, useState } from 'react' 4 | import PdfJs from './types/pdfJsApi' 5 | import PdfJsUrl from '@/assets/pdf.worker.min.js?url' 6 | interface LoaderProps { 7 | file: string | Uint8Array 8 | children: (doc: PDFDocumentProxy) => React.ReactElement 9 | onDocumentLoaded?: (doc: PDFDocumentProxy) => void 10 | } 11 | enum LoadingState { 12 | Complete, 13 | Fail, 14 | Loading 15 | } 16 | const Loader: FC = ({ file, children, onDocumentLoaded }) => { 17 | const docRef = useRef(null) 18 | const [loadingState, setLoadingState] = useState(LoadingState.Loading) 19 | useEffect(() => { 20 | docRef.current = null 21 | const params = Object.assign('string' === typeof file ? { url: file } : { data: file }) 22 | PdfJs.GlobalWorkerOptions.workerSrc = PdfJsUrl 23 | const loadingTask = PdfJs.getDocument(params) 24 | loadingTask.promise.then(doc => { 25 | docRef.current = doc 26 | onDocumentLoaded?.(doc) 27 | setLoadingState(LoadingState.Complete) 28 | }) 29 | return () => { 30 | loadingTask.destroy() 31 | } 32 | }, [file]) 33 | if (loadingState === LoadingState.Complete && docRef.current) { 34 | return children(docRef.current) 35 | } 36 | return 37 | } 38 | export default Loader 39 | -------------------------------------------------------------------------------- /src/pdfViewer/core/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'jotai' 2 | 3 | export const PdfViewInnerScope = Symbol('pdfInnerViewer') 4 | 5 | const PdfProvider = ({ children }) => { 6 | return {children} 7 | } 8 | 9 | export default PdfProvider 10 | -------------------------------------------------------------------------------- /src/pdfViewer/core/annotation/Annotation.tsx: -------------------------------------------------------------------------------- 1 | import { PageViewport, PDFPageProxy } from 'pdfjs-dist' 2 | import { FC, ReactElement } from 'react' 3 | import { IAnnotation } from '../types/annotation' 4 | 5 | interface AnnotationProps extends IAnnotation { 6 | page: PDFPageProxy 7 | viewport: PageViewport 8 | renderAnnotation?: (annotaion: IAnnotation) => ReactElement 9 | } 10 | const Annotation: FC = ({ page, viewport, rect, annotationType, renderAnnotation, ...rest }) => { 11 | const bound = { 12 | left: Math.min(rect[0], rect[2]), 13 | top: page.view[1] + page.view[3] - Math.max(rect[1], rect[3]), 14 | height: rect[3] - rect[1], 15 | width: rect[2] - rect[0] 16 | } 17 | return ( 18 |
27 | {renderAnnotation?.({ rect, annotationType, ...rest, pageIndex: page.pageNumber - 1 })} 28 |
29 | ) 30 | } 31 | 32 | export default Annotation 33 | -------------------------------------------------------------------------------- /src/pdfViewer/core/hooks/useScrollMode.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai' 2 | import { PdfViewInnerScope } from '../Provider' 3 | import { ScrollMode } from '../types/layout' 4 | 5 | const scrollModeAtom = atom('vertical') 6 | 7 | export const useScrollMode = () => useAtom(scrollModeAtom, PdfViewInnerScope) 8 | -------------------------------------------------------------------------------- /src/pdfViewer/core/layers/index.module.less: -------------------------------------------------------------------------------- 1 | .pageLayer{ 2 | margin: 0 auto; 3 | position: relative; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | border-radius: 9px; 8 | &::after{ 9 | position: absolute; 10 | right: 0; 11 | top: 0; 12 | left: 0; 13 | bottom: 0; 14 | content: ''; 15 | box-shadow: rgba(149, 157, 165, 0.4) 0px 4px 24px -9px; 16 | pointer-events: none; 17 | } 18 | } -------------------------------------------------------------------------------- /src/pdfViewer/core/types/annotation.ts: -------------------------------------------------------------------------------- 1 | export enum AnnotationType { 2 | Text = 1, 3 | Link = 2 4 | } 5 | 6 | export interface IAnnotation { 7 | annotationFlags: number 8 | annotationType: AnnotationType 9 | rect: number[] 10 | rotation: number 11 | subtype: string 12 | pageIndex: number 13 | dest?: [{ num: number }] 14 | } 15 | -------------------------------------------------------------------------------- /src/pdfViewer/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './layout' 2 | export * from './page' 3 | export * from './point' -------------------------------------------------------------------------------- /src/pdfViewer/core/types/layout.ts: -------------------------------------------------------------------------------- 1 | export interface LayoutSize { 2 | width: number 3 | height: number 4 | scale: number 5 | } 6 | export type ScrollMode = 'horizontal' | 'vertical' -------------------------------------------------------------------------------- /src/pdfViewer/core/types/page.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | 3 | export interface PageSize { 4 | width: number 5 | height: number 6 | scale: number 7 | } 8 | 9 | export interface RenderPageProps { 10 | canvasLayer: ReactElement 11 | textLayer: ReactElement 12 | annotationLayer: ReactElement 13 | pageIndex: number 14 | pageSize: PageSize 15 | } 16 | -------------------------------------------------------------------------------- /src/pdfViewer/core/types/pdfJsApi.ts: -------------------------------------------------------------------------------- 1 | import * as PdfJs from 'pdfjs-dist' 2 | 3 | export default PdfJs 4 | -------------------------------------------------------------------------------- /src/pdfViewer/core/types/point.ts: -------------------------------------------------------------------------------- 1 | export interface Point{ 2 | x: number 3 | y: number 4 | } 5 | 6 | export interface DistanceTolerance{ 7 | 8 | } -------------------------------------------------------------------------------- /src/pdfViewer/core/types/text.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | 3 | export interface TextItemChar { 4 | transform: any 5 | str: string 6 | } 7 | export interface TextItem { 8 | str: string 9 | /** 10 | * - Text direction: 'ttb', 'ltr' or 'rtl'. 11 | */ 12 | dir: string 13 | /** 14 | * - Transformation matrix. 15 | */ 16 | transform: Array 17 | /** 18 | * - Width in device space. 19 | */ 20 | width: number 21 | /** 22 | * - Height in device space. 23 | */ 24 | height: number 25 | /** 26 | * - Font name used by PDF.js for converted font. 27 | */ 28 | fontName: string 29 | /** 30 | * - Indicating if the text content is followed by a 31 | * line-break. 32 | */ 33 | hasEOL: boolean 34 | /** 35 | * 36 | */ 37 | chars: Array 38 | } 39 | export interface TextContentBound { 40 | offset: { left: number; right: number; top: number; bottom: number; size: number[]; trans: TextContentTrans } 41 | textItem: TextItem 42 | style: CSSProperties 43 | str: string 44 | } 45 | export type TextContentOffset = { 46 | left: number 47 | right: number 48 | top: number 49 | bottom: number 50 | size: number[] 51 | trans: TextContentTrans 52 | } 53 | export interface TextRect { 54 | x: number 55 | y: number 56 | width: number 57 | height: number 58 | text: string 59 | offset?: any 60 | } 61 | export type TextContentTrans = number[] 62 | -------------------------------------------------------------------------------- /src/pdfViewer/core/utils/canvas.ts: -------------------------------------------------------------------------------- 1 | export function resizeCanvas(ctx: CanvasRenderingContext2D, width: number, height: number) { 2 | const tempCanvas = document.createElement('canvas') 3 | tempCanvas.width = ctx.canvas.width 4 | tempCanvas.height = ctx.canvas.height 5 | const tempCtx = tempCanvas.getContext('2d') 6 | tempCtx?.drawImage(ctx.canvas, 0, 0) 7 | ctx.canvas.width = width 8 | ctx.canvas.height = height 9 | ctx.drawImage(tempCanvas, 0, 0) 10 | } 11 | -------------------------------------------------------------------------------- /src/pdfViewer/core/utils/pages.ts: -------------------------------------------------------------------------------- 1 | import { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist' 2 | 3 | const pagesMap = new Map() 4 | export const getPage = (doc: PDFDocumentProxy, pageIndex: number): Promise => { 5 | if (!doc) { 6 | return Promise.reject('Document not loaded yet') 7 | } 8 | const pageKey = `${doc.loadingTask.docId}_${pageIndex}` 9 | const page = pagesMap.get(pageKey) 10 | if (page) { 11 | return Promise.resolve(page) 12 | } 13 | return new Promise((resolve, _) => { 14 | doc.getPage(pageIndex + 1).then(page => { 15 | pagesMap.set(pageKey, page) 16 | resolve(page) 17 | }) 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/pdfViewer/core/utils/zoom.ts: -------------------------------------------------------------------------------- 1 | const ZoomLevels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.1, 1.3, 1.5, 1.7, 1.9, 2.1, 2.4, 2.7, 3.0, 3.3, 3.7, 4] 2 | 3 | export const increaseLevel = (current: number) => ZoomLevels.find(i => i > current) || current 4 | 5 | export const decreaseLevel = (current: number) => { 6 | const idx = ZoomLevels.findIndex(i => i >= current) 7 | return idx <= 0 ? current : ZoomLevels[idx - 1] 8 | } 9 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ipc' 2 | -------------------------------------------------------------------------------- /src/plugins/ipc.ts: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = window 2 | 3 | export interface IpcResponse { 4 | data?: T 5 | error?: any 6 | } 7 | 8 | interface IpcInstance { 9 | send: (target: string, ...args: any[]) => Promise> 10 | on: (event: string, callback: (...args: any[]) => void) => void 11 | off: (event: string) => void 12 | } 13 | 14 | export const ipcInstance: IpcInstance = { 15 | send: async (target: string, ...args: any[]) => { 16 | const payloads: any[] = args.map(e => e) 17 | const response: IpcResponse = await ipcRenderer.invoke(target, ...payloads) 18 | /* eslint-disable-next-line no-useless-call */ 19 | if (response.hasOwnProperty.call(response, 'error')) throw response.error 20 | 21 | return response 22 | }, 23 | on: (event, callback) => { 24 | ipcRenderer.on(event, (e, ...args) => { 25 | callback(...args) 26 | }) 27 | // Use tryOnUnmounted if use @vueuse https://vueuse.org/shared/tryOnUnmounted/ 28 | }, 29 | off: event => { 30 | ipcRenderer.removeAllListeners(event) 31 | } 32 | } 33 | 34 | export function useIpc() { 35 | return ipcInstance 36 | } 37 | -------------------------------------------------------------------------------- /src/routes/TransitionRoute.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react' 2 | import { motion } from 'framer-motion' 3 | // only fade tranisition right now 4 | const TransitionRoute = ({ children, type = 'fade' }: { children: ReactNode; type?: 'fade' }) => { 5 | return ( 6 | 7 | {children} 8 | 9 | ) 10 | } 11 | 12 | export default TransitionRoute 13 | -------------------------------------------------------------------------------- /src/routes/globalLocation.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react' 2 | import { Location } from 'react-router-dom' 3 | 4 | // react router v6 v6.4.0 make location contextual,so we need a global location context,or we can't get correct location in Home component 5 | export const GlobalLocationContext = createContext(null) 6 | 7 | export const GlobalLocationContextProvider = GlobalLocationContext.Provider 8 | 9 | export const useGlobalLocation = () => useContext(GlobalLocationContext) 10 | -------------------------------------------------------------------------------- /src/scenes/collections/index.tsx: -------------------------------------------------------------------------------- 1 | import Collection from '@/components/collection' 2 | import Container from '@/components/base/container' 3 | import { FC } from 'react' 4 | import { Content } from '@/components' 5 | 6 | const Collections: FC = () => { 7 | return ( 8 | 9 | 10 | 17 | 18 | 19 | ) 20 | } 21 | export default Collections 22 | -------------------------------------------------------------------------------- /src/scenes/editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from '@/components' 2 | import { EditorV1 } from '@/components/editor/Editor' 3 | import EditorManager from '@/components/editor/EditorManager' 4 | import { useParams, useSearchParams } from 'react-router-dom' 5 | 6 | const EditorPage = () => { 7 | const {id} = useParams() 8 | const [searchParams] = useSearchParams() 9 | const documentId = searchParams.get('documentId') 10 | 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default EditorPage 19 | -------------------------------------------------------------------------------- /src/scenes/home/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from '@/components' 2 | import { getHours } from 'date-fns' 3 | import styles from './index.module.less' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | const Header = () => { 7 | const { t } = useTranslation() 8 | const currentHour = getHours(new Date()) 9 | let currentTime 10 | if (currentHour >= 1 && currentHour < 11) { 11 | currentTime = 'Morning' 12 | } 13 | if (currentHour >= 11 && currentHour < 13) { 14 | currentTime = 'Noon' 15 | } 16 | if (currentHour >= 13 && currentHour < 18) { 17 | currentTime = 'Afternoon' 18 | } 19 | if (currentHour >= 18 && currentHour < 22) { 20 | currentTime = 'Evening' 21 | } 22 | if (currentHour >= 22 && currentHour < 23) { 23 | currentTime = 'Night' 24 | } 25 | if (currentHour >= 23 || currentHour === 0) { 26 | currentTime = 'Midnight' 27 | } 28 | return ( 29 | 30 |
{t('home.greeting', { time: currentTime })}
31 |
32 | ) 33 | } 34 | export default Header 35 | -------------------------------------------------------------------------------- /src/scenes/home/animation.ts: -------------------------------------------------------------------------------- 1 | export const openSpring = { type: 'spring', stiffness: 200, damping: 30,duration: 0.4 } 2 | export const closeSpring = { type: 'spring', stiffness: 300, damping: 35,duration: 0.4 } 3 | -------------------------------------------------------------------------------- /src/scenes/pdf/index.module.less: -------------------------------------------------------------------------------- 1 | .header{ 2 | padding: 4px 20px; 3 | font-family: Muli,sans-serif; 4 | user-select: none; 5 | box-shadow: 0 2px 6px rgba(0,0,0,0.1); 6 | margin-bottom: 6px; 7 | position: relative; 8 | height: 56px; 9 | .title{ 10 | font-weight: 600; 11 | font-size: 18px; 12 | } 13 | .author{ 14 | font-size: 12px; 15 | color: rgba(87, 87, 87,0.6) 16 | } 17 | .progressTrack{ 18 | position: absolute; 19 | bottom: -4px; 20 | height: 4px; 21 | left: 0; 22 | right: 0; 23 | background: transparent; 24 | } 25 | .progress{ 26 | height: 4px; 27 | border-radius: 999px; 28 | background: #8590ae; 29 | } 30 | .star{ 31 | & path{ 32 | transition: all 0.2s ease; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/stores/base.store.ts: -------------------------------------------------------------------------------- 1 | import produce from 'immer' 2 | import { orderBy } from 'lodash' 3 | 4 | type PartialWithId = Partial & { id: number } 5 | 6 | const BaseStore = { 7 | clear(data: Map) { 8 | return produce(data, draft => { 9 | draft.clear() 10 | }) 11 | }, 12 | add(item: PartialWithId, data: Map) { 13 | return produce(data, draft => { 14 | draft.set(item.id, item) 15 | }) 16 | }, 17 | update(item: PartialWithId, data: Map) { 18 | return produce(data, draft => { 19 | draft.set(item.id, item) 20 | }) 21 | }, 22 | bulkAdd(items: PartialWithId[], data: Map) { 23 | return produce(data, draft => { 24 | items.forEach(i => draft.set(i.id, i)) 25 | }) 26 | }, 27 | remove(id: number, data: Map) { 28 | return produce(data, draft => { 29 | draft.delete(id) 30 | }) 31 | }, 32 | get(id: number | undefined, data: Map): T | undefined { 33 | if (id) { 34 | return data.get(id) 35 | } 36 | }, 37 | orderedData(data: Map): T[] { 38 | return orderBy(Array.from(data.values()), 'createdAt', 'desc') 39 | } 40 | } 41 | 42 | export default BaseStore 43 | -------------------------------------------------------------------------------- /src/stores/block.store.ts: -------------------------------------------------------------------------------- 1 | import useDb from '@/hooks/stores/useDb' 2 | import { useMemo } from 'react' 3 | 4 | const useBlockStore = () => { 5 | const db = useDb('blocks') 6 | const getBlockById = (id: number) => { 7 | return db.get(id) 8 | } 9 | const getBlocksByIds = (ids: number[]) => { 10 | return db.bulkGet(ids) 11 | } 12 | 13 | return useMemo( 14 | () => ({ 15 | getBlockById, 16 | getBlocksByIds 17 | }), 18 | [] 19 | ) 20 | } 21 | 22 | export default useBlockStore 23 | -------------------------------------------------------------------------------- /src/stores/editor.service.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eramax/Rendevoz-pdf/84405320c27a1b87b7bc44ff29c1c79072781a5d/src/stores/editor.service.ts -------------------------------------------------------------------------------- /src/stores/label.service.ts: -------------------------------------------------------------------------------- 1 | import { Label, ListData, RendevozResult } from '@/../typings/data' 2 | import { request } from '@/utils/request' 3 | import qs from 'qs' 4 | 5 | class LabelService { 6 | async addLabel (label: Label) { 7 | return request