├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── release.yml │ ├── rust.yml │ └── test.yml ├── .gitignore ├── CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── postcss.config.js ├── public ├── index.html └── splashscreen.html ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── migrations │ └── init │ │ ├── down.sql │ │ └── up.sql ├── rustfmt.toml ├── src │ ├── db.rs │ ├── feed.rs │ ├── files.rs │ ├── json.rs │ ├── main.rs │ ├── models.rs │ ├── paths.rs │ ├── plugins.rs │ ├── schema.rs │ ├── scripts │ │ └── init.js │ ├── storage.rs │ ├── tests.rs │ ├── tray.rs │ ├── tree │ │ ├── mod.rs │ │ ├── node.rs │ │ └── visitor.rs │ └── window.rs └── tauri.conf.json ├── src ├── .eslintrc.js ├── App.test.tsx ├── asset │ └── logo.svg ├── components │ ├── App.tsx │ ├── Logo.tsx │ ├── dir │ │ ├── DirDelModal.tsx │ │ ├── DirNewModal.tsx │ │ └── DirRenameModal.tsx │ ├── feed │ │ ├── ArticleView.tsx │ │ ├── AudioPlayer.tsx │ │ ├── Channel.tsx │ │ ├── ChannelList.tsx │ │ ├── FeedManager.tsx │ │ └── dataAgent.ts │ ├── kanban │ │ ├── Board.tsx │ │ ├── Card.tsx │ │ ├── Column.tsx │ │ ├── types.ts │ │ └── updateCard.ts │ ├── md │ │ ├── LICENSE │ │ ├── Markdown.tsx │ │ ├── ReactCodeMirror.tsx │ │ ├── darkTheme.ts │ │ └── useCodeMirror.ts │ ├── mindmap │ │ ├── LICENSE │ │ ├── mindmap.css │ │ └── mindmap.tsx │ ├── misc │ │ ├── Dropdown.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── Portal.tsx │ │ ├── Spinner.tsx │ │ ├── Toggle.tsx │ │ ├── Tooltip.tsx │ │ ├── Tree.tsx │ │ ├── TreeNodeElement.tsx │ │ └── VirtualTree.tsx │ ├── note │ │ ├── Note.tsx │ │ ├── NoteDelModal.tsx │ │ ├── NoteHeader.tsx │ │ ├── NoteMetadata.tsx │ │ ├── NoteMoveInput.tsx │ │ ├── NoteMoveModal.tsx │ │ ├── NoteNewInput.tsx │ │ ├── NoteNewModal.tsx │ │ ├── NoteSumList.tsx │ │ ├── Title.tsx │ │ ├── Toc.tsx │ │ └── backlinks │ │ │ ├── BacklinkBranch.tsx │ │ │ ├── BacklinkMatchLeaf.tsx │ │ │ ├── BacklinkNoteBranch.tsx │ │ │ ├── Backlinks.tsx │ │ │ ├── updateBacklinks.ts │ │ │ └── useBacklinks.ts │ ├── settings │ │ ├── AboutModal.tsx │ │ ├── BaseModal.tsx │ │ ├── SettingsModal.tsx │ │ └── SettingsToggle.tsx │ ├── sidebar │ │ ├── SideMenu.tsx │ │ ├── Sidebar.tsx │ │ ├── SidebarContent.tsx │ │ ├── SidebarDropdown.tsx │ │ ├── SidebarHeader.tsx │ │ ├── SidebarHistory.tsx │ │ ├── SidebarItem.tsx │ │ ├── SidebarNoteLink.tsx │ │ ├── SidebarNotes.tsx │ │ ├── SidebarNotesBar.tsx │ │ ├── SidebarNotesSortDropdown.tsx │ │ ├── SidebarNotesTree.tsx │ │ ├── SidebarPlaylist.tsx │ │ ├── SidebarSearch.tsx │ │ ├── SidebarTab.tsx │ │ ├── SidebarTags.tsx │ │ └── StatusBar.tsx │ └── view │ │ ├── ForceGraph.tsx │ │ ├── HeatMap.tsx │ │ ├── MainView.tsx │ │ ├── chronicle.tsx │ │ ├── feed.tsx │ │ ├── graph.tsx │ │ ├── hashtags.tsx │ │ ├── journals.tsx │ │ ├── kanban.tsx │ │ ├── md.tsx │ │ └── tasks.tsx ├── context │ ├── useCurrentMd.tsx │ ├── useCurrentView.tsx │ └── viewReducer.ts ├── editor │ └── hooks │ │ ├── useDebounce.ts │ │ ├── useDeleteNote.ts │ │ ├── useExport.ts │ │ ├── useHotkeys.ts │ │ ├── useNoteSearch.ts │ │ ├── useOnNoteLinkClick.ts │ │ ├── useOpen.ts │ │ └── useTasks.ts ├── file │ ├── directory.ts │ ├── files.ts │ ├── open.ts │ ├── process.ts │ ├── storage.ts │ ├── util.ts │ └── write.ts ├── index.tsx ├── lib │ ├── store.ts │ └── userSettings.ts ├── react-app-env.d.ts ├── setupTests.ts ├── styles │ ├── fonts │ │ ├── IBMPlexSerif-Regular.ttf │ │ ├── KaTeX_AMS-Regular.ttf │ │ ├── KaTeX_AMS-Regular.woff │ │ ├── KaTeX_AMS-Regular.woff2 │ │ ├── KaTeX_Caligraphic-Bold.ttf │ │ ├── KaTeX_Caligraphic-Bold.woff │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ ├── KaTeX_Caligraphic-Regular.ttf │ │ ├── KaTeX_Caligraphic-Regular.woff │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ ├── KaTeX_Fraktur-Bold.ttf │ │ ├── KaTeX_Fraktur-Bold.woff │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ ├── KaTeX_Fraktur-Regular.ttf │ │ ├── KaTeX_Fraktur-Regular.woff │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ ├── KaTeX_Main-Bold.ttf │ │ ├── KaTeX_Main-Bold.woff │ │ ├── KaTeX_Main-Bold.woff2 │ │ ├── KaTeX_Main-BoldItalic.ttf │ │ ├── KaTeX_Main-BoldItalic.woff │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ ├── KaTeX_Main-Italic.ttf │ │ ├── KaTeX_Main-Italic.woff │ │ ├── KaTeX_Main-Italic.woff2 │ │ ├── KaTeX_Main-Regular.ttf │ │ ├── KaTeX_Main-Regular.woff │ │ ├── KaTeX_Main-Regular.woff2 │ │ ├── KaTeX_Math-BoldItalic.ttf │ │ ├── KaTeX_Math-BoldItalic.woff │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ ├── KaTeX_Math-Italic.ttf │ │ ├── KaTeX_Math-Italic.woff │ │ ├── KaTeX_Math-Italic.woff2 │ │ ├── KaTeX_SansSerif-Bold.ttf │ │ ├── KaTeX_SansSerif-Bold.woff │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ ├── KaTeX_SansSerif-Italic.ttf │ │ ├── KaTeX_SansSerif-Italic.woff │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ ├── KaTeX_SansSerif-Regular.ttf │ │ ├── KaTeX_SansSerif-Regular.woff │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ ├── KaTeX_Script-Regular.ttf │ │ ├── KaTeX_Script-Regular.woff │ │ ├── KaTeX_Script-Regular.woff2 │ │ ├── KaTeX_Size1-Regular.ttf │ │ ├── KaTeX_Size1-Regular.woff │ │ ├── KaTeX_Size1-Regular.woff2 │ │ ├── KaTeX_Size2-Regular.ttf │ │ ├── KaTeX_Size2-Regular.woff │ │ ├── KaTeX_Size2-Regular.woff2 │ │ ├── KaTeX_Size3-Regular.ttf │ │ ├── KaTeX_Size3-Regular.woff │ │ ├── KaTeX_Size3-Regular.woff2 │ │ ├── KaTeX_Size4-Regular.ttf │ │ ├── KaTeX_Size4-Regular.woff │ │ ├── KaTeX_Size4-Regular.woff2 │ │ ├── KaTeX_Typewriter-Regular.ttf │ │ ├── KaTeX_Typewriter-Regular.woff │ │ ├── KaTeX_Typewriter-Regular.woff2 │ │ ├── RobotoMono-Regular.ttf │ │ └── Sniglet-Regular.ttf │ ├── index.css │ └── styles.css ├── types │ ├── model.ts │ └── utils.d.ts └── utils │ ├── file-extensions.ts │ └── helper.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Screenshots** 19 | If applicable, add screenshots to help explain your problem. 20 | 21 | **Desktop (please complete the following information):** 22 | 23 | - OS Version [e.g. Windows 10 21H1, macOS Catalina 10.15.7, or Ubuntu 20.04] 24 | - mdSilo version 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | 29 | **Are you willing to submit a PR?** 30 | - [ ] I'm willing to submit a PR! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | 21 | **Are you willing to submit a PR?** 22 | - [ ] I'm willing to submit a PR! -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: 'monthly' 12 | rebase-strategy: 'disabled' 13 | open-pull-requests-limit: 20 14 | 15 | - package-ecosystem: 'cargo' 16 | directory: '/src-tauri' 17 | schedule: 18 | interval: 'monthly' 19 | rebase-strategy: 'disabled' 20 | open-pull-requests-limit: 20 21 | 22 | - package-ecosystem: "github-actions" 23 | directory: "/" 24 | schedule: 25 | interval: "monthly" 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: 'Release' 2 | on: 3 | # Allows you to run this workflow manually from the Actions tab 4 | workflow_dispatch: 5 | 6 | jobs: 7 | publish-mdsilo: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - build: linux 13 | os: ubuntu-latest 14 | arch: x86_64 15 | target: x86_64-unknown-linux-gnu 16 | - build: macos 17 | os: macos-latest 18 | arch: x86_64 19 | target: x86_64-apple-darwin 20 | - buid: macos 21 | os: macos-latest 22 | arch: aarch64 23 | target: aarch64-apple-darwin 24 | - build: windows 25 | os: windows-latest 26 | arch: x86_64 27 | target: x86_64-pc-windows-msvc 28 | 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: setup node 33 | uses: actions/setup-node@v4 34 | with: 35 | node-version: 18 36 | - name: install Rust stable 37 | uses: actions-rs/toolchain@v1 38 | with: 39 | toolchain: stable 40 | - name: install webkit2gtk (ubuntu only) 41 | if: matrix.os == 'ubuntu-latest' 42 | run: | 43 | sudo apt-get update 44 | sudo apt-get install -y webkit2gtk-4.0 45 | sudo apt-get install -y libayatana-appindicator3-dev 46 | - name: install app dependencies and build it 47 | run: yarn install --network-timeout 1000000 && yarn build 48 | - uses: tauri-apps/tauri-action@v0.5 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 51 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} 52 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} 53 | with: 54 | tagName: app-v__VERSION__ 55 | releaseName: 'mdSilo Desktop v__VERSION__' 56 | releaseBody: 'See the assets to download this version and install.' 57 | releaseDraft: true 58 | prerelease: false 59 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Cargo Test 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | platform: [macos-latest, ubuntu-latest, windows-latest] 11 | 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: install Rust stable 16 | uses: actions-rs/toolchain@v1 17 | with: 18 | toolchain: stable 19 | - name: install webkit2gtk (ubuntu only) 20 | if: matrix.platform == 'ubuntu-latest' 21 | run: | 22 | sudo apt-get update 23 | sudo apt-get install -y webkit2gtk-4.0 24 | - name: Run tests 25 | run: mkdir build && cd src-tauri && cargo test 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint, TypeCheck, Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 18 18 | 19 | - run: yarn && yarn lint 20 | - run: yarn tc 21 | - run: yarn test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | devlog.md 26 | tsconfig.tsbuildinfo 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to mdSilo 2 | 3 | Thank you for considering contributing to mdSilo! We welcome your contributions to help make this note app even better. 4 | 5 | Before you start contributing, please take a moment to read and understand this document to ensure a smooth collaboration process. 6 | 7 | ## Table of Contents 8 | - [Getting Started](#getting-started) 9 | - [How Can I Contribute?](#how-can-i-contribute) 10 | - [Coding Guidelines](#coding-guidelines) 11 | - [Submitting Changes](#submitting-changes) 12 | - [Community and Communication](#community-and-communication) 13 | - [License](#license) 14 | 15 | ## Getting Started 16 | 17 | Before you can start contributing to mdSilo, make sure you have the following prerequisites: 18 | 19 | - [Node.js](https://nodejs.org/en/download) and npm or yarn installed 20 | - [Rust](https://www.rust-lang.org/tools/install) and Cargo Installed 21 | - [Git](https://git-scm.com/downloads) installed 22 | - Familiarity with [Tauri](https://tauri.app/) and [React](https://react.dev/) 23 | 24 | Now, follow these steps: 25 | 26 | 1. Fork the [mdSilo repository](https://github.com/mdsilo/mdSilo-app) on GitHub. 27 | 2. Clone your forked repository to your local machine. 28 | 3. Install project dependencies by running `npm run tauri dev` or `yarn install` in the project root. 29 | 4. Start the development server with `npm run tauri dev` or `yarn run tauri dev`. 30 | 5. Make your changes, create new features, or fix bugs. 31 | 32 | ## How Can I Contribute? 33 | 34 | You can contribute to mdSilo in several ways: 35 | 36 | - Report bugs and suggest improvements by opening issues. 37 | - Submit pull requests to add new features or fix existing issues. 38 | - Participate in discussions and provide feedback on existing issues and pull requests. 39 | 40 | ## Coding Guidelines 41 | 42 | Please follow these guidelines when contributing code to mdSilo: 43 | 44 | - Follow the coding style and conventions used in the project. 45 | - Write clear and concise code with appropriate comments where necessary. 46 | - Ensure your changes do not introduce linting errors or break existing functionality. 47 | - Test your changes thoroughly before submitting a pull request. 48 | 49 | ## Submitting Changes 50 | 51 | To submit your changes, follow these steps: 52 | 53 | 1. Create a new branch for your changes: `git checkout -b my-feature-branch`. 54 | 2. Commit your changes with descriptive commit messages. 55 | 3. Push your changes to your forked repository: `git push origin my-feature-branch`. 56 | 4. Open a pull request on the [mdSilo repository](https://github.com/mdSilo/mdSilo-app) with details about your changes. 57 | 58 | A maintainer will review your pull request, provide feedback, and merge it once it meets the project's standards. 59 | 60 | ## Community and Communication 61 | 62 | We value and encourage open communication within the mdSilo community: 63 | 64 | - Join our [Discord server](https://discord.gg/EXYSEHRTFt) to interact with other contributors and users. 65 | - Participate in discussions on GitHub issues and pull requests. 66 | - Follow us on social media for updates and announcements. 67 | 68 | ## License 69 | 70 | By contributing to mdSilo, you agree that your contributions will be licensed under the [AGPL-3.0 License](LICENSE). 71 | 72 | ## Code of Conduct 73 | 74 | Please note that mdSilo follows the Contributor Covenant [Code of Conduct](CODE_OF_CONDUCT.md). Please review and adhere to it in all your interactions with the project. 75 | 76 | ## Thank You 77 | 78 | Thank you for contributing to mdSilo. Your contributions help make this project better for everyone. We appreciate your time and effort in helping us achieve our goals. 79 | 80 | Happy coding! 81 | 82 | Feel free to adjust this CONTRIBUTING.md to fit the specific guidelines and processes of your mdSilo project. 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdsilo-app", 3 | "version": "0.5.10", 4 | "private": true, 5 | "license": "AGPL-3.0-or-later", 6 | "homepage": "https://mdsilo.com", 7 | "scripts": { 8 | "start": "react-scripts start", 9 | "build": "react-scripts build", 10 | "eject": "react-scripts eject", 11 | "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!@tauri-apps)/\"", 12 | "lint": "eslint . --ext .tsx,.ts", 13 | "fix": "prettier --write src", 14 | "tc": "tsc", 15 | "tauri": "tauri" 16 | }, 17 | "dependencies": { 18 | "codemirror": "6.0.0", 19 | "@codemirror/commands": "6.0.0", 20 | "@codemirror/lang-markdown": "6.0.0", 21 | "@codemirror/lang-json": "6.0.0", 22 | "@codemirror/language-data": "6.0.0", 23 | "@codemirror/state": "6.0.0", 24 | "@codemirror/view": "6.0.0", 25 | 26 | "@headlessui/react": "^1.7.19", 27 | "@popperjs/core": "^2.11.8", 28 | "@react-spring/web": "^9.7.3", 29 | "@tabler/icons-react": "^2.42.0", 30 | "@tailwindcss/forms": "^0.5.7", 31 | "@tailwindcss/typography": "^0.5.12", 32 | "@tauri-apps/api": "1.6.0", 33 | "@tippyjs/react": "^4.2.6", 34 | "@dnd-kit/core": "^6.1.0", 35 | "@dnd-kit/sortable": "^8.0.0", 36 | 37 | "d3-drag": "^3.0.0", 38 | "d3-force": "^3.0.0", 39 | "d3-selection": "^3.0.0", 40 | "d3-zoom": "^3.0.0", 41 | "fuse.js": "^6.6.2", 42 | "immer": "9.0.21", 43 | "is-hotkey": "^0.2.0", 44 | "mdsmirror": "0.0.31", 45 | "mdsmap": "0.0.3", 46 | "html2canvas": "1.4.1", 47 | "jspdf": "2.5.1", 48 | "react": "17.0.2", 49 | "react-dom": "17.0.2", 50 | "react-highlight-words": "^0.20.0", 51 | "react-popper": "^2.3.0", 52 | "react-scripts": "^5.0.1", 53 | "react-virtual": "^2.10.4", 54 | "react-virtualized": "^9.22.5", 55 | "styled-components": "5.3.6", 56 | "tailwindcss": "^3.4.3", 57 | "zustand": "^3.7.2" 58 | }, 59 | "devDependencies": { 60 | "@tauri-apps/cli": "1.6.3", 61 | "@testing-library/jest-dom": "^5.16.5", 62 | "@testing-library/react": "^12.1.4", 63 | "@types/d3-drag": "^3.0.7", 64 | "@types/d3-force": "^3.0.10", 65 | "@types/d3-selection": "^3.0.10", 66 | "@types/d3-zoom": "^3.0.8", 67 | "@types/is-hotkey": "^0.1.10", 68 | "@types/jest": "^29.1.2", 69 | "@types/node": "^20.11.19", 70 | "@types/react": "^17.0.43", 71 | "@types/react-dom": "^17.0.14", 72 | "@types/react-highlight-words": "^0.16.7", 73 | "@types/react-virtualized": "^9.21.29", 74 | "@types/tailwindcss": "^3.1.0", 75 | "@typescript-eslint/eslint-plugin": "^5.40.0", 76 | "@typescript-eslint/parser": "^5.40.0", 77 | "autoprefixer": "^10.4.17", 78 | "eslint": "8.25.0", 79 | "eslint-config-prettier": "^8.5.0", 80 | "eslint-import-resolver-typescript": "^3.5.1", 81 | "eslint-plugin-import": "^2.26.0", 82 | "eslint-plugin-jest-dom": "^4.0.2", 83 | "eslint-plugin-react": "^7.27.1", 84 | "eslint-plugin-react-hooks": "^4.6.0", 85 | "eslint-plugin-testing-library": "^5.7.2", 86 | "postcss": "8.4.22", 87 | "prettier": "2.7.1", 88 | "typescript": "^5.1.6" 89 | }, 90 | "browserslist": { 91 | "production": [ 92 | ">0.2%", 93 | "not dead", 94 | "not op_mini all" 95 | ], 96 | "development": [ 97 | "last 1 chrome version", 98 | "last 1 firefox version", 99 | "last 1 safari version" 100 | ] 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mdSilo 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /public/splashscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mdSilo 8 | 33 | 34 | 35 | 36 |
37 | 38 |

mdSilo

39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | WixTools 5 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mdsilo" 3 | version = "0.5.10" 4 | description = "mdSilo Desktop" 5 | authors = ["dloh"] 6 | license = "AGPL-3.0-or-later" 7 | repository = "" 8 | default-run = "mdsilo" 9 | edition = "2021" 10 | rust-version = "1.85.0" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [build-dependencies] 15 | tauri-build = { version = "1.5.6", features = [] } 16 | 17 | [dependencies] 18 | serde_json = "1.0.140" 19 | serde = { version = "1.0.219", features = ["derive"] } 20 | tauri = { version = "1.8.3", features = ["clipboard-all", "dialog-all", "protocol-all", "shell-all", "system-tray", "updater", "window-all"] } 21 | trash = "5.2.2" 22 | notify = "6.1.1" 23 | open = "5.3.2" 24 | bincode = { version = "2.0", features = ["serde"] } 25 | chrono = "0.4.41" 26 | whatlang = "0.16.4" 27 | # walk dir 28 | crossbeam = "0.8.4" 29 | ignore = "0.4.23" 30 | indextree = "4.7.4" 31 | ## rss reader 32 | reqwest = { version = "0.12.15", features = ["json", "socks"] } 33 | rss = { version = "2.0.12", features = ["serde"] } 34 | atom_syndication = "0.12.7" 35 | bytes = "1.10.1" 36 | diesel = { version = "2.2.10", features = ["sqlite", "chrono"] } 37 | diesel_migrations = { version = "2.2.0", features = ["sqlite"] } 38 | libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } 39 | 40 | [dev-dependencies] 41 | tokio = { version = "1.45.1", features = ["full"] } 42 | 43 | [features] 44 | # by default Tauri runs in production mode 45 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 46 | default = [ "custom-protocol" ] 47 | # this feature is used used for production builds where `devPath` points to the filesystem 48 | # DO NOT remove this 49 | custom-protocol = [ "tauri/custom-protocol" ] 50 | 51 | [profile.release] 52 | strip = true 53 | lto = true 54 | opt-level = "s" 55 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/migrations/init/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | DROP TABLE channels; 3 | DROP TABLE articles; 4 | -------------------------------------------------------------------------------- /src-tauri/migrations/init/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | DROP TABLE IF EXISTS channels; 3 | 4 | CREATE TABLE channels ( 5 | id INTEGER NOT NULL PRIMARY KEY, 6 | title VARCHAR NOT NULL, 7 | link VARCHAR NOT NULL UNIQUE, 8 | description VARCHAR, 9 | published DATETIME, 10 | ty VARCHAR NOT NULL DEFAULT 'rss' 11 | ); 12 | 13 | DROP TABLE IF EXISTS articles; 14 | 15 | CREATE TABLE articles ( 16 | id INTEGER NOT NULL PRIMARY KEY, 17 | title VARCHAR NOT NULL, 18 | url VARCHAR NOT NULL UNIQUE, 19 | feed_link VARCHAR NOT NULL, 20 | audio_url VARCHAR NOT NULL DEFAULT '', 21 | description VARCHAR NOT NULL, 22 | published DATETIME, 23 | content VARCHAR, 24 | author VARCHAR, 25 | image VARCHAR, 26 | read_status INTEGER NOT NULL DEFAULT 0, -- 0: unread 1: read 27 | star_status INTEGER NOT NULL DEFAULT 0 -- 0: unstar 1: star-ed 28 | ); 29 | 30 | -- DROP TABLE IF EXISTS notes; 31 | 32 | -- CREATE TABLE notes ( 33 | -- id VARCHAR NOT NULL PRIMARY KEY, -- root dir 34 | -- content TEXT NOT NULL, 35 | -- saved DATETIME, 36 | -- ); 37 | -------------------------------------------------------------------------------- /src-tauri/rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 85 2 | tab_spaces = 2 3 | -------------------------------------------------------------------------------- /src-tauri/src/json.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | // use crate::db; 3 | use crate::files::{read_dir, write_file, EventPayload}; 4 | use crate::storage::get_data; 5 | use crate::tree::assemble_note_tree; 6 | // use crate::models::Note; 7 | 8 | #[derive(serde::Serialize, Clone, Debug, Default)] 9 | pub struct NoteData { 10 | pub id: String, // !!Important!! id === file_path 11 | pub title: String, 12 | pub content: String, 13 | pub file_path: String, 14 | pub cover: String, 15 | pub created_at: String, 16 | pub updated_at: String, 17 | pub is_daily: bool, 18 | pub is_dir: bool, 19 | } 20 | 21 | pub type NotesData = HashMap; 22 | 23 | #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] 24 | pub struct NoteTreeItem { 25 | pub id: String, 26 | pub title: String, 27 | pub created_at: String, 28 | pub updated_at: String, 29 | pub is_dir: bool, 30 | } 31 | 32 | pub type NoteTree = HashMap>; 33 | 34 | #[derive(serde::Serialize, serde::Deserialize, Clone, Debug)] 35 | pub struct ActivityData { 36 | activity_num: u32, 37 | create_num: u32, 38 | update_num: u32, 39 | } 40 | 41 | pub type ActivityRecord = HashMap; 42 | 43 | #[derive(serde::Serialize, Clone, Debug)] 44 | pub struct JsonData { 45 | isloaded: bool, 46 | notesobj: NotesData, 47 | notetree: NoteTree, 48 | activities: ActivityRecord, 49 | } 50 | 51 | pub async fn load_dir(dir: &str) -> (NotesData, NoteTree) { 52 | // init data and tree 53 | let mut notes_data: NotesData = HashMap::new(); 54 | let mut notes_tree: NoteTree = HashMap::new(); 55 | // load_dir_recursively(dir, &mut notes_data, &mut notes_tree).await; 56 | if let Ok(tree) = read_dir(dir) { 57 | let root = tree.root; 58 | let inner = tree.inner(); 59 | assemble_note_tree(root, inner, &mut notes_data, &mut notes_tree); 60 | } 61 | 62 | return (notes_data, notes_tree); 63 | } 64 | 65 | // get activity in storage 66 | pub fn get_activity_data() -> ActivityRecord { 67 | let store_data = get_data(String::from("activities")).unwrap_or_default(); 68 | let data = store_data.data; 69 | let activities: ActivityRecord = serde_json::from_value(data).unwrap_or_default(); 70 | 71 | return activities; 72 | } 73 | 74 | #[tauri::command] 75 | pub async fn write_json(dir: String, window: tauri::Window) -> bool { 76 | let notes_data = load_dir(&dir).await; 77 | let activity = get_activity_data(); 78 | let data = JsonData { 79 | isloaded: true, 80 | notesobj: notes_data.0, 81 | notetree: notes_data.1, 82 | activities: activity, 83 | }; 84 | 85 | let json = serde_json::to_string(&data).unwrap_or_default(); 86 | let to_dir = format!("{}/mdsilo.json", dir); 87 | let res = write_file(to_dir, json).await; 88 | 89 | // println!("loaded dir: {} ? -> {}", dir, res); 90 | 91 | // read files and write to json on rust end, emit event; 92 | // listen loaded event on ts end, then load to store 93 | // frontend: src/file/directory.ts/DirectoryAPI/listen 94 | window 95 | .emit( 96 | "changes", 97 | EventPayload { 98 | paths: vec![dir], 99 | event: if res { 100 | String::from("loaded") 101 | } else { 102 | String::from("unloaded") 103 | }, 104 | }, 105 | ) 106 | .unwrap_or(()); 107 | 108 | return res; 109 | } 110 | 111 | // #[tauri::command] 112 | // pub async fn save_notes(dir: String, content: String) -> usize { 113 | 114 | // let data = Note { 115 | // id: dir, 116 | // content, 117 | // saved: Utc::now().to_string(), 118 | // }; 119 | 120 | // db::save_notes(data) 121 | // } 122 | 123 | // #[tauri::command] 124 | // pub async fn get_notes(dir: String) -> Option { 125 | // db::get_notes_by_id(dir) 126 | // } 127 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | mod db; 7 | mod feed; 8 | mod files; 9 | mod json; 10 | mod models; 11 | mod paths; 12 | mod schema; 13 | mod storage; 14 | mod tests; 15 | mod tray; 16 | mod tree; 17 | mod window; 18 | mod plugins; 19 | 20 | extern crate diesel; 21 | extern crate diesel_migrations; 22 | 23 | use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; 24 | use tauri::Manager; 25 | 26 | #[tauri::command] 27 | async fn close_splashscreen(window: tauri::Window) { 28 | // Close splashscreen 29 | if let Some(splashscreen) = window.get_window("splashscreen") { 30 | splashscreen.close().unwrap_or(()); 31 | } 32 | // Show main window 33 | if let Some(mainwindow) = window.get_window("main") { 34 | mainwindow.show().unwrap_or(()); 35 | } 36 | } 37 | 38 | pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); 39 | 40 | fn main() { 41 | let mut connection = db::establish_connection(); 42 | connection 43 | .run_pending_migrations(MIGRATIONS) 44 | .expect("Error on migrating"); 45 | 46 | tauri::Builder::default() 47 | .plugin(plugins::inject_plugin()) 48 | .invoke_handler(tauri::generate_handler![ 49 | close_splashscreen, 50 | window::msg_dialog, 51 | window::web_window, 52 | feed::fetch_feed, 53 | feed::add_channel, 54 | feed::import_channels, 55 | feed::get_channels, 56 | feed::delete_channel, 57 | feed::add_articles_with_channel, 58 | feed::get_articles, 59 | feed::get_article_by_url, 60 | feed::update_article_read_status, 61 | feed::update_article_star_status, 62 | feed::get_unread_num, 63 | feed::update_all_read_status, 64 | files::read_directory, 65 | files::is_dir, 66 | files::is_file, 67 | files::get_basename, 68 | files::get_dirpath, 69 | files::get_parent_dir, 70 | files::join_paths, 71 | files::get_file_meta, 72 | files::file_exist, 73 | files::create_dir_recursive, 74 | files::create_file, 75 | files::read_file, 76 | files::write_file, 77 | files::download_file, 78 | files::rename_file, 79 | files::copy_file, 80 | files::copy_file_to_assets, 81 | files::delete_files, 82 | files::list_directory, 83 | files::listen_dir, 84 | files::open_url, 85 | files::open_link, 86 | files::detect_lang, 87 | files::watch_event, 88 | storage::create_mdsilo_dir, 89 | storage::set_data, 90 | storage::get_data, 91 | storage::delete_data, 92 | storage::set_log, 93 | storage::get_log, 94 | storage::del_log, 95 | json::write_json, 96 | // json::save_notes, 97 | // json::get_notes, 98 | ]) 99 | .system_tray(tray::menu()) 100 | .on_system_tray_event(tray::handler) 101 | .run(tauri::generate_context!()) 102 | .expect("error while running"); 103 | } 104 | -------------------------------------------------------------------------------- /src-tauri/src/models.rs: -------------------------------------------------------------------------------- 1 | use super::schema::{articles, channels}; 2 | use diesel::{sql_types::*, Insertable, Queryable, QueryableByName}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Queryable, Serialize, QueryableByName)] 6 | pub struct Channel { 7 | #[diesel(sql_type = Integer)] 8 | pub id: i32, 9 | #[diesel(sql_type = Text)] 10 | pub title: String, 11 | #[diesel(sql_type = Text)] 12 | pub link: String, 13 | #[diesel(sql_type = Text)] 14 | pub description: String, 15 | #[diesel(sql_type = Text)] 16 | pub published: String, 17 | #[diesel(sql_type = Text)] 18 | pub ty: String, // podcast || rss 19 | } 20 | 21 | #[derive(Debug, Queryable, Serialize, QueryableByName)] 22 | pub struct Article { 23 | #[diesel(sql_type = Integer)] 24 | pub id: i32, 25 | #[diesel(sql_type = Text)] 26 | pub title: String, 27 | #[diesel(sql_type = Text)] 28 | pub url: String, 29 | #[diesel(sql_type = Text)] 30 | pub feed_link: String, 31 | #[diesel(sql_type = Text)] 32 | pub audio_url: String, 33 | #[diesel(sql_type = Text)] 34 | pub description: String, 35 | #[diesel(sql_type = Text)] 36 | pub published: String, 37 | #[diesel(sql_type = Text)] 38 | pub content: String, 39 | #[diesel(sql_type = Text)] 40 | pub author: String, 41 | #[diesel(sql_type = Text)] 42 | pub image: String, 43 | #[diesel(sql_type = Integer)] 44 | pub read_status: i32, 45 | #[diesel(sql_type = Integer)] 46 | pub star_status: i32, 47 | } 48 | 49 | #[derive(Debug, Insertable, Serialize, Deserialize)] 50 | #[diesel(table_name = channels)] 51 | pub struct NewChannel { 52 | pub title: String, 53 | pub link: String, 54 | pub description: String, 55 | pub published: String, 56 | pub ty: String, 57 | } 58 | 59 | #[derive(Debug, Insertable, Clone, Serialize, Deserialize)] 60 | #[diesel(table_name = articles)] 61 | pub struct NewArticle { 62 | pub title: String, 63 | pub url: String, 64 | pub feed_link: String, 65 | pub audio_url: String, 66 | pub description: String, 67 | pub content: String, 68 | pub published: String, 69 | pub author: String, 70 | pub image: String, 71 | } 72 | 73 | // #[derive(Debug, Insertable, Queryable, Serialize, QueryableByName)] 74 | // #[diesel(table_name = notes)] 75 | // pub struct Note { 76 | // #[diesel(sql_type = Text)] 77 | // pub id: String, 78 | // #[diesel(sql_type = Text)] 79 | // pub content: String, 80 | // #[diesel(sql_type = Text)] 81 | // pub saved: String, 82 | // } 83 | 84 | // TODO: save daily activities to db 85 | -------------------------------------------------------------------------------- /src-tauri/src/plugins.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | api::path::local_data_dir, 3 | Runtime, generate_handler, 4 | plugin::{Builder, TauriPlugin}, 5 | }; 6 | 7 | use std::path::Path; 8 | 9 | pub const INIT_SCRIPT: &str = include_str!("./scripts/init.js"); 10 | 11 | pub fn inject_plugin() -> TauriPlugin { 12 | Builder::new("inject") 13 | .invoke_handler(generate_handler![]) 14 | .js_init_script(inject_script(None)) 15 | .build() 16 | } 17 | 18 | pub fn inject_script(script_path: Option) -> String { 19 | // inject js script 20 | let mut script = format!("// ## Script Injection ## \n\n {INIT_SCRIPT}"); 21 | // TODO: set the script dir or default dir is `local_data_dir/mdsilo/plugins` 22 | let script_path = script_path.unwrap_or_else(|| { 23 | if let Some(local_dir) = local_data_dir() { 24 | Path::new(&local_dir).join("mdsilo/plugins").display().to_string() 25 | } else { 26 | String::new() 27 | } 28 | }); 29 | 30 | if !script_path.is_empty() { 31 | // check is dir or file and read all files 32 | let file_path = Path::new(&script_path); 33 | if file_path.is_dir() { 34 | if let Ok(paths) = std::fs::read_dir(file_path) { 35 | for path in paths { 36 | if let Ok(file_path_) = path { 37 | let file = file_path_.path().display().to_string(); 38 | let script_content = 39 | std::fs::read_to_string(&file).unwrap_or_default(); 40 | script += &format!("{script_content}\n"); 41 | } 42 | } 43 | } 44 | } else if file_path.is_file() { 45 | let script_content = 46 | std::fs::read_to_string(&script_path).unwrap_or_default(); 47 | script += &format!("{script_content}\n"); 48 | } 49 | } 50 | 51 | script 52 | } 53 | -------------------------------------------------------------------------------- /src-tauri/src/schema.rs: -------------------------------------------------------------------------------- 1 | // @generated automatically by Diesel CLI. 2 | 3 | diesel::table! { 4 | articles (id) { 5 | id -> Integer, 6 | title -> Text, 7 | url -> Text, 8 | feed_link -> Text, 9 | audio_url -> Text, 10 | description -> Text, 11 | published -> Timestamp, 12 | content -> Text, 13 | author -> Text, 14 | image -> Text, 15 | read_status -> Integer, 16 | star_status -> Integer, 17 | } 18 | } 19 | 20 | diesel::table! { 21 | channels (id) { 22 | id -> Integer, 23 | title -> Text, 24 | link -> Text, 25 | description -> Text, 26 | published -> Timestamp, 27 | ty -> Text, 28 | } 29 | } 30 | 31 | // diesel::table! { 32 | // notes (id) { 33 | // id -> Text, 34 | // content -> Text, 35 | // saved -> Timestamp, 36 | // } 37 | // } 38 | 39 | diesel::allow_tables_to_appear_in_same_query!(articles, channels,); 40 | -------------------------------------------------------------------------------- /src-tauri/src/scripts/init.js: -------------------------------------------------------------------------------- 1 | // *** Core Script - IPC *** 2 | // copy from: https://github.com/lencx/ChatGPT/blob/main/src-tauri/src/scripts/core.js 3 | // under GNU Affero General Public License v3.0 4 | 5 | const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0]; 6 | 7 | function transformCallback(callback = () => {}, once = false) { 8 | const identifier = uid(); 9 | const prop = `_${identifier}`; 10 | Object.defineProperty(window, prop, { 11 | value: (result) => { 12 | if (once) { 13 | Reflect.deleteProperty(window, prop); 14 | } 15 | return callback(result) 16 | }, 17 | writable: false, 18 | configurable: true, 19 | }) 20 | return identifier; 21 | } 22 | 23 | async function invoke(cmd, args) { 24 | return new Promise((resolve, reject) => { 25 | if (!window.__TAURI_POST_MESSAGE__) { 26 | reject('__TAURI_POST_MESSAGE__ does not exist!'); 27 | } 28 | 29 | const callback = transformCallback((e) => { 30 | resolve(e); 31 | Reflect.deleteProperty(window, `_${error}`); 32 | }, true) 33 | 34 | const error = transformCallback((e) => { 35 | reject(e); 36 | Reflect.deleteProperty(window, `_${callback}`); 37 | }, true) 38 | 39 | window.__TAURI_POST_MESSAGE__({ 40 | cmd, 41 | callback, 42 | error, 43 | ...args 44 | }); 45 | }); 46 | } 47 | 48 | async function message(message) { 49 | invoke('messageDialog', { 50 | __tauriModule: 'Dialog', 51 | message: { 52 | cmd: 'messageDialog', 53 | message: message.toString(), 54 | title: null, 55 | type: null, 56 | buttonLabel: null 57 | } 58 | }); 59 | } 60 | 61 | // functions availabe: 62 | // invoke(): to call commands on rust backend 63 | // events available: 64 | // PageLoaded event with note id 65 | 66 | window.uid = uid; 67 | window.invoke = invoke; 68 | window.message = message; 69 | window.transformCallback = transformCallback; 70 | 71 | -------------------------------------------------------------------------------- /src-tauri/src/tray.rs: -------------------------------------------------------------------------------- 1 | use tauri::Manager; 2 | use tauri::{ 3 | AppHandle, CustomMenuItem, SystemTray, SystemTrayEvent, SystemTrayMenu, 4 | SystemTrayMenuItem, 5 | }; 6 | 7 | pub const MAIN_WIN: &str = "main"; 8 | 9 | pub fn menu() -> SystemTray { 10 | let show = CustomMenuItem::new("show".to_string(), "Show"); 11 | let hide = CustomMenuItem::new("hide".to_string(), "Hide"); 12 | let quit = CustomMenuItem::new("quit".to_string(), "Quit"); 13 | let tray_menu = SystemTrayMenu::new() 14 | .add_item(show) 15 | .add_item(hide) 16 | .add_native_item(SystemTrayMenuItem::Separator) 17 | .add_item(quit); 18 | 19 | #[cfg(target_os = "macos")] 20 | { 21 | SystemTray::new() 22 | .with_menu(tray_menu) 23 | .with_menu_on_left_click(false) 24 | } 25 | 26 | #[cfg(not(target_os = "macos"))] 27 | { 28 | SystemTray::new().with_menu(tray_menu) 29 | } 30 | } 31 | 32 | pub fn handler(app: &AppHandle, event: SystemTrayEvent) { 33 | match event { 34 | SystemTrayEvent::LeftClick { 35 | position: _, 36 | size: _, 37 | .. 38 | } => { 39 | if let Some(window) = app.get_window(MAIN_WIN) { 40 | window.set_focus().unwrap_or(()); 41 | window.unminimize().unwrap_or(()); 42 | window.show().unwrap_or(()); 43 | } 44 | } 45 | SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { 46 | "show" => { 47 | if let Some(window) = app.get_window(MAIN_WIN) { 48 | window.set_focus().unwrap_or(()); 49 | window.show().unwrap_or(()); 50 | } 51 | } 52 | "hide" => { 53 | if let Some(window) = app.get_window(MAIN_WIN) { 54 | window.set_focus().unwrap_or(()); 55 | window.unminimize().unwrap_or(()); 56 | window.hide().unwrap_or(()); 57 | } 58 | } 59 | "quit" => app.exit(0), 60 | _ => {} // TODO: MORE EVENT 61 | }, 62 | _ => {} 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src-tauri/src/tree/visitor.rs: -------------------------------------------------------------------------------- 1 | use super::Node; 2 | use crossbeam::channel::Sender; 3 | use ignore::{ 4 | DirEntry, Error as IgnoreError, ParallelVisitor, ParallelVisitorBuilder, WalkState, 5 | }; 6 | 7 | pub enum TraversalState { 8 | Ongoing(Node), 9 | Done, 10 | } 11 | 12 | pub struct BranchVisitor { 13 | tx: Sender, 14 | ctn: bool, 15 | } 16 | 17 | pub struct BranchVisitorBuilder { 18 | tx: Sender, 19 | ctn: bool, 20 | } 21 | 22 | impl BranchVisitorBuilder { 23 | pub fn new(tx: Sender, ctn: bool) -> Self { 24 | Self { tx, ctn } 25 | } 26 | } 27 | 28 | impl BranchVisitor { 29 | pub fn new(tx: Sender, ctn: bool) -> Self { 30 | Self { tx, ctn } 31 | } 32 | } 33 | 34 | impl From for TraversalState { 35 | fn from(node: Node) -> Self { 36 | TraversalState::Ongoing(node) 37 | } 38 | } 39 | 40 | impl ParallelVisitor for BranchVisitor { 41 | fn visit(&mut self, entry: Result) -> WalkState { 42 | entry 43 | .map(|e| TraversalState::from(Node::from((&e, self.ctn)))) 44 | .map(|n| self.tx.send(n).unwrap()) 45 | .map(|_| WalkState::Continue) 46 | .unwrap_or_else(|_| WalkState::Skip) 47 | } 48 | } 49 | 50 | impl<'s> ParallelVisitorBuilder<'s> for BranchVisitorBuilder { 51 | fn build(&mut self) -> Box { 52 | let visitor = BranchVisitor::new(self.tx.clone(), self.ctn); 53 | Box::new(visitor) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src-tauri/src/window.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use tauri::{api::dialog, Manager}; 3 | 4 | use crate::files::read_directory; 5 | 6 | pub const INIT_SCRIPT: &str = include_str!("./scripts/init.js"); 7 | 8 | #[tauri::command] 9 | pub async fn web_window( 10 | app: tauri::AppHandle, 11 | label: String, 12 | title: String, 13 | url: String, 14 | script_path: Option, // script path: can be file or dir 15 | ) { 16 | // inject js script 17 | let mut inject_script = format!("// ## [{title}] Script Injection ## \n\n"); 18 | let script_path = script_path.unwrap_or_default(); 19 | if !script_path.is_empty() { 20 | // check is dir or file and read all files 21 | let file_path = Path::new(&script_path); 22 | if file_path.is_dir() { 23 | let dir_data = read_directory(&script_path).await; 24 | if let Ok(data) = dir_data { 25 | let dir_files = data.files; 26 | for file in dir_files { 27 | let file_script = file.file_text; 28 | inject_script += &format!("{file_script}\n\n"); 29 | } 30 | } 31 | } else if file_path.is_file() { 32 | let script_content = 33 | std::fs::read_to_string(&script_path).unwrap_or_else(|msg| { 34 | let main_window = app.get_window("main").unwrap(); 35 | let err_msg = format!("[app.items.script] {}\n{}", script_path, msg); 36 | dialog::message(Some(&main_window), &title, err_msg); 37 | "".to_string() 38 | }); 39 | inject_script += &format!("{script_content}\n"); 40 | } 41 | } 42 | 43 | std::thread::spawn(move || { 44 | let _window = tauri::WindowBuilder::new( 45 | &app, 46 | label, 47 | tauri::WindowUrl::App(url.parse().unwrap()), 48 | ) 49 | .initialization_script(INIT_SCRIPT) 50 | .initialization_script(&inject_script) 51 | .title(title) 52 | .build() 53 | .unwrap(); 54 | }); 55 | } 56 | 57 | #[tauri::command] 58 | pub fn msg_dialog(app: tauri::AppHandle, title: &str, msg: &str) { 59 | let win = app.app_handle().get_window("main"); 60 | tauri::api::dialog::message(win.as_ref(), title, msg); 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "package": { 3 | "productName": "mdsilo", 4 | "version": "0.5.10" 5 | }, 6 | "build": { 7 | "distDir": "../build", 8 | "devPath": "http://localhost:3000", 9 | "beforeDevCommand": "yarn start", 10 | "beforeBuildCommand": "yarn build", 11 | "withGlobalTauri": true 12 | }, 13 | "tauri": { 14 | "bundle": { 15 | "active": true, 16 | "targets": "all", 17 | "identifier": "mdsilo", 18 | "publisher": "mdSilo Team", 19 | "icon": [ 20 | "icons/32x32.png", 21 | "icons/128x128.png", 22 | "icons/128x128@2x.png", 23 | "icons/icon.icns", 24 | "icons/icon.ico" 25 | ], 26 | "resources": [], 27 | "externalBin": [], 28 | "copyright": "GNU Affero General Public License v3.0", 29 | "category": "Productivity", 30 | "shortDescription": "", 31 | "longDescription": "", 32 | "deb": { 33 | "depends": [] 34 | }, 35 | "macOS": { 36 | "frameworks": [], 37 | "minimumSystemVersion": "", 38 | "exceptionDomain": "", 39 | "signingIdentity": null, 40 | "providerShortName": null, 41 | "entitlements": null 42 | }, 43 | "windows": { 44 | "certificateThumbprint": null, 45 | "digestAlgorithm": "sha256", 46 | "timestampUrl": "" 47 | } 48 | }, 49 | "allowlist": { 50 | "clipboard": { 51 | "all": true 52 | }, 53 | "dialog": { 54 | "all": true 55 | }, 56 | "window": { 57 | "all": true 58 | }, 59 | "shell": { 60 | "all": true 61 | }, 62 | "protocol": { 63 | "all": true, 64 | "asset": true, 65 | "assetScope": ["**", "**/*"] 66 | } 67 | }, 68 | "windows": [ 69 | { 70 | "title": "mdSilo", 71 | "width": 1024, 72 | "height": 768, 73 | "minWidth": 800, 74 | "minHeight": 600, 75 | "resizable": true, 76 | "fullscreen": false, 77 | "visible": false 78 | }, 79 | { 80 | "width": 800, 81 | "height": 600, 82 | "decorations": false, 83 | "url": "splashscreen.html", 84 | "label": "splashscreen" 85 | } 86 | ], 87 | "security": { 88 | "csp": "default-src blob: data: filesystem: wss: https: tauri: 'unsafe-inline' asset: https://asset.localhost 'self'; script-src 'self'" 89 | }, 90 | "updater": { 91 | "active": true, 92 | "dialog": true, 93 | "endpoints": [ 94 | "https://mdSilo.github.io/mdSilo-app/install.json" 95 | ], 96 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRkUxMUMwNTU0QjcyM0MKUldROGNrdFZ3QkgrNm1xWFpmb3FpK3ExV1NWWjk0TFRmSm42UHVqQk9PZmdYa0JhZlpsRzFHZm8K" 97 | }, 98 | "systemTray": { 99 | "iconPath": "icons/icon.ico", 100 | "iconAsTemplate": true 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:react/recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | 'plugin:react-hooks/recommended', 13 | 'plugin:import/errors', 14 | 'plugin:import/warnings', 15 | 'plugin:import/typescript', 16 | ], 17 | parser: '@typescript-eslint/parser', 18 | parserOptions: { 19 | ecmaFeatures: { 20 | jsx: true, 21 | }, 22 | ecmaVersion: 12, 23 | sourceType: 'module', 24 | }, 25 | plugins: ['react', '@typescript-eslint', 'import'], 26 | rules: { 27 | 'react/react-in-jsx-scope': 'off', 28 | '@typescript-eslint/explicit-module-boundary-types': 'off', 29 | 'import/no-named-as-default': 'off', 30 | 'import/order': [ 31 | 'error', 32 | { 33 | groups: [ 34 | 'builtin', 35 | 'external', 36 | 'internal', 37 | 'parent', 38 | 'sibling', 39 | 'index', 40 | ], 41 | }, 42 | ], 43 | }, 44 | settings: { 45 | 'import/resolver': { 46 | typescript: {}, 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | // import { render, screen } from '@testing-library/react'; 3 | import * as fileUtil from 'file/util'; 4 | import { rmFileNameExt, getFileExt } from 'file/process'; 5 | import { shortenString, decodeHTMLEntity } from 'utils/helper'; 6 | // import App from './components/App'; 7 | 8 | 9 | beforeEach(() => { 10 | Object.defineProperty(window, 'matchMedia', { 11 | writable: true, 12 | value: jest.fn().mockImplementation((query) => ({ 13 | matches: false, 14 | media: query, 15 | onchange: null, 16 | addEventListener: jest.fn(), 17 | removeEventListener: jest.fn(), 18 | dispatchEvent: jest.fn(), 19 | })), 20 | }) 21 | }) 22 | 23 | // FIXME: 24 | // TypeError: Cannot read properties of undefined (reading 'document') 25 | // import d3 from 'd3'; on mindmap/view 26 | // 27 | // test('renders App component', () => { 28 | // render(); 29 | // expect(screen.getByText('mdSilo')).toBeInTheDocument(); 30 | // }) 31 | 32 | test('file util', () => { 33 | expect(fileUtil.normalizeSlash('C:/')).toBe('C:'); 34 | expect(fileUtil.normalizeSlash('C:\\Files\\mdsilo\\app.msi')).toBe('C:/Files/mdsilo/app.msi'); 35 | expect(fileUtil.joinPath(...['/', 'md', '/silo/'])).toBe('/md/silo'); 36 | expect(fileUtil.trimSlashAll('/\\md/silo\\')).toBe('md/silo'); 37 | }) 38 | 39 | test('file ext', () => { 40 | expect(rmFileNameExt('/home/user/mdsilo.md')).toBe('/home/user/mdsilo'); 41 | expect(rmFileNameExt('/home/user/md.silo.md')).toBe('/home/user/md.silo'); 42 | expect(rmFileNameExt('mdSilo.md')).toBe('mdSilo'); 43 | expect(rmFileNameExt('md.Silo.md')).toBe('md.Silo'); 44 | expect(rmFileNameExt('/home/user/mdsilo')).toBe('/home/user/mdsilo'); 45 | expect(getFileExt('mdsilo')).toBe(''); 46 | expect(getFileExt('mdsilo.dmg')).toBe('dmg'); 47 | expect(getFileExt('md.silo.dmg')).toBe('dmg'); 48 | }) 49 | 50 | test('string util', () => { 51 | const txt = 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry.'; 52 | expect(shortenString(txt, 'dummy', 42)) 53 | .toBe('m Ipsum is simply ==dummy== text of the print'); 54 | expect(shortenString(txt, 'Ipsum', 42)) 55 | .toBe('Lorem ==Ipsum== is simply dummy text of the p'); 56 | expect(shortenString(txt, 'been', 42)) 57 | .toBe('dustry. Lorem Ipsum has ==been== the industry.'); 58 | expect(shortenString('Ipsum is simply dummy text of the print', 'dummy', 42)) 59 | .toBe('Ipsum is simply ==dummy== text of the print'); 60 | }) 61 | 62 | test('decode html entity', () => { 63 | const txt = 'Lorem> Ipsum & is simply < dummy " text ' of the printing and typesetting © industry'; 64 | expect(decodeHTMLEntity(txt)).toBe( 65 | 'Lorem> Ipsum & is simply < dummy " text ' of the printing and typesetting © industry' 66 | ); 67 | }) 68 | -------------------------------------------------------------------------------- /src/asset/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/tauri'; 2 | import { useMemo, useEffect } from 'react'; 3 | import 'styles/styles.css'; 4 | import 'tippy.js/dist/tippy.css'; 5 | import { ProvideCurrentView } from 'context/useCurrentView'; 6 | import useHotkeys from 'editor/hooks/useHotkeys'; 7 | import { useStore, SidebarTab } from 'lib/store'; 8 | import SideMenu from './sidebar/SideMenu'; 9 | import Sidebar from './sidebar/Sidebar'; 10 | import StatusBar from './sidebar/StatusBar'; 11 | import MainView from './view/MainView'; 12 | import FindOrCreateModal from './note/NoteNewModal'; 13 | import SettingsModal from './settings/SettingsModal'; 14 | import AboutModal from './settings/AboutModal'; 15 | 16 | const App = () => { 17 | const isFindOrCreateModalOpen= useStore((state) => state.isFindOrCreateModalOpen); 18 | const setIsFindOrCreateModalOpen = useStore((state) => state.setIsFindOrCreateModalOpen); 19 | 20 | const darkMode = useStore((state) => state.darkMode); 21 | 22 | const setSidebarTab = useStore((state) => state.setSidebarTab); 23 | const setIsSidebarOpen = useStore((state) => state.setIsSidebarOpen); 24 | const setIsSettingsOpen = useStore((state) => state.setIsSettingsOpen); 25 | const isSettingsOpen = useStore((state) => state.isSettingsOpen); 26 | const setIsAboutOpen = useStore((state) => state.setIsAboutOpen); 27 | const isAboutOpen = useStore((state) => state.isAboutOpen); 28 | 29 | const hotkeys = useMemo( 30 | () => [ 31 | { 32 | hotkey: 'mod+n', 33 | callback: () => setIsFindOrCreateModalOpen((isOpen) => !isOpen), 34 | }, 35 | { 36 | hotkey: 'alt+x', 37 | callback: () => setIsSidebarOpen((isOpen) => !isOpen), 38 | }, 39 | { 40 | hotkey: 'mod+s', 41 | callback: () => { /* TODO: for saving */ }, 42 | }, 43 | { 44 | hotkey: 'mod+shift+d', 45 | callback: () => setSidebarTab(SidebarTab.Silo), 46 | }, 47 | { 48 | hotkey: 'mod+shift+f', 49 | callback: () => setSidebarTab(SidebarTab.Search), 50 | }, 51 | { 52 | hotkey: 'mod+shift+h', 53 | callback: () => setSidebarTab(SidebarTab.Hashtag), 54 | }, 55 | { 56 | hotkey: 'mod+shift+p', 57 | callback: () => setSidebarTab(SidebarTab.Playlist), 58 | }, 59 | ], 60 | [setIsFindOrCreateModalOpen, setIsSidebarOpen, setSidebarTab] 61 | ); 62 | useHotkeys(hotkeys); 63 | 64 | useEffect(() => { 65 | const closeSplash = () => { invoke('close_splashscreen'); }; 66 | document.addEventListener('DOMContentLoaded', closeSplash); 67 | 68 | return () => { 69 | document.removeEventListener('DOMContentLoaded', closeSplash, true); 70 | }; 71 | }, []); 72 | 73 | const appContainerClassName = `h-screen flex flex-col ${darkMode ? 'dark' : ''}`; 74 | 75 | return ( 76 | 77 |
78 |
79 | 80 | 81 |
82 |
83 | 84 |
85 | {isFindOrCreateModalOpen ? ( 86 | 87 | ) : null} 88 | setIsSettingsOpen(false)} 91 | /> 92 | setIsAboutOpen(false)} 95 | /> 96 |
97 |
98 |
99 | ) 100 | } 101 | 102 | export default App; 103 | -------------------------------------------------------------------------------- /src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import logo from 'asset/logo.svg'; 2 | 3 | type Props = { 4 | width?: number; 5 | height?: number; 6 | className?: string; 7 | }; 8 | 9 | export default function Logo(props: Props) { 10 | const { width = 32, height = 32, className = 'rounded img-effect' } = props; 11 | return ( 12 |
13 | mdSilo 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/dir/DirDelModal.tsx: -------------------------------------------------------------------------------- 1 | import { BaseModal } from 'components/settings/BaseModal'; 2 | import { deleteFiles } from 'file/util'; 3 | 4 | type Props = { 5 | dirPath: string; 6 | isOpen: boolean; 7 | handleClose: () => void; 8 | }; 9 | 10 | export default function DirDelModal(props: Props) { 11 | const { dirPath, isOpen, handleClose } = props; 12 | 13 | return ( 14 | 15 |
16 |

{dirPath}

17 | 20 | 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/dir/DirNewModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { BaseModal } from 'components/settings/BaseModal'; 3 | import { createDirRecursive, joinPaths } from 'file/util'; 4 | 5 | type Props = { 6 | dirPath: string; 7 | isOpen: boolean; 8 | handleClose: () => void; 9 | }; 10 | 11 | export default function DirNewModal(props: Props) { 12 | const { dirPath, isOpen, handleClose } = props; 13 | const [inputText, setInputText] = useState(''); 14 | 15 | return ( 16 | 17 |
18 |

{dirPath}

19 |
20 | setInputText(e.target.value)} 26 | autoFocus 27 | /> 28 |
29 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/dir/DirRenameModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { BaseModal } from 'components/settings/BaseModal'; 3 | import { renameFile, joinPaths, getParentDir } from 'file/util'; 4 | 5 | type Props = { 6 | dirPath: string; 7 | isOpen: boolean; 8 | handleClose: () => void; 9 | }; 10 | 11 | export default function DirRenameModal(props: Props) { 12 | const { dirPath, isOpen, handleClose } = props; 13 | const [inputText, setInputText] = useState(''); 14 | 15 | return ( 16 | 17 |
18 |

{dirPath}

19 |
20 | setInputText(e.target.value)} 26 | autoFocus 27 | /> 28 |
29 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/feed/ArticleView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { IconChevronLeft, IconHeadphones, IconLink, IconStar } from "@tabler/icons-react"; 3 | import { useStore } from "lib/store"; 4 | import { getFavicon, fmtDatetime } from "utils/helper"; 5 | import { ArticleType } from "types/model"; 6 | import Tooltip from "components/misc/Tooltip"; 7 | 8 | type ViewProps = { 9 | article: ArticleType | null; 10 | starArticle: (url: string, status: number) => Promise; 11 | hideChannelCol: () => void; 12 | }; 13 | 14 | export function ArticleView(props: ViewProps) { 15 | const { article, starArticle, hideChannelCol } = props; 16 | const [isStar, setIsStar] = useState(article?.star_status === 1); 17 | const [pageContent, setPageContent] = useState(""); 18 | 19 | const setCurrentPod = useStore(state => state.setCurrentPod); 20 | 21 | useEffect(() => { 22 | if (article) { 23 | const content = (article.content || article.description || "").replace( 24 | /]+>/gi, 25 | (a: string) => { 26 | return (!/\starget\s*=/gi.test(a)) ? a.replace(/^ 38 | ); 39 | } 40 | 41 | const { title, url, feed_link, author, published } = article; 42 | const ico = getFavicon(url); 43 | 44 | return ( 45 |
46 |
47 |
{title}
48 |
49 | 50 | 51 | 52 | 53 | # 54 | 55 | {fmtDatetime(published || '')} 56 | {author} 57 | 63 | 64 | 65 | { 68 | await starArticle(article.url, Math.abs(article.star_status - 1)); 69 | setIsStar(!isStar); 70 | }} 71 | > 72 | 73 | 74 | {article.audio_url.trim() && ( 75 | setCurrentPod( 78 | {title, url: article.audio_url, published: article.published, article_url: article.url, feed_link: article.feed_link} 79 | )} 80 | > 81 | 82 | 83 | )} 84 |
85 |
86 |
87 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /src/components/feed/AudioPlayer.tsx: -------------------------------------------------------------------------------- 1 | import { IconPlaylist } from '@tabler/icons-react'; 2 | import { SidebarTab, store } from 'lib/store'; 3 | import { PodType } from 'types/model'; 4 | 5 | type Props = { 6 | currentPod: PodType | null; 7 | className?: string; 8 | }; 9 | 10 | export default function AudioPlayer(props: Props) { 11 | const { currentPod, className = '' } = props; 12 | // console.log("current pod: ", currentPod) 13 | 14 | const TriggerPlaylist = () => { 15 | store.getState().setIsSidebarOpen(true); 16 | store.getState().setSidebarTab(SidebarTab.Playlist); 17 | }; 18 | 19 | if (!currentPod) { 20 | return (
no player
); 21 | } 22 | 23 | return ( 24 |
25 | 28 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/feed/ChannelList.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { IconHeadphones, IconRefresh, IconRss, IconSettings, IconStar } from "@tabler/icons-react"; 3 | import { getFavicon } from "utils/helper"; 4 | import Tooltip from "components/misc/Tooltip"; 5 | import Spinner from "components/misc/Spinner"; 6 | import { ChannelType } from "types/model"; 7 | 8 | type Props = { 9 | channelList: ChannelType[]; 10 | refreshList: () => Promise; 11 | onShowManager: () => void; 12 | refreshing: boolean; 13 | doneNum: number; 14 | onClickFeed: (link: string) => Promise; 15 | onClickStar: () => Promise; 16 | }; 17 | 18 | export function ChannelList(props: Props) { 19 | const { channelList, refreshList, onShowManager, onClickFeed, onClickStar, refreshing, doneNum } = props; 20 | 21 | const [highlighted, setHighlighted] = useState(); 22 | 23 | const renderFeedList = (): JSX.Element => { 24 | return ( 25 | <> 26 | {channelList.map((channel: ChannelType, idx: number) => { 27 | const { unread = 0, title, ty, link } = channel; 28 | const ico = getFavicon(link); 29 | const activeClass = `${highlighted?.link === link ? 'border-l-2 border-green-500' : ''}`; 30 | 31 | return ( 32 |
{ 36 | onClickFeed(link); 37 | setHighlighted(channel); 38 | }} 39 | > 40 | 41 |
42 | > 43 | {title} 44 |
45 |
46 | 47 | {unread} 48 | {ty === 'rss' 49 | ? 50 | : 51 | } 52 | 53 |
54 | ); 55 | })} 56 | 57 | ); 58 | }; 59 | 60 | return ( 61 |
62 |
63 |
64 | 65 | 68 | 69 | 70 | 73 | 74 |
75 |
76 | {refreshing && ( 77 |
78 | 79 | {doneNum}/{channelList.length} 80 |
81 | )} 82 |
83 |
87 |
88 | 89 | Starred 90 |
91 |
92 | {renderFeedList()} 93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/components/feed/dataAgent.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from "@tauri-apps/api"; 2 | import { ChannelType, ArticleType } from "types/model"; 3 | 4 | type FeedResult = { 5 | channel: ChannelType; 6 | articles: ArticleType[]; 7 | }; 8 | 9 | export const fetchFeed = async (url: string): Promise => { 10 | return await invoke('fetch_feed', { url }) 11 | } 12 | 13 | export const addChannel = async ( 14 | url: string, ty: string, title: string | null 15 | ): Promise => { 16 | return await invoke('add_channel', { url, ty, title }) 17 | } 18 | 19 | export const importChannels = async (list: string[]) => { 20 | return await invoke('import_channels', { list }) 21 | } 22 | 23 | export const getChannels = async (): Promise => { 24 | return await invoke('get_channels') 25 | } 26 | 27 | export const deleteChannel = async (link: string) => { 28 | return await invoke('delete_channel', { link }) 29 | }; 30 | 31 | export const getArticleList = async ( 32 | feedLink: string | null, 33 | readStatus: number | null, 34 | starStatus: number | null, 35 | ) : Promise => { 36 | return await invoke('get_articles', { feedLink, readStatus, starStatus }) 37 | } 38 | 39 | export const getArticleByUrl = async (url: string): Promise => { 40 | return await invoke('get_article_by_url', { url }) 41 | } 42 | 43 | export const getUnreadNum = async (): Promise<{ [key: string]: number }> => { 44 | return await invoke('get_unread_num') 45 | } 46 | 47 | export const updateArticleStarStatus = async ( 48 | articleUrl: string, 49 | star_status: number, // 0 || 1 50 | ): Promise => { 51 | return await invoke('update_article_star_status', { 52 | url: articleUrl, 53 | status: star_status, 54 | }) 55 | } 56 | 57 | export const updateArticleReadStatus = async ( 58 | articleUrl: string, 59 | read_status: number, 60 | ): Promise => { 61 | return await invoke('update_article_read_status', { 62 | url: articleUrl, 63 | status: read_status, 64 | }) 65 | } 66 | 67 | export const updateAllReadStatus = async ( 68 | feedLink: string, 69 | readStatus: number, 70 | ): Promise => { 71 | return await invoke('update_all_read_status', { feedLink, readStatus }) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/kanban/types.ts: -------------------------------------------------------------------------------- 1 | export type Id = string | number; 2 | 3 | export type Column = { 4 | id: Id; 5 | title: string; 6 | hdColor?: string; 7 | bgColor?: string; 8 | ftColor?: string; 9 | }; 10 | 11 | export type Card = { 12 | id: Id; 13 | columnId: Id; 14 | content: string; 15 | bgColor?: string; 16 | ftColor?: string; 17 | items?: CardItem[]; 18 | }; 19 | 20 | export type CardItem = { 21 | name: string; 22 | uri: string; 23 | category: string; // note, book, podcast, video, ... 24 | }; 25 | 26 | export type KanbanData = { 27 | columns: Column[]; 28 | cards: Card[]; 29 | bgColor?: string; 30 | bgImg?: string; 31 | }; 32 | 33 | export type Kanbans = Record; // {name: data} 34 | -------------------------------------------------------------------------------- /src/components/kanban/updateCard.ts: -------------------------------------------------------------------------------- 1 | import { store } from 'lib/store'; 2 | import { joinPath } from 'file/util'; 3 | import FileAPI from 'file/files'; 4 | import { rmFileNameExt } from 'file/process'; 5 | import { Id, KanbanData, CardItem, Kanbans } from './types'; 6 | 7 | /** 8 | * Upsert the note in kanban card 9 | * 10 | * @param noteId of current note, aka filePath or [title, filePath] 11 | * @param oldTitle of current note, to rename 12 | */ 13 | export const updateCardItems = async ( 14 | id: Id, noteId: string | string[], oldTitle?: string 15 | ) => { 16 | const initDir = store.getState().initDir; 17 | const currentKb = store.getState().currentBoard; 18 | // console.log("currentKb", currentKb); 19 | if (!initDir || !currentKb.trim()) return; 20 | 21 | const [title, itemUri, category] = typeof noteId === "string" 22 | ? [noteId.split("/").pop(), noteId, "note"] 23 | : [...noteId, "attach"]; 24 | if (!title) return; 25 | 26 | const kanbanJsonPath = joinPath(initDir, `kanban.json`); 27 | const jsonFile = new FileAPI(kanbanJsonPath); 28 | const json = await jsonFile.readFile(); 29 | const kanbans: Kanbans = JSON.parse(json); 30 | const data: KanbanData = kanbans[currentKb]; 31 | 32 | const cards = data.cards; 33 | const newCards = cards.map((card) => { 34 | if (card.id !== id) return card; 35 | 36 | const items = card.items || []; 37 | const newItem: CardItem = { 38 | name: title, 39 | uri: itemUri, 40 | category, 41 | }; 42 | // console.log("old title in card: ", oldTitle) 43 | if (oldTitle) { 44 | // filter out old one 45 | const newItems = items.filter(itm => rmFileNameExt(itm.name) !== oldTitle); 46 | // console.log("items in card: ", newItems); 47 | newItems.push(newItem); 48 | 49 | return { ...card, items: newItems }; 50 | } else { 51 | // push new item 52 | items.push(newItem); 53 | 54 | return { ...card, items }; 55 | } 56 | }); 57 | 58 | // update and save to file 59 | data.cards = newCards; 60 | kanbans[currentKb] = data; 61 | await jsonFile.writeFile(JSON.stringify(kanbans)); 62 | // reset current card 63 | store.getState().setCurrentCard(undefined); 64 | }; 65 | 66 | 67 | export const updateCardLinks = async (newPath: string, oldPath: string) => { 68 | const initDir = store.getState().initDir; 69 | if (!initDir) return; 70 | 71 | const oldTitle = oldPath.split("/").pop(); 72 | const newTitle = newPath.split("/").pop(); 73 | if (!oldTitle || !newTitle) return; 74 | 75 | const kanbanJsonPath = joinPath(initDir, `kanban.json`); 76 | const jsonFile = new FileAPI(kanbanJsonPath); 77 | const json0 = await jsonFile.readFile(); 78 | // just replace 79 | //const oldItem = String.raw`{\"name\":\"${oldTitle}\",\"uri\":\"${oldPath}\",\"category\":\"note\"}`; 80 | //const newItem = String.raw`{\"name\":\"${newTitle}\",\"uri\":\"${newPath}\",\"category\":\"note\"}`; 81 | //const json = json0.replaceAll(oldItem, newItem); 82 | const json1 = json0.replaceAll(oldTitle, newTitle); 83 | const json = json1.replaceAll(oldPath, newPath); 84 | 85 | await jsonFile.writeFile(json); 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/md/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | MIT License 4 | 5 | Copyright (c) 2021 uiw 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /src/components/md/Markdown.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; 3 | import { json } from "@codemirror/lang-json"; 4 | import { languages } from "@codemirror/language-data"; 5 | import CodeMirror from "./ReactCodeMirror"; 6 | 7 | type Props = { 8 | lang?: string; 9 | initialContent: string; 10 | onChange: (value: string) => void; 11 | onFocus?: () => void; 12 | dark: boolean; 13 | readMode?: boolean; 14 | className?: string; 15 | }; 16 | 17 | function Markdown(props: Props) { 18 | const { 19 | lang = "markdown", 20 | initialContent, 21 | onChange, 22 | onFocus, 23 | dark, 24 | readMode = false, 25 | className = '', 26 | } = props; 27 | 28 | //const readMode = useStore((state) => state.readMode); 29 | 30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 31 | const onValueChange = (value: string, _viewUpdate: unknown) => { 32 | // console.log('md Changed, value:', value, _viewUpdate); 33 | onChange(value); 34 | }; 35 | 36 | return ( 37 | 49 | ); 50 | } 51 | 52 | export default memo(Markdown); 53 | -------------------------------------------------------------------------------- /src/components/md/ReactCodeMirror.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prop-types */ 2 | import React, { useEffect, useRef, useImperativeHandle, forwardRef } from 'react'; 3 | import { EditorState, EditorStateConfig, Extension } from '@codemirror/state'; 4 | import { EditorView, ViewUpdate } from '@codemirror/view'; 5 | import { useCodeMirror } from './useCodeMirror'; 6 | 7 | export interface ReactCodeMirrorProps 8 | extends Omit, 9 | Omit, 'onChange' | 'placeholder'> { 10 | value?: string; 11 | height?: string; 12 | minHeight?: string; 13 | maxHeight?: string; 14 | width?: string; 15 | minWidth?: string; 16 | maxWidth?: string; 17 | autoFocus?: boolean; 18 | placeholder?: string | HTMLElement; 19 | /** 20 | * `light` / `dark` / `Extension` Defaults to `light`. 21 | * @default light 22 | */ 23 | theme?: 'light' | 'dark' | Extension; 24 | /** 25 | * Whether to optional basicSetup by default 26 | * @default true 27 | */ 28 | basicSetup?: boolean; 29 | /** 30 | * This disables editing of the editor content by the user. 31 | * @default true 32 | */ 33 | editable?: boolean; 34 | readOnly?: boolean; 35 | /** 36 | * Whether to optional basicSetup by default 37 | * @default true 38 | */ 39 | indentWithTab?: boolean; 40 | onChange?(value: string, viewUpdate: ViewUpdate): void; 41 | onUpdate?(viewUpdate: ViewUpdate): void; 42 | // https://codemirror.net/docs/ref/#state.Extension 43 | extensions?: Extension[]; 44 | root?: ShadowRoot | Document; 45 | } 46 | 47 | export interface ReactCodeMirrorRef { 48 | editor?: HTMLDivElement | null; 49 | state?: EditorState; 50 | view?: EditorView; 51 | } 52 | 53 | const ReactCodeMirror = forwardRef((props, ref) => { 54 | const { 55 | className, 56 | value = '', 57 | selection, 58 | extensions = [], 59 | onChange, 60 | onUpdate, 61 | autoFocus, 62 | theme = 'light', 63 | height, 64 | minHeight, 65 | maxHeight, 66 | width, 67 | minWidth, 68 | maxWidth, 69 | basicSetup, 70 | placeholder, 71 | indentWithTab, 72 | editable, 73 | readOnly, 74 | root, 75 | ...other 76 | } = props; 77 | const editor = useRef(null); 78 | const { state, view, container, setContainer } = useCodeMirror({ 79 | container: editor.current, 80 | root, 81 | value, 82 | autoFocus, 83 | theme, 84 | height, 85 | minHeight, 86 | maxHeight, 87 | width, 88 | minWidth, 89 | maxWidth, 90 | basicSetup, 91 | placeholder, 92 | indentWithTab, 93 | editable, 94 | readOnly, 95 | selection, 96 | onChange, 97 | onUpdate, 98 | extensions, 99 | }); 100 | useImperativeHandle(ref, () => ({ editor: container, state, view }), [container, state, view]); 101 | useEffect(() => { 102 | setContainer(editor.current); 103 | // eslint-disable-next-line react-hooks/exhaustive-deps 104 | }, []); 105 | 106 | if (typeof value !== 'string') { 107 | throw new Error(`value must be typeof string but got ${typeof value}`); 108 | } 109 | 110 | const defaultClassNames = typeof theme === 'string' ? `cm-theme-${theme}` : 'cm-theme'; 111 | return ( 112 |
117 |
118 | ); 119 | }); 120 | 121 | ReactCodeMirror.displayName = 'CodeMirror'; 122 | 123 | export default ReactCodeMirror; 124 | -------------------------------------------------------------------------------- /src/components/mindmap/LICENSE: -------------------------------------------------------------------------------- 1 | Modified from: https://github.com/dundalek/markmap 2 | 3 | MIT LICENSE : https://github.com/dundalek/markmap/blob/master/LICENSE 4 | 5 | Jakub Dundalek 6 | -------------------------------------------------------------------------------- /src/components/mindmap/mindmap.css: -------------------------------------------------------------------------------- 1 | svg#mindmap { 2 | width: 100%; 3 | height: 100%; 4 | } 5 | 6 | .markmap-node { 7 | cursor: pointer; 8 | } 9 | 10 | .markmap-node-circle { 11 | fill: #fff; 12 | stroke-width: 1.5px; 13 | } 14 | 15 | .markmap-node-text { 16 | fill: #000; 17 | font: 10px sans-serif; 18 | } 19 | 20 | .markmap-link { 21 | fill: none; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/mindmap/mindmap.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef, useState } from 'react'; 2 | import { parse, transform, markmap } from 'mdsmap'; 3 | import { writeFile } from 'file/write'; 4 | import { normalizeSlash } from 'file/util'; 5 | import { saveDilog } from 'file/open'; 6 | import './mindmap.css'; 7 | 8 | type Props = { 9 | title: string; 10 | mdValue: string; 11 | initDir?: string; 12 | className?: string; 13 | }; 14 | 15 | export function Mindmap(props: Props) { 16 | const { title, mdValue, initDir, className = '' } = props; 17 | 18 | const [svgElement, setSvgElement] = useState(null); 19 | const svgRef = useRef(null); 20 | 21 | const renderSVG = useCallback(() => { 22 | if (!svgRef.current || !mdValue.trim()) { 23 | return; 24 | } 25 | 26 | const data = transform(parse(mdValue, {}), title); 27 | const svg: SVGAElement = markmap(svgRef.current, data, { 28 | preset: 'colorful', // or default 29 | linkShape: 'diagonal' // or bracket 30 | }); 31 | setSvgElement(svg); 32 | }, [mdValue, title]); 33 | 34 | useEffect(() => { 35 | if (!svgRef.current) { return; } 36 | 37 | renderSVG(); 38 | }, [renderSVG]); 39 | 40 | const saveSVG = useCallback(async () => { 41 | if (!svgElement || !initDir) return; 42 | const w = svgElement.clientWidth; 43 | const h = svgElement.clientHeight; 44 | if (w && h) { 45 | svgElement.setAttribute("viewBox", `0 0 ${w} ${h}`); 46 | } 47 | svgElement.setAttribute("style", "background-color:white"); 48 | // console.log("w/h", w, h, svgElement); 49 | const styleNode = document.createElement('style'); 50 | styleNode.setAttribute('type', 'text/css'); 51 | styleNode.innerHTML = `svg#mindmap {width: 100%; height: 100%;} .markmap-node-circle {fill: #fff; stroke-width: 1.5px;} .markmap-node-text {fill: #000; font: 10px sans-serif;} .markmap-link {fill: none;}`; 52 | svgElement.appendChild(styleNode); 53 | // prepare to save 54 | const fname = `${title.trim().replaceAll(' ', '-') || 'untitled'}-mindmap.svg`; 55 | const dir = await saveDilog(fname); 56 | const defaultDir = `${initDir}/mindmap/${fname}`; 57 | const saveDir = normalizeSlash(dir || defaultDir); 58 | await writeFile(saveDir, svgElement.outerHTML); 59 | }, [svgElement, initDir, title]); 60 | 61 | return ( 62 |
63 | 71 |
72 | 75 |
76 |
77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /src/components/misc/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | useState, 3 | ReactNode, 4 | MouseEventHandler, 5 | useRef, 6 | useCallback, 7 | } from 'react'; 8 | import { usePopper } from 'react-popper'; 9 | import { Menu } from '@headlessui/react'; 10 | import { Placement } from '@popperjs/core'; 11 | import Portal from './Portal'; 12 | import Tooltip from './Tooltip'; 13 | 14 | type Props = { 15 | buttonChildren: ReactNode; 16 | children: ReactNode; 17 | buttonClassName?: string; 18 | itemsClassName?: string; 19 | placement?: Placement; 20 | offset?: [number | null | undefined, number | null | undefined]; 21 | tooltipContent?: ReactNode; 22 | tooltipPlacement?: Placement; 23 | }; 24 | 25 | export default function Dropdown(props: Props) { 26 | const { 27 | buttonChildren, 28 | children, 29 | buttonClassName = '', 30 | itemsClassName = '', 31 | placement, 32 | offset, 33 | tooltipContent, 34 | tooltipPlacement, 35 | } = props; 36 | 37 | const referenceElementRef = useRef(null); 38 | const [popperElement, setPopperElement] = useState( 39 | null 40 | ); 41 | const { styles, attributes } = usePopper( 42 | referenceElementRef.current, 43 | popperElement, 44 | { 45 | placement, 46 | modifiers: [{ name: 'offset', options: { offset } }], 47 | } 48 | ); 49 | 50 | return ( 51 | 52 | {({ open }) => ( 53 | <> 54 | 60 | 66 | {buttonChildren} 67 | 68 | 69 | {open && ( 70 | 71 | 78 | {children} 79 | 80 | 81 | )} 82 | 83 | )} 84 | 85 | ); 86 | } 87 | 88 | type DropdownItemProps = 89 | | { 90 | children: ReactNode; 91 | onClick: MouseEventHandler; 92 | as?: 'button'; 93 | target?: string; 94 | className?: string; 95 | } 96 | | { 97 | children: ReactNode; 98 | href: string; 99 | as: 'a'; 100 | target?: string; 101 | className?: string; 102 | } 103 | | { 104 | children: ReactNode; 105 | href: string; 106 | as: 'link'; 107 | target?: string; 108 | className?: string; 109 | }; 110 | 111 | export function DropdownItem(props: DropdownItemProps) { 112 | const { children, target = '_blank', className = '' } = props; 113 | 114 | const itemClassName = useCallback( 115 | (active: boolean) => 116 | `flex w-full items-center px-4 py-2 text-left text-sm text-gray-800 dark:text-gray-200 select-none ${ 117 | active ? 'bg-gray-100 dark:bg-gray-700' : '' 118 | } ${className}`, 119 | [className] 120 | ); 121 | 122 | return ( 123 | 124 | {({ active }) => 125 | (props.as === 'a' || props.as === 'link') ? ( 126 | 132 | {children} 133 | 134 | ) : ( 135 | 138 | ) 139 | } 140 | 141 | ); 142 | } 143 | -------------------------------------------------------------------------------- /src/components/misc/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ReactNode } from 'react'; 2 | import { setLog } from 'file/storage'; 3 | 4 | type Props = { 5 | children: ReactNode; 6 | fallback?: ReactNode; 7 | }; 8 | 9 | type State = { 10 | hasError: boolean; 11 | }; 12 | 13 | export default class ErrorBoundary extends Component { 14 | constructor(props: Props) { 15 | super(props); 16 | this.state = { hasError: false }; 17 | } 18 | 19 | static getDerivedStateFromError() { 20 | return { hasError: true }; 21 | } 22 | 23 | componentDidCatch(error: Error) { 24 | // sentry? TODO 25 | setLog('Error', `${error.name}: ${error.message}, ${error.cause}, ${error.stack}`); 26 | } 27 | 28 | render() { 29 | const { children, fallback } = this.props; 30 | 31 | if (this.state.hasError) { 32 | return ( 33 | fallback ?? ( 34 |
35 |

An unexpected error occurred.

36 |
37 | ) 38 | ); 39 | } 40 | 41 | return children; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/misc/Portal.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { useRef, useEffect, useState } from 'react'; 3 | import { createPortal } from 'react-dom'; 4 | 5 | type Props = { 6 | children: ReactNode; 7 | selector?: string; 8 | }; 9 | 10 | export default function Portal({ children, selector }: Props) { 11 | const ref = useRef(null); 12 | const [mounted, setMounted] = useState(false); 13 | 14 | useEffect(() => { 15 | ref.current = document.querySelector(selector ?? '#app-container'); 16 | setMounted(true); 17 | }, [selector]); 18 | 19 | return mounted && ref.current ? createPortal(children, ref.current) : null; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/misc/Spinner.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | className?: string; 3 | }; 4 | 5 | export default function Spinner(props: Props) { 6 | const { className = '' } = props; 7 | return ( 8 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/misc/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type Props = { 4 | id: string; 5 | isChecked: boolean; 6 | setIsChecked: (isChecked: boolean) => void; 7 | className?: string; 8 | }; 9 | 10 | export default function Toggle(props: Props) { 11 | const { id, className, isChecked, setIsChecked } = props; 12 | 13 | return ( 14 |
15 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/misc/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import Tippy, { TippyProps } from '@tippyjs/react'; 2 | 3 | type TooltipProps = TippyProps; 4 | 5 | export default function Tooltip(props: TooltipProps) { 6 | const { 7 | children, 8 | duration = 0, 9 | arrow = false, 10 | offset = [0, 6], 11 | touch = ['hold', 500], 12 | ...otherProps 13 | } = props; 14 | 15 | return ( 16 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/misc/Tree.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback, useEffect, ReactNode, memo } from 'react'; 2 | import TreeNodeElement from './TreeNodeElement'; 3 | 4 | export type TreeNode = { 5 | id: string; 6 | labelNode: ReactNode; 7 | showArrow?: boolean; 8 | toIndent?: boolean; 9 | children?: TreeNode[]; 10 | }; 11 | 12 | export type FlattenedTreeNode = { 13 | id: string; 14 | labelNode: ReactNode; 15 | showArrow?: boolean; 16 | toIndent?: boolean; 17 | hasChildren: boolean; 18 | depth: number; 19 | collapsed: boolean; 20 | }; 21 | 22 | type Props = { 23 | data: TreeNode[]; 24 | className?: string; 25 | collapseAll?: boolean; 26 | collapseIds?: string[]; 27 | }; 28 | 29 | function Tree(props: Props) { 30 | const { data, className, collapseAll = false, collapseIds = [] } = props; 31 | const [closedNodeIds, setClosedNodeIds] = useState( 32 | collapseAll ? data.map((node) => node.id) : collapseIds 33 | ); 34 | 35 | const onNodeClick = useCallback( 36 | (node: FlattenedTreeNode) => { 37 | node.collapsed 38 | ? setClosedNodeIds(closedNodeIds.filter((id) => id !== node.id)) 39 | : setClosedNodeIds([...closedNodeIds, node.id]); 40 | 41 | // avoid trigger useEffect 42 | while(collapseIds.length > 0) { 43 | collapseIds.pop(); 44 | } 45 | },[closedNodeIds, collapseIds] 46 | ); 47 | 48 | // change state per props changed 49 | useEffect(() => { 50 | if (collapseIds.length > 0 && collapseIds !== closedNodeIds) { 51 | setClosedNodeIds(collapseIds); 52 | } 53 | }, [collapseIds, closedNodeIds]); 54 | 55 | const flattenNode = useCallback( 56 | (node: TreeNode, depth: number, result: FlattenedTreeNode[]) => { 57 | const { id, labelNode, children, showArrow, toIndent } = node; 58 | const collapsed = closedNodeIds.includes(id); 59 | result.push({ 60 | id, 61 | labelNode, 62 | showArrow: showArrow ?? true, 63 | toIndent: toIndent ?? true, 64 | hasChildren: (children ?? []).length > 0, 65 | depth, 66 | collapsed, 67 | }); 68 | 69 | // collapse trigger: 70 | // if parent id in collapseIds, the children won't be flattened 71 | if (!collapsed && children) { 72 | for (const child of children) { 73 | flattenNode(child, depth + 1, result); 74 | } 75 | } 76 | }, 77 | [closedNodeIds] 78 | ); 79 | 80 | const flattenedData = useMemo(() => { 81 | const result: FlattenedTreeNode[] = []; 82 | for (const node of data) { 83 | flattenNode(node, 0, result); 84 | } 85 | return result; 86 | }, [data, flattenNode]); 87 | 88 | return ( 89 |
90 | {flattenedData.map((node, index) => ( 91 | 96 | ))} 97 |
98 | ); 99 | } 100 | 101 | export default memo(Tree); 102 | -------------------------------------------------------------------------------- /src/components/misc/TreeNodeElement.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo, CSSProperties, forwardRef, ForwardedRef } from 'react'; 2 | import { IconCaretRight } from '@tabler/icons-react'; 3 | import { FlattenedTreeNode } from './Tree'; 4 | 5 | type Props = { 6 | node: FlattenedTreeNode; 7 | onClick: (node: FlattenedTreeNode) => void; 8 | style?: CSSProperties; 9 | }; 10 | 11 | const TreeNodeElement = (props: Props, forwardedRef: ForwardedRef) => { 12 | const { node, onClick, style } = props; 13 | 14 | const leftPadding = useMemo(() => { 15 | let padding = node.toIndent 16 | ? node.depth * 16 17 | : Math.max(node.depth - 1, 0) * 16; 18 | if (!node.showArrow) { 19 | padding += 4; 20 | } 21 | return padding; 22 | }, [node.depth, node.showArrow, node.toIndent]); 23 | 24 | return ( 25 |
onClick(node) : undefined} 32 | > 33 | {node.showArrow ? ( 34 | 41 | ) : null} 42 | {node.labelNode} 43 |
44 | ); 45 | }; 46 | 47 | export default memo(forwardRef(TreeNodeElement)); 48 | -------------------------------------------------------------------------------- /src/components/misc/VirtualTree.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useCallback, memo, useRef } from 'react'; 2 | import { useVirtual } from 'react-virtual'; 3 | import { FlattenedTreeNode, TreeNode as TreeNodeType } from './Tree'; 4 | import TreeNodeElement from './TreeNodeElement'; 5 | 6 | type Props = { 7 | data: TreeNodeType[]; 8 | className?: string; 9 | collapseAll?: boolean; 10 | }; 11 | 12 | function VirtualTree(props: Props) { 13 | const { data, className, collapseAll = false } = props; 14 | 15 | const [closedNodeIds, setClosedNodeIds] = useState( 16 | collapseAll ? data.map((node) => node.id) : [] 17 | ); 18 | 19 | const onNodeClick = useCallback( 20 | (node: FlattenedTreeNode) => 21 | node.collapsed 22 | ? setClosedNodeIds((closedNodeIds) => 23 | closedNodeIds.filter((id) => id !== node.id) 24 | ) 25 | : setClosedNodeIds((closedNodeIds) => [...closedNodeIds, node.id]), 26 | [] 27 | ); 28 | 29 | const flattenNode = useCallback( 30 | (node: TreeNodeType, depth: number, result: FlattenedTreeNode[]) => { 31 | const { id, labelNode, children, showArrow } = node; 32 | const collapsed = closedNodeIds.includes(id); 33 | result.push({ 34 | id, 35 | labelNode, 36 | showArrow: showArrow ?? true, 37 | hasChildren: (children ?? []).length > 0, 38 | depth, 39 | collapsed, 40 | }); 41 | 42 | if (!collapsed && children) { 43 | for (const child of children) { 44 | flattenNode(child, depth + 1, result); 45 | } 46 | } 47 | }, 48 | [closedNodeIds] 49 | ); 50 | 51 | const flattenedData = useMemo(() => { 52 | const result: FlattenedTreeNode[] = []; 53 | for (const node of data) { 54 | flattenNode(node, 0, result); 55 | } 56 | return result; 57 | }, [data, flattenNode]); 58 | 59 | const parentRef = useRef(null); 60 | const rowVirtualizer = useVirtual({ 61 | size: flattenedData.length, 62 | parentRef, 63 | }); 64 | 65 | return ( 66 |
67 |
74 | {rowVirtualizer.virtualItems.map((virtualRow, index) => { 75 | const node = flattenedData[virtualRow.index]; 76 | return ( 77 | 90 | ); 91 | })} 92 |
93 |
94 | ); 95 | } 96 | 97 | export default memo(VirtualTree); 98 | -------------------------------------------------------------------------------- /src/components/note/NoteDelModal.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import useHotkeys from 'editor/hooks/useHotkeys'; 3 | import useDeleteNote from 'editor/hooks/useDeleteNote'; 4 | import { BaseModal } from 'components/settings/BaseModal'; 5 | 6 | type Props = { 7 | noteId: string; 8 | noteTitle: string; 9 | isOpen: boolean; 10 | handleClose: () => void; 11 | }; 12 | 13 | export default function NoteDelModal(props: Props) { 14 | const { noteId, noteTitle, isOpen, handleClose } = props; 15 | 16 | const hotkeys = useMemo( 17 | () => [ 18 | { 19 | hotkey: 'esc', 20 | callback: handleClose, 21 | }, 22 | ], 23 | [handleClose] 24 | ); 25 | useHotkeys(hotkeys); 26 | 27 | const onDeleteClick = useDeleteNote(noteId, noteTitle); 28 | 29 | return ( 30 | 31 |
32 |

{noteId}

33 | 36 | 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/note/NoteMetadata.tsx: -------------------------------------------------------------------------------- 1 | import { useStore } from 'lib/store'; 2 | import { fmtDatetime, countWords } from 'utils/helper'; 3 | 4 | type Props = { 5 | noteId: string; 6 | }; 7 | 8 | export default function NoteMetadata(props: Props) { 9 | const { noteId } = props; 10 | const note = useStore((state) => state.notes[noteId]); 11 | 12 | if (!note) { 13 | return null; 14 | } 15 | 16 | const mdContent = note.content || ''; 17 | const wordCount = countWords(mdContent); 18 | const ctnLen = mdContent.length; 19 | return ( 20 |
21 | {ctnLen > 0 ? (

~{wordCount} words, {ctnLen} characters

) : null} 22 |

Created: {fmtDatetime(note.created_at)}

23 |

Modified: {fmtDatetime(note.updated_at)}

24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/note/NoteMoveModal.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import useHotkeys from 'editor/hooks/useHotkeys'; 3 | import MoveToInput from './NoteMoveInput'; 4 | 5 | type Props = { 6 | noteId: string; 7 | setIsOpen: (isOpen: boolean) => void; 8 | }; 9 | 10 | export default function MoveToModal(props: Props) { 11 | const { noteId, setIsOpen } = props; 12 | 13 | const hotkeys = useMemo( 14 | () => [ 15 | { 16 | hotkey: 'esc', 17 | callback: () => setIsOpen(false), 18 | }, 19 | ], 20 | [setIsOpen] 21 | ); 22 | useHotkeys(hotkeys); 23 | 24 | return ( 25 |
26 |
setIsOpen(false)} 29 | /> 30 |
31 | setIsOpen(false)} 34 | className="z-30 w-full max-w-screen-sm bg-white rounded shadow-popover dark:bg-gray-800" 35 | /> 36 |
37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/note/NoteNewModal.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import useHotkeys from 'editor/hooks/useHotkeys'; 3 | import { store } from 'lib/store'; 4 | import FindOrCreateInput from './NoteNewInput'; 5 | 6 | type Props = { 7 | setIsOpen: (isOpen: boolean) => void; 8 | }; 9 | 10 | export default function FindOrCreateModal(props: Props) { 11 | const { setIsOpen } = props; 12 | 13 | const handleClose = useCallback(() => { 14 | store.getState().setCurrentCard(undefined); 15 | setIsOpen(false); 16 | }, [setIsOpen]) 17 | 18 | const hotkeys = useMemo( 19 | () => [ 20 | { 21 | hotkey: 'esc', 22 | callback: () => handleClose(), 23 | }, 24 | ], 25 | [handleClose] 26 | ); 27 | useHotkeys(hotkeys); 28 | 29 | return ( 30 |
31 |
35 |
36 | setIsOpen(false)} 38 | className="z-30 w-full max-w-screen-sm bg-white rounded shadow-popover dark:bg-gray-800" 39 | /> 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/note/NoteSumList.tsx: -------------------------------------------------------------------------------- 1 | import { IconPencil } from '@tabler/icons-react'; 2 | import { parser, serializer } from "mdsmirror"; 3 | import { useCurrentViewContext, DispatchType } from 'context/useCurrentView'; 4 | import { Note } from 'types/model'; 5 | import Tree from 'components/misc/Tree'; 6 | import Tooltip from 'components/misc/Tooltip'; 7 | import { openFilePath } from 'file/open'; 8 | 9 | type Props = { 10 | anchor: string; 11 | notes: Note[]; 12 | className?: string; 13 | isDate?: boolean; 14 | onClick?: (anchor: string) => void; 15 | }; 16 | 17 | export default function NoteSumList(props: Props) { 18 | const { anchor, notes, className, isDate, onClick } = props; 19 | const currentView = useCurrentViewContext(); 20 | const dispatch = currentView.dispatch; 21 | 22 | const nodeData = [ 23 | { 24 | id: anchor, 25 | labelNode: ( 26 |
27 | {anchor} 28 | {isDate ? ( 29 | 30 | 33 | 34 | ) : null} 35 |
36 | ), 37 | children: notes.map(noteToTreeData(dispatch)), 38 | } 39 | ]; 40 | 41 | const collapseAll = false; 42 | 43 | return ( 44 | 45 | ); 46 | } 47 | 48 | // eslint-disable-next-line react/display-name 49 | const noteToTreeData = (dispatch: DispatchType) => (note: Note) => { 50 | const doc = parser.parse(note.content); 51 | const value = doc.content.content 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | .filter((node: any) => node.type.name === 'paragraph') 54 | .slice(0, 2); 55 | const sum: string = serializer.serialize(value); 56 | 57 | return { 58 | id: note.id, 59 | labelNode: ( 60 |
61 | 72 |
{sum}
73 |
74 | ), 75 | showArrow: false, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/note/Title.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useRef } from 'react'; 2 | import { useStore } from 'lib/store'; 3 | 4 | type Props = { 5 | initialTitle: string; 6 | onChange: (value: string) => void; 7 | className?: string; 8 | isDaily?: boolean; 9 | }; 10 | 11 | function Title(props: Props) { 12 | const { 13 | initialTitle, 14 | onChange, 15 | className = '', 16 | isDaily = false, 17 | } = props; 18 | const titleRef = useRef(null); 19 | 20 | const isCheckSpellOn = useStore((state) => state.isCheckSpellOn); 21 | const readMode = useStore((state) => state.readMode); 22 | 23 | const emitChange = () => { 24 | if (!titleRef.current) { 25 | return; 26 | } 27 | const title = titleRef.current.textContent ?? ''; 28 | onChange(title); 29 | }; 30 | 31 | // Set the initial title 32 | useEffect(() => { 33 | if (!titleRef.current) { 34 | return; 35 | } 36 | titleRef.current.textContent = initialTitle; 37 | }, [initialTitle]); 38 | 39 | return ( 40 |
{ 46 | // Disallow newlines in the title field 47 | if (event.key === 'Enter') { 48 | event.preventDefault(); 49 | } 50 | }} 51 | onPaste={(event) => { 52 | // Remove styling and newlines from the text 53 | event.preventDefault(); 54 | let text = event.clipboardData.getData('text/plain'); 55 | text = text.replace(/\r?\n|\r/g, ' '); 56 | document.execCommand('insertText', false, text); 57 | }} 58 | onBlur={emitChange} 59 | contentEditable={!(readMode || isDaily)} 60 | spellCheck={isCheckSpellOn} 61 | /> 62 | ); 63 | } 64 | 65 | export default memo(Title); 66 | -------------------------------------------------------------------------------- /src/components/note/Toc.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { IconCaretRight, IconPoint } from '@tabler/icons-react'; 3 | 4 | export type Heading = { 5 | title: string; 6 | level: number; 7 | id: string; 8 | }; 9 | 10 | type Props = { 11 | headings: Heading[]; 12 | metaInfo?: string; 13 | className?: string; 14 | }; 15 | 16 | export default function Toc(props: Props) { 17 | const { headings, metaInfo = '', className = '' } = props; 18 | const [showTOC, setShowTOC] = useState(false); 19 | 20 | return ( 21 | <> 22 | 36 | {showTOC && headings.length ? ( 37 |
38 | {headings.map((heading) => ( 39 |
47 | 48 | {heading.title} 49 |
50 | ))} 51 |
52 | ) : null} 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/note/backlinks/BacklinkBranch.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | type BacklinkBranchProps = { 4 | title: string; 5 | }; 6 | 7 | const BacklinkBranch = (props: BacklinkBranchProps) => { 8 | const { title } = props; 9 | return

{title}

; 10 | }; 11 | 12 | export default memo(BacklinkBranch); 13 | -------------------------------------------------------------------------------- /src/components/note/backlinks/BacklinkMatchLeaf.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import MsEditor from "mdsmirror"; 3 | import useOnNoteLinkClick from 'editor/hooks/useOnNoteLinkClick'; 4 | import { useStore } from 'lib/store'; 5 | import { shortenString } from 'utils/helper'; 6 | import { BacklinkMatch } from './useBacklinks'; 7 | 8 | type BacklinkMatchLeafProps = { 9 | noteId: string; 10 | match: BacklinkMatch; 11 | className?: string; 12 | }; 13 | 14 | const BacklinkMatchLeaf = (props: BacklinkMatchLeafProps) => { 15 | const { noteId, match, className } = props; 16 | const { onClick: onNoteLinkClick } = useOnNoteLinkClick(); 17 | const darkMode = useStore((state) => state.darkMode); 18 | const isRTL = useStore((state) => state.isRTL); 19 | 20 | const leafValue: string = match.context 21 | ? getContextString(match.context) || match.text 22 | : match.text; 23 | const editorValue = shortenString(leafValue, match.text); 24 | 25 | const containerClassName = `block text-left text-xs rounded p-2 my-1 w-full break-words ${className}`; 26 | 27 | return ( 28 | 34 | ); 35 | }; 36 | 37 | export default memo(BacklinkMatchLeaf); 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | const getContextString = (nodes: any[]) => 41 | nodes.reduce((res, node) => res + ' ' + (node.text || ''), ''); 42 | -------------------------------------------------------------------------------- /src/components/note/backlinks/BacklinkNoteBranch.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import useOnNoteLinkClick from 'editor/hooks/useOnNoteLinkClick'; 3 | import { Backlink } from './useBacklinks'; 4 | 5 | type BacklinkNoteBranchProps = { 6 | backlink: Backlink; 7 | }; 8 | 9 | const BacklinkNoteBranch = (props: BacklinkNoteBranchProps) => { 10 | const { backlink } = props; 11 | const { onClick: onNoteLinkClick } = useOnNoteLinkClick(); 12 | 13 | return ( 14 | 23 | ); 24 | }; 25 | 26 | export default memo(BacklinkNoteBranch); 27 | -------------------------------------------------------------------------------- /src/components/note/backlinks/Backlinks.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useCurrentViewContext } from 'context/useCurrentView'; 3 | import Tree from 'components/misc/Tree'; 4 | import useBacklinks from './useBacklinks'; 5 | import type { Backlink, BacklinkMatch } from './useBacklinks'; 6 | import BacklinkBranch from './BacklinkBranch'; 7 | import BacklinkMatchLeaf from './BacklinkMatchLeaf'; 8 | import BacklinkNoteBranch from './BacklinkNoteBranch'; 9 | 10 | const MAX_EXPANDED_MATCHES = 42; 11 | 12 | type Props = { 13 | className?: string; 14 | isCollapse?: boolean; 15 | }; 16 | 17 | export default function Backlinks(props: Props) { 18 | const { className, isCollapse = false } = props; 19 | const currentView = useCurrentViewContext(); 20 | const params = currentView.state.params; 21 | const noteId = params?.noteId || ''; 22 | const { linkedBacklinks, unlinkedBacklinks } = useBacklinks(noteId); 23 | 24 | const backlinkData = useMemo( 25 | () => getTreeData(linkedBacklinks, unlinkedBacklinks), 26 | [linkedBacklinks, unlinkedBacklinks] 27 | ); 28 | 29 | const collapseAll = useMemo(() => { 30 | const numOfLinkedMatches = getNumOfMatches(linkedBacklinks); 31 | const numOfUnlinkedMatches = getNumOfMatches(unlinkedBacklinks); 32 | return ( 33 | isCollapse || 34 | numOfLinkedMatches > MAX_EXPANDED_MATCHES || 35 | numOfUnlinkedMatches > MAX_EXPANDED_MATCHES 36 | ); 37 | }, [linkedBacklinks, unlinkedBacklinks, isCollapse]); 38 | 39 | return ( 40 | 41 | ); 42 | } 43 | 44 | export const getNumOfMatches = (backlinks: Backlink[]) => 45 | backlinks.reduce( 46 | (numOfMatches, backlink) => numOfMatches + backlink.matches.length, 47 | 0 48 | ); 49 | 50 | const getTreeData = ( 51 | linkedBacklinks: Backlink[], 52 | unlinkedBacklinks: Backlink[] 53 | ) => { 54 | const numOfLinkedMatches = getNumOfMatches(linkedBacklinks); 55 | const numOfUnlinkedMatches = getNumOfMatches(unlinkedBacklinks); 56 | 57 | return [ 58 | { 59 | id: 'linked-backlinks', 60 | labelNode: (), 61 | children: linkedBacklinks.map(backlinkToTreeData(true)), 62 | }, 63 | { 64 | id: 'unlinked-backlinks', 65 | labelNode: (), 66 | children: unlinkedBacklinks.map(backlinkToTreeData(false)), 67 | }, 68 | ]; 69 | }; 70 | 71 | // eslint-disable-next-line react/display-name 72 | const backlinkToTreeData = (isLinked: boolean) => (backlink: Backlink) => { 73 | const matches: Array = []; 74 | const linePaths: Record = {}; 75 | 76 | // Only keep matches with unique line paths 77 | for (const match of backlink.matches) { 78 | const linePathKey = `${match.from}-${match.to}`; 79 | if (!linePaths[linePathKey]) { 80 | matches.push(match); 81 | linePaths[linePathKey] = true; 82 | } 83 | } 84 | 85 | const idPrefix = isLinked ? 'linked' : 'unlinked'; 86 | 87 | return { 88 | id: `${idPrefix}-${backlink.id}`, 89 | labelNode: , 90 | children: matches.map((match) => ({ 91 | id: `${idPrefix}-${backlink.id}-${match.from}-${match.to}`, 92 | labelNode: ( 93 | 98 | ), 99 | showArrow: false, 100 | })), 101 | }; 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/note/backlinks/updateBacklinks.ts: -------------------------------------------------------------------------------- 1 | import { store } from 'lib/store'; 2 | import { isUrl } from 'utils/helper'; 3 | import { LINK_REGEX, WIKILINK_REGEX } from 'components/view/ForceGraph' 4 | import { writeFile } from 'file/write'; 5 | import { loadDir } from 'file/open'; 6 | import { computeLinkedBacklinks } from './useBacklinks'; 7 | 8 | /** 9 | * Updates the backlink properties of notes on the current note title changed. 10 | * the current note is the note other notes link to 11 | * @param noteTitle of current note 12 | * @param newTitle of current note, it is undefined on delete note 13 | */ 14 | const updateBacklinks = async (noteTitle: string, newTitle?: string) => { 15 | const isLoaded = store.getState().isLoaded; 16 | const setIsLoaded = store.getState().setIsLoaded; 17 | const initDir = store.getState().initDir; 18 | // console.log("updateBackLinks loaded?", isLoaded); 19 | if (!isLoaded && initDir) { 20 | loadDir(initDir).then(() => setIsLoaded(true)); 21 | } 22 | 23 | const notes = store.getState().notes; 24 | const updateNote = store.getState().updateNote; 25 | const backlinks = computeLinkedBacklinks(notes, noteTitle); 26 | for (const backlink of backlinks) { 27 | const note = notes[backlink.id]; 28 | if (!note) { 29 | continue; 30 | } 31 | 32 | let content = note.content; 33 | // CASE: []() 34 | const link_array: RegExpMatchArray[] = [...note.content.matchAll(LINK_REGEX)]; 35 | for (const match of link_array) { 36 | const href = match[2]; 37 | if (!isUrl(href)) { 38 | const title = decodeURI(href); 39 | if (noteTitle === title) { 40 | newTitle = newTitle?.trim(); 41 | const replaceTo = newTitle 42 | ? `[${match[1]}](${encodeURI(newTitle)})` // rename 43 | : match[1] // delete 44 | content = content.replaceAll(match[0], replaceTo); 45 | } 46 | } 47 | } 48 | // CASE: [[]] 49 | const wiki_array: RegExpMatchArray[] = [...note.content.matchAll(WIKILINK_REGEX)]; 50 | // console.log("wiki arr", wiki_array, noteTitle, newTitle) 51 | for (const match of wiki_array) { 52 | const href = match[1]; 53 | if (!isUrl(href)) { 54 | const title = href; 55 | if (noteTitle === title) { 56 | newTitle = newTitle?.trim(); 57 | const replaceTo = newTitle 58 | ? `[[${newTitle}]]` // rename 59 | : match[1] // delete 60 | content = content.replaceAll(match[0], replaceTo); 61 | } 62 | } 63 | } 64 | 65 | // update content and write file 66 | updateNote({ id: note.id, content }); 67 | await writeFile(note?.file_path, content); 68 | } 69 | }; 70 | 71 | export default updateBacklinks; 72 | -------------------------------------------------------------------------------- /src/components/settings/AboutModal.tsx: -------------------------------------------------------------------------------- 1 | import { getVersion, getTauriVersion } from '@tauri-apps/api/app'; 2 | import { writeText } from '@tauri-apps/api/clipboard'; 3 | import { useState, useEffect } from 'react'; 4 | import { openUrl } from 'file/open'; 5 | import { getLog, clearLog } from 'file/storage'; 6 | import { BaseModal } from './BaseModal'; 7 | 8 | 9 | type Props = { 10 | isOpen: boolean; 11 | handleClose: () => void; 12 | } 13 | 14 | export default function AboutModal({ isOpen, handleClose }: Props) { 15 | const [appVersion, setAppVersion] = useState(''); 16 | const [tauriVersion, setTauriVersion] = useState(''); 17 | const [hasVersion, setHasVersion] = useState(false); 18 | 19 | useEffect(() => { 20 | if (!hasVersion) { 21 | getVersion().then(ver => setAppVersion(ver)).catch(() => {/**/}); 22 | getTauriVersion().then(ver => setTauriVersion(ver)).catch(() => {/**/}); 23 | } 24 | return () => { setHasVersion(true); }; 25 | }, [hasVersion]); 26 | 27 | return ( 28 | 29 |
30 |

mdSilo Desktop

31 |

App Version: {appVersion}

32 |

Tauri Version: {tauriVersion}

33 |
34 | 40 | 48 | 58 |
59 | 67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/settings/BaseModal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | The following was modified from the source code: 3 | https://github.com/cwackerfuss/react-wordle/blob/main/src/components/modals/BaseModal.tsx 4 | 5 | MIT License Copyright (c) 2022 Hannah Park 6 | */ 7 | 8 | import { Fragment } from 'react'; 9 | import { Dialog, Transition } from '@headlessui/react'; 10 | import { IconX } from '@tabler/icons-react'; 11 | 12 | type Props = { 13 | title: string; 14 | children: React.ReactNode; 15 | isOpen: boolean; 16 | handleClose: () => void; 17 | } 18 | 19 | export const BaseModal = ({ title, children, isOpen, handleClose }: Props) => { 20 | return ( 21 | 22 | 27 |
28 | 37 | 40 | 41 | 47 | 56 |
57 |
58 | handleClose()} 62 | /> 63 |
64 |
65 |
66 | 70 | {title} 71 | 72 |
{children}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/components/settings/SettingsToggle.tsx: -------------------------------------------------------------------------------- 1 | import Toggle from 'components/misc/Toggle'; 2 | 3 | type Props = { 4 | name: string; 5 | descript?: string; 6 | check: boolean; 7 | handleCheck: (isChecked: boolean) => void; 8 | optionLeft?: string; 9 | optionRight?: string; 10 | } 11 | 12 | export const SettingsToggle = (props: Props) => { 13 | const { 14 | name, 15 | descript, 16 | check, 17 | handleCheck, 18 | optionLeft = 'Off', 19 | optionRight = 'On', 20 | } = props; 21 | 22 | return ( 23 |
24 |
25 |

{name}

26 |

{descript}

27 |
28 |
29 | {optionLeft} 30 | 36 | {optionRight} 37 |
38 |
39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { useTransition, animated, SpringConfig } from '@react-spring/web'; 3 | import { isMobile } from 'utils/helper'; 4 | import { useStore } from 'lib/store'; 5 | import SidebarContent from './SidebarContent'; 6 | import SidebarHeader from './SidebarHeader'; 7 | 8 | const SPRING_CONFIG: SpringConfig = { 9 | mass: 1, 10 | tension: 170, 11 | friction: 10, 12 | clamp: true, 13 | } as const; 14 | 15 | type Props = { 16 | className?: string; 17 | }; 18 | 19 | function Sidebar(props: Props) { 20 | const { className='' } = props; 21 | 22 | const isSidebarOpen = useStore((state) => state.isSidebarOpen); 23 | const setIsSidebarOpen = useStore((state) => state.setIsSidebarOpen); 24 | 25 | const transition = useTransition< 26 | boolean, 27 | { 28 | transform: string; 29 | dspl: number; 30 | backgroundOpacity: number; 31 | backgroundColor: string; 32 | } 33 | >(isSidebarOpen, { 34 | initial: { 35 | transform: 'translateX(0%)', 36 | dspl: 1, 37 | backgroundOpacity: 0.3, 38 | backgroundColor: 'black', 39 | }, 40 | from: { 41 | transform: 'translateX(-100%)', 42 | dspl: 0, 43 | backgroundOpacity: 0, 44 | backgroundColor: 'transparent', 45 | }, 46 | enter: { 47 | transform: 'translateX(0%)', 48 | dspl: 1, 49 | backgroundOpacity: 0.3, 50 | backgroundColor: 'black', 51 | }, 52 | leave: { 53 | transform: 'translateX(-100%)', 54 | dspl: 0, 55 | backgroundOpacity: 0, 56 | backgroundColor: 'transparent', 57 | }, 58 | config: SPRING_CONFIG, 59 | expires: (item) => !item, 60 | }); 61 | 62 | return transition( 63 | (styles, item) => 64 | item && ( 65 | <> 66 | {isMobile() ? ( 67 | 73 | displ === 0 ? 'none' : 'initial' 74 | ), 75 | }} 76 | onClick={() => setIsSidebarOpen(false)} 77 | /> 78 | ) : null} 79 | 84 | displ === 0 ? 'none' : 'initial' 85 | ), 86 | }} 87 | > 88 |
91 | 92 | 93 |
94 |
95 | 96 | ) 97 | ); 98 | } 99 | 100 | export default memo(Sidebar); 101 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarContent.tsx: -------------------------------------------------------------------------------- 1 | import { IconFolder, IconHash, IconPlaylist, IconSearch } from '@tabler/icons-react'; 2 | import Tooltip from 'components/misc/Tooltip'; 3 | import { SidebarTab as SidebarTabType, useStore } from 'lib/store'; 4 | import SidebarNotes from './SidebarNotes'; 5 | import SidebarPlaylist from './SidebarPlaylist'; 6 | import SidebarSearch from './SidebarSearch'; 7 | import SidebarTab from './SidebarTab'; 8 | import SidebarTags from './SidebarTags'; 9 | 10 | type Props = { 11 | className?: string; 12 | }; 13 | 14 | export default function SidebarContent(props: Props) { 15 | const { className } = props; 16 | const activeTab = useStore((state) => state.sidebarTab); 17 | const setActiveTab = useStore((state) => state.setSidebarTab); 18 | 19 | return ( 20 |
21 | 22 |
23 | {activeTab === SidebarTabType.Silo ? : null} 24 | {activeTab === SidebarTabType.Search ? : null} 25 | {activeTab === SidebarTabType.Hashtag ? : null} 26 | {activeTab === SidebarTabType.Playlist ? : null} 27 |
28 |
29 | ); 30 | } 31 | 32 | type TabsProps = { 33 | activeTab: SidebarTabType; 34 | setActiveTab: (tab: SidebarTabType) => void; 35 | }; 36 | 37 | const Tabs = (props: TabsProps) => { 38 | const { activeTab, setActiveTab } = props; 39 | 40 | return ( 41 |
42 | 43 | setActiveTab(SidebarTabType.Silo)} 46 | Icon={IconFolder} 47 | /> 48 | 49 | 50 | setActiveTab(SidebarTabType.Search)} 53 | Icon={IconSearch} 54 | /> 55 | 56 | 57 | setActiveTab(SidebarTabType.Hashtag)} 60 | Icon={IconHash} 61 | /> 62 | 63 | 64 | setActiveTab(SidebarTabType.Playlist)} 67 | Icon={IconPlaylist} 68 | /> 69 | 70 |
71 | ); 72 | }; 73 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarHeader.tsx: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api'; 2 | import { Menu } from '@headlessui/react'; 3 | import { 4 | IconChevronsDown, IconChevronLeft, IconSettings, IconBrowser, 5 | IconPizza, IconInfoCircle, IconCurrentLocation 6 | } from '@tabler/icons-react'; 7 | import { useStore } from 'lib/store'; 8 | import Tooltip from 'components/misc/Tooltip'; 9 | import { DropdownItem } from 'components/misc/Dropdown'; 10 | import { isMobile } from 'utils/helper'; 11 | 12 | 13 | export default function SidebarHeader() { 14 | const setIsSidebarOpen = useStore((state) => state.setIsSidebarOpen); 15 | const setIsSettingsOpen = useStore((state) => state.setIsSettingsOpen); 16 | const setIsAboutOpen = useStore((state) => state.setIsAboutOpen); 17 | 18 | return ( 19 |
20 | 21 | 22 |
23 | mdSilo 24 | 25 |
26 | 27 | { 30 | e.stopPropagation(); 31 | setIsSidebarOpen(false); 32 | }} 33 | > 34 | 35 | 36 | 37 |
38 | 39 | { 41 | if (isMobile()) { 42 | setIsSidebarOpen(false); 43 | } 44 | setIsSettingsOpen(true); 45 | }} 46 | > 47 | 48 | Settings 49 | 50 | 55 | 56 | Website 57 | 58 | 63 | 64 | Help Us 65 | 66 | setIsAboutOpen(true)}> 67 | 68 | About 69 | 70 | { 72 | const dir_path = await invoke("create_mdsilo_dir"); 73 | await invoke("open_url", {url: dir_path}); 74 | }} 75 | > 76 | 77 | Local mdsilo 78 | 79 | 80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, forwardRef, HTMLAttributes, memo } from 'react'; 2 | 3 | interface SidebarItemProps extends HTMLAttributes { 4 | isHighlighted?: boolean; 5 | } 6 | 7 | function SidebarItem( 8 | props: SidebarItemProps, 9 | forwardedRef: ForwardedRef 10 | ) { 11 | const { children, className = '', isHighlighted, ...otherProps } = props; 12 | const itemClassName = `w-full overflow-x-hidden overflow-ellipsis whitespace-nowrap text-gray-800 hover:bg-gray-200 dark:text-gray-300 dark:hover:bg-gray-700 ${isHighlighted ? 'bg-gray-300 dark:bg-gray-600' : 'bg-gray-50 dark:bg-gray-800'} ${className}`; 13 | 14 | return ( 15 |
16 | {children} 17 |
18 | ); 19 | } 20 | 21 | export default memo(forwardRef(SidebarItem)); 22 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarNotes.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from 'react'; 2 | import { NoteTreeItem, useStore } from 'lib/store'; 3 | import { Sort } from 'lib/userSettings'; 4 | import { ciStringCompare, dateCompare } from 'utils/helper'; 5 | import { onOpenFile, onListDir } from 'editor/hooks/useOpen'; 6 | import ErrorBoundary from '../misc/ErrorBoundary'; 7 | import SidebarNotesBar from './SidebarNotesBar'; 8 | import SidebarNotesTree from './SidebarNotesTree'; 9 | import SidebarHistory from './SidebarHistory'; 10 | 11 | type SidebarNotesProps = { 12 | className?: string; 13 | }; 14 | 15 | function SidebarNotes(props: SidebarNotesProps) { 16 | const { className='' } = props; 17 | 18 | const currentDir = useStore((state) => state.currentDir); 19 | 20 | const noteTree = useStore((state) => state.noteTree); 21 | const noteSort = useStore((state) => state.noteSort); 22 | // console.log("note tree", noteTree) 23 | const [sortedNoteTree, numOfNotes] = useMemo(() => { 24 | if (currentDir) { 25 | const treeList = noteTree[currentDir] || []; 26 | return [sortNoteTree(treeList, noteSort), treeList.length]; 27 | } else { 28 | return [[], 0]; 29 | } 30 | }, [noteTree, currentDir, noteSort]); 31 | 32 | // console.log("tree", numOfNotes, sortedNoteTree, currentDir) 33 | 34 | const btnClass = "p-1 mt-4 mx-4 text-white rounded bg-blue-500 hover:bg-blue-800"; 35 | 36 | return ( 37 | 38 |
39 | {currentDir ? ( 40 | 44 | ) : null } 45 | {sortedNoteTree && sortedNoteTree.length > 0 ? ( 46 | 50 | ) : currentDir ? null : ( 51 | <> 52 | 53 | 54 | 55 | 56 | )} 57 |
58 |
59 | ); 60 | } 61 | 62 | /** 63 | * Sorts the tree item with the given noteSort. 64 | */ 65 | const sortNoteTree = ( 66 | tree: NoteTreeItem[], 67 | noteSort: Sort 68 | ): NoteTreeItem[] => { 69 | // Copy tree shallowly 70 | const newTree = [...tree]; 71 | // Sort tree items (one level) 72 | if (newTree.length >= 2) { 73 | newTree.sort((n1, n2) => { 74 | switch (noteSort) { 75 | case Sort.DateModifiedAscending: 76 | return dateCompare(n1.updated_at, n2.updated_at); 77 | case Sort.DateModifiedDescending: 78 | return dateCompare(n2.updated_at, n1.updated_at); 79 | case Sort.DateCreatedAscending: 80 | return dateCompare(n1.created_at, n2.created_at); 81 | case Sort.DateCreatedDescending: 82 | return dateCompare(n2.created_at, n1.created_at); 83 | case Sort.TitleAscending: 84 | return ciStringCompare(n1.title, n2.title); 85 | case Sort.TitleDescending: 86 | return ciStringCompare(n2.title, n1.title); 87 | default: 88 | return ciStringCompare(n1.title, n2.title); 89 | } 90 | }); 91 | newTree.sort((n1, n2) => Number(Boolean(n2.is_dir)) - Number(Boolean(n1.is_dir))); 92 | } 93 | 94 | return newTree; 95 | }; 96 | 97 | export default memo(SidebarNotes); 98 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarNotesBar.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { IconArrowBarToUp } from '@tabler/icons-react'; 3 | import { useStore } from 'lib/store'; 4 | import { Sort } from 'lib/userSettings'; 5 | import Tooltip from 'components/misc/Tooltip'; 6 | import { normalizeSlash, getParentDir } from 'file/util'; 7 | import { listDirPath } from 'editor/hooks/useOpen'; 8 | import SidebarNotesSortDropdown from './SidebarNotesSortDropdown'; 9 | import { SidebarDirDropdown } from './SidebarDropdown'; 10 | 11 | type Props = { 12 | noteSort: Sort; 13 | numOfNotes: number; 14 | }; 15 | 16 | function SidebarNotesBar(props: Props) { 17 | const { noteSort, numOfNotes} = props; 18 | const setNoteSort = useStore((state) => state.setNoteSort); 19 | 20 | const initDir = useStore((state) => state.initDir); 21 | const currentDir = useStore((state) => state.currentDir); 22 | const checkInit: boolean = !currentDir || currentDir === initDir; 23 | 24 | const currentFolder = currentDir 25 | ? normalizeSlash(currentDir).split('/').pop() || '/' 26 | : 'md'; 27 | const barClass = `px-2 text-sm bg-blue-500 text-white rounded overflow-hidden`; 28 | 29 | return ( 30 |
31 |
32 | 36 |
37 | 38 |
39 |
40 | {currentDir ? ( 41 | 47 | ) : ( 48 | 49 | {currentFolder}: {numOfNotes} 50 | 51 | )} 52 |
53 |
54 |
55 | 56 | 68 | 69 |
70 | ); 71 | } 72 | 73 | export default memo(SidebarNotesBar); 74 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarNotesSortDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState, memo } from 'react'; 2 | import { Menu } from '@headlessui/react'; 3 | import { IconSortDescending, IconCheck } from '@tabler/icons-react'; 4 | import { usePopper } from 'react-popper'; 5 | import { ReadableNameBySort, Sort } from 'lib/userSettings'; 6 | import Tooltip from 'components/misc/Tooltip'; 7 | import Portal from 'components/misc/Portal'; 8 | 9 | type Props = { 10 | currentSort: Sort; 11 | setCurrentSort: (sort: Sort) => void; 12 | }; 13 | 14 | const SidebarNotesSortDropdown = (props: Props) => { 15 | const { currentSort, setCurrentSort } = props; 16 | 17 | const buttonRef = useRef(null); 18 | const [popperElement, setPopperElement] = useState( 19 | null 20 | ); 21 | const { styles, attributes } = usePopper(buttonRef.current, popperElement, { 22 | placement: 'bottom-start', 23 | }); 24 | 25 | return ( 26 | 27 | {({ open }) => ( 28 | <> 29 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | {open && ( 43 | 44 | 51 | {Object.values(Sort).map((sort, index, arr) => { 52 | const isActive = currentSort === sort; 53 | const showDivider = 54 | (index + 1) % 2 === 0 && index !== arr.length - 1; 55 | return ( 56 | 57 | {({ active }) => ( 58 | 80 | )} 81 | 82 | ); 83 | })} 84 | 85 | 86 | )} 87 | 88 | )} 89 | 90 | ); 91 | }; 92 | 93 | export default memo(SidebarNotesSortDropdown); 94 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarNotesTree.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useCallback, memo } from 'react'; 2 | import List from 'react-virtualized/dist/commonjs/List'; 3 | import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'; 4 | import { NoteTreeItem } from 'lib/store'; 5 | import { useCurrentViewContext } from 'context/useCurrentView'; 6 | import SidebarNoteLink from './SidebarNoteLink'; 7 | 8 | type Props = { 9 | data: NoteTreeItem[]; 10 | className?: string; 11 | }; 12 | 13 | function SidebarNotesTree(props: Props) { 14 | const { data, className } = props; 15 | 16 | const currentView = useCurrentViewContext(); 17 | const params = currentView.state.params; 18 | const noteId = params?.noteId || ''; 19 | 20 | const currentNoteId = useMemo(() => { 21 | const id = noteId; 22 | return id && typeof id === 'string' ? id : undefined; 23 | }, [noteId]); 24 | 25 | const Row = useCallback( 26 | ({ index, style }: {index: number; style: React.CSSProperties}) => { 27 | const node = data[index]; 28 | return ( 29 | 35 | ); 36 | }, 37 | [currentNoteId, data] 38 | ); 39 | 40 | return ( 41 |
42 | 43 | {({ width, height }) => ( 44 | 51 | )} 52 | 53 |
54 | ); 55 | } 56 | 57 | export default memo(SidebarNotesTree); 58 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarTab.tsx: -------------------------------------------------------------------------------- 1 | import { ForwardedRef, forwardRef, memo } from 'react'; 2 | import { Icon } from '@tabler/icons-react'; 3 | 4 | type Props = { 5 | isActive: boolean; 6 | setActive: () => void; 7 | Icon: Icon; 8 | className?: string; 9 | }; 10 | 11 | const SidebarTab = ( 12 | props: Props, 13 | forwardedRef: ForwardedRef 14 | ) => { 15 | const { isActive, setActive, Icon, className = '' } = props; 16 | return ( 17 | 31 | ); 32 | }; 33 | 34 | export default memo(forwardRef(SidebarTab)); 35 | -------------------------------------------------------------------------------- /src/components/sidebar/StatusBar.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useCallback, useState } from 'react'; 2 | import { IconHeadphones } from '@tabler/icons-react'; 3 | import { store, useStore } from 'lib/store'; 4 | import AudioPlayer from 'components/feed/AudioPlayer'; 5 | import * as dataAgent from 'components/feed/dataAgent'; 6 | import { useCurrentViewContext } from 'context/useCurrentView'; 7 | 8 | type Props = { 9 | className?: string; 10 | }; 11 | 12 | function Statusbar(props: Props) { 13 | const { className = '' } = props; 14 | 15 | const currentPod = useStore((state) => state.currentPod); 16 | const [hide, setHide] = useState(false); 17 | // console.log("current pod: ", currentPod) 18 | const currentView = useCurrentViewContext(); 19 | const dispatch = currentView.dispatch; 20 | const toArticle = useCallback(async (url: string) => { 21 | const article = await dataAgent.getArticleByUrl(url); 22 | if (article) { 23 | store.getState().setCurrentArticle(article); 24 | dispatch({view: 'feed'}); 25 | } 26 | }, [dispatch]) 27 | 28 | return ( 29 |
30 | 33 | {currentPod && ( 34 |
35 | await toArticle(currentPod.article_url)} 38 | > 39 | {currentPod.title} 40 | 41 | 42 |
43 | )} 44 |
45 | ); 46 | } 47 | 48 | export default memo(Statusbar); 49 | -------------------------------------------------------------------------------- /src/components/view/MainView.tsx: -------------------------------------------------------------------------------- 1 | import MsEditor from "mdsmirror"; 2 | import ErrorBoundary from 'components/misc/ErrorBoundary'; 3 | import { useCurrentViewContext } from 'context/useCurrentView'; 4 | import Chronicle from './chronicle'; 5 | import Journals from './journals'; 6 | import Tasks from './tasks'; 7 | import Kanban from './kanban'; 8 | import Graph from './graph'; 9 | import NotePage from './md'; 10 | import HashTags from "./hashtags"; 11 | import Feed from "./feed"; 12 | 13 | export default function MainView() { 14 | const currentView = useCurrentViewContext(); 15 | const viewTy = currentView.state.view; 16 | // 17 | return ( 18 | <> 19 | {viewTy === 'default' ? ( 20 | 21 | ) : viewTy === 'feed' ? ( 22 | 23 | ) : viewTy === 'chronicle' ? ( 24 | 25 | ) : viewTy === 'task' ? ( 26 | 27 | ) : viewTy === 'kanban' ? ( 28 | 29 | ) : viewTy === 'graph' ? ( 30 | 31 | ) : viewTy === 'journal' ? ( 32 | 33 | ) : viewTy === 'tag' ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | ); 40 | } 41 | 42 | function DefaultView() { 43 | return ( 44 | 45 |
46 |

47 | Hello, welcome to mdSilo Desktop. 48 |

49 | 50 |
51 |
52 | ); 53 | } 54 | 55 | const defaultValue = ` 56 | A lightweight, local-first personal Wiki and knowledge base for storing ideas, thought, knowledge with the powerful all-in-one reading/writing tool. Use it to organize writing, network thoughts and build a Second Brain on top of local plain text Markdown files. 57 | 58 | ## Features 59 | - ➰ I/O: Feed & Podcast client(Input) and Personal Wiki(Output); 60 | - 🔀 All-In-One Editor: Markdown, WYSIWYG, Mind Map... 61 | - 📝 Markdown and extensions: Math/Chemical Equation, Diagram, Hashtag... 62 | - 🗄️ Build personal wiki with bidirectional wiki links 63 | - ⌨️ Slash commands, Hotkeys and Hovering toolbar... 64 | - 📋 Kanban board to manage the process of knowledge growing 65 | - 🕸️ Graph view to visualize the networked writing 66 | - 📅 Chronicle view and Daily activities graph 67 | - ✔️ Task view to track todo/doing/done 68 | - 🔍 Full-text search 69 | - ✨ Available for Windows, macOS, Linux and Web 70 | 71 | For human brain, Reading and Writing is the I/O: the communication between the information processing system and the outside world. mdSilo is here to boost your daily I/O, it is tiny yet powerful, free for everyone. 72 | \\ 73 | `; 74 | -------------------------------------------------------------------------------- /src/components/view/graph.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import ErrorBoundary from 'components/misc/ErrorBoundary'; 3 | import ForceGraph from 'components/view/ForceGraph'; 4 | import { useStore } from 'lib/store'; 5 | import { loadDir } from 'file/open'; 6 | 7 | export default function Graph() { 8 | const isLoaded = useStore((state) => state.isLoaded); 9 | const setIsLoaded = useStore((state) => state.setIsLoaded); 10 | const initDir = useStore((state) => state.initDir); 11 | // console.log("g loaded?", isLoaded); 12 | useEffect(() => { 13 | if (!isLoaded && initDir) { 14 | loadDir(initDir).then(() => setIsLoaded(true)); 15 | } 16 | }, [initDir, isLoaded, setIsLoaded]); 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/view/hashtags.tsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from 'components/misc/ErrorBoundary'; 2 | import { useCurrentViewContext } from 'context/useCurrentView'; 3 | import { SearchTree } from 'components/sidebar/SidebarSearch'; 4 | 5 | export default function HashTags() { 6 | const className = ''; 7 | const currentView = useCurrentViewContext(); 8 | const tag = currentView.state.tag || ''; 9 | 10 | return ( 11 | 12 |
13 | 14 | {`#${tag}`} 15 | 16 | 17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/components/view/journals.tsx: -------------------------------------------------------------------------------- 1 | import MsEditor from "mdsmirror"; 2 | import { useCurrentViewContext } from 'context/useCurrentView'; 3 | import { useStore } from 'lib/store'; 4 | import { Note } from 'types/model'; 5 | import ErrorBoundary from 'components/misc/ErrorBoundary'; 6 | import FindOrCreateInput from 'components/note/NoteNewInput'; 7 | import { dateCompare, regDateStr, strToDate } from 'utils/helper'; 8 | import { openFilePath } from "file/open"; 9 | 10 | export default function Journals() { 11 | const initDir = useStore((state) => state.initDir); 12 | const notes = useStore((state) => state.notes); 13 | const notesArr = Object.values(notes); 14 | const dailyNotes = notesArr.filter(n => regDateStr.test(n.title)); 15 | dailyNotes.sort( 16 | (n1, n2) => dateCompare(strToDate(n2.title), strToDate(n1.title)) 17 | ); 18 | 19 | return ( 20 | 21 |
22 |
23 | {initDir ? ( 24 | 27 | ) : null} 28 |
29 |
30 | {dailyNotes.map((n) => ())} 31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | type NoteItemProps = { 38 | note: Note; 39 | }; 40 | 41 | function NoteItem(props: NoteItemProps) { 42 | const { note } = props; 43 | const currentView = useCurrentViewContext(); 44 | const dispatch = currentView.dispatch; 45 | const darkMode = useStore((state) => state.darkMode); 46 | const isRTL = useStore((state) => state.isRTL); 47 | 48 | return ( 49 |
50 | 61 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/view/kanban.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect, useMemo } from 'react'; 2 | import { useStore } from 'lib/store'; 3 | import ErrorBoundary from 'components/misc/ErrorBoundary'; 4 | import KanbanBoard from 'components/kanban/Board'; 5 | import { Card, Column, Kanbans } from 'components/kanban/types'; 6 | import { joinPath } from 'file/util'; 7 | import FileAPI from 'file/files'; 8 | 9 | export default function Kanban() { 10 | const initDir = useStore((state) => state.initDir); 11 | const currentKanban = useStore((state) => state.currentBoard); 12 | const setCurrentKanban = useStore((state) => state.setCurrentBoard); 13 | const [kanbans, setKanbans] = useState({}); 14 | 15 | // console.log("currentKanban", currentKanban, kanbans); 16 | 17 | useEffect(() => { 18 | const kanbanJsonPath = initDir ? joinPath(initDir, `kanban.json`) : ''; 19 | if (kanbanJsonPath) { 20 | const jsonFile = new FileAPI(kanbanJsonPath); 21 | jsonFile.readFile().then(json => { 22 | const kanbans: Kanbans = JSON.parse(json || "{}"); 23 | // console.log("effect Kanbans", kanbans); 24 | setKanbans(kanbans); 25 | }); 26 | } 27 | }, [initDir]); 28 | 29 | const onKanbanChange = useCallback( 30 | async (columns: Column[], cards: Card[], bgColor?: string, bgImg?: string) => { 31 | const saveFile = new FileAPI('kanban.json', initDir); 32 | const name = currentKanban || "default"; 33 | const oldData = kanbans[name]; 34 | const newData = { 35 | columns, 36 | cards, 37 | bgColor: bgColor || oldData?.bgColor, 38 | bgImg: bgImg || oldData?.bgImg, 39 | }; 40 | kanbans[name] = newData; 41 | // console.log("to save kanba", kanbans); 42 | await saveFile.writeFile(JSON.stringify(kanbans)); 43 | }, 44 | [currentKanban, initDir, kanbans] 45 | ); 46 | 47 | const [newKanban, setNewKanban] = useState("new kanban"); 48 | 49 | const kanbanData = useMemo(() => { 50 | const name = currentKanban || "default"; 51 | return kanbans[name] ?? {columns: [], cards: []}; 52 | }, [currentKanban, kanbans]); 53 | 54 | return ( 55 | 56 |
57 |
58 | {Object.keys(kanbans).map((k, index) => ( 59 | 64 | ))} 65 | {setNewKanban(ev.target.value);}} 71 | onKeyDown={(ev) => { 72 | if (ev.key !== "Enter") return; 73 | setCurrentKanban(newKanban); 74 | }} 75 | /> 76 | 77 | {Object.keys(kanbans).map((k, index) => ( 78 | 79 | ))} 80 | 81 |
82 | {kanbanData && ( 83 | 88 | )} 89 |
90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/components/view/md.tsx: -------------------------------------------------------------------------------- 1 | import { useCurrentViewContext } from 'context/useCurrentView'; 2 | import Note from 'components/note/Note'; 3 | 4 | export default function NotePage() { 5 | const currentView = useCurrentViewContext(); 6 | const params = currentView.state.params; 7 | const noteId = params?.noteId || ''; 8 | // const hlHash = params?.hash || ''; 9 | 10 | if (!noteId || typeof noteId !== 'string') { 11 | return ( 12 |
13 |

14 | This note does not exists! 15 |

16 |
17 | ); 18 | } 19 | 20 | return ( 21 |
22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/context/useCurrentMd.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { useContext, createContext, Dispatch } from 'react'; 3 | import { ViewAction, ViewState } from './viewReducer'; 4 | 5 | type CurrentMd = { 6 | ty: string; 7 | id: string; 8 | state: ViewState; 9 | dispatch: Dispatch; 10 | }; 11 | 12 | const CurrContext = createContext(undefined); 13 | 14 | type Props = { 15 | children: ReactNode; 16 | value: CurrentMd; 17 | }; 18 | 19 | export function ProvideCurrentMd({ children, value }: Props) { 20 | return {children}; 21 | } 22 | 23 | export const useCurrentMdContext = () => { 24 | const context = useContext(CurrContext); 25 | if (context === undefined) { 26 | throw new Error('useCurrentContext must be used within a provider'); 27 | } 28 | return context; 29 | }; 30 | -------------------------------------------------------------------------------- /src/context/useCurrentView.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { useContext, createContext, useReducer, Dispatch } from 'react'; 3 | import { viewReducer, initialState, ViewAction, ViewState } from './viewReducer'; 4 | 5 | export type DispatchType = Dispatch; 6 | 7 | type CurrentView = { 8 | state: ViewState; 9 | dispatch: DispatchType; 10 | }; 11 | 12 | const CurrentViewContext = createContext(undefined); 13 | 14 | export function ProvideCurrentView({ children }: {children: ReactNode;}) { 15 | const [state, dispatch] = useReducer(viewReducer, initialState); 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | export const useCurrentViewContext = () => { 24 | const context = useContext(CurrentViewContext); 25 | if (context === undefined) { 26 | throw new Error('useCurrentViewContext must be used within a provider'); 27 | } 28 | return context; 29 | }; 30 | -------------------------------------------------------------------------------- /src/context/viewReducer.ts: -------------------------------------------------------------------------------- 1 | import { store } from 'lib/store'; 2 | import { setWindowTitle } from 'file/util'; 3 | 4 | export const initialState = {view: 'default'}; 5 | 6 | type ViewParams = { noteId: string; stackIds?: string[], hash?: string }; 7 | 8 | export interface ViewState { 9 | view: string; 10 | params?: ViewParams; 11 | tag?: string; 12 | } 13 | 14 | export type ViewAction = 15 | | { view: 'default' } 16 | | { view: 'feed' } 17 | | { view: 'chronicle' } 18 | | { view: 'task' } 19 | | { view: 'graph' } 20 | | { view: 'journal' } 21 | | { view: 'kanban' } 22 | | { 23 | view: 'md'; 24 | params: ViewParams; 25 | } 26 | | { 27 | view: 'tag'; 28 | tag: string; 29 | }; 30 | 31 | export function viewReducer(state: ViewState, action: ViewAction): ViewState { 32 | const actionView = action.view; 33 | if (actionView === 'md') { 34 | store.getState().setCurrentNoteId(action.params.noteId); 35 | } else { 36 | store.getState().setCurrentNoteId(''); 37 | setWindowTitle(`: ${actionView} - mdSilo`); 38 | } 39 | 40 | switch (actionView) { 41 | case 'default': 42 | return {...state, view: 'default'}; 43 | case 'feed': 44 | return {...state, view: 'feed'}; 45 | case 'chronicle': 46 | return {...state, view: 'chronicle'}; 47 | case 'task': 48 | return {...state, view: 'task'}; 49 | case 'graph': 50 | return {...state, view: 'graph'}; 51 | case 'kanban': 52 | return {...state, view: 'kanban'}; 53 | case 'journal': 54 | return {...state, view: 'journal'}; 55 | case 'md': 56 | return {view: 'md', params: action.params}; 57 | case 'tag': 58 | return {view: 'tag', tag: action.tag}; 59 | default: 60 | throw new Error(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/editor/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useState } from 'react'; 2 | 3 | export default function useDebounce( 4 | value: T, 5 | delay: number 6 | ): [T, Dispatch>] { 7 | const [debouncedValue, setDebouncedValue] = useState(value); 8 | 9 | useEffect(() => { 10 | const handler = setTimeout(() => { 11 | setDebouncedValue(value); 12 | }, delay); 13 | 14 | return () => { 15 | clearTimeout(handler); 16 | }; 17 | }, [value, delay]); 18 | 19 | return [debouncedValue, setDebouncedValue]; 20 | } 21 | -------------------------------------------------------------------------------- /src/editor/hooks/useDeleteNote.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useCurrentViewContext } from 'context/useCurrentView'; 3 | import { store } from 'lib/store'; 4 | import updateBacklinks from 'components/note/backlinks/updateBacklinks'; 5 | import { deleteFile } from 'file/write'; 6 | 7 | export default function useDeleteNote(noteId: string, noteTitle: string) { 8 | const currentView = useCurrentViewContext(); 9 | const dispatch = currentView.dispatch; 10 | 11 | const onDeleteClick = useCallback(async () => { 12 | dispatch({view: 'default'}); 13 | doDeleteNote(noteId, noteTitle); 14 | }, [dispatch, noteId, noteTitle]); 15 | 16 | return onDeleteClick; 17 | } 18 | 19 | export async function doDeleteNote(noteId: string, noteTitle: string) { 20 | // delete in store 21 | store.getState().deleteNote(noteId); 22 | // delete backlinks 23 | await updateBacklinks(noteTitle, undefined); 24 | // delete in disk, 25 | await deleteFile(noteId); 26 | } 27 | -------------------------------------------------------------------------------- /src/editor/hooks/useExport.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { invoke } from '@tauri-apps/api'; 3 | import { jsPDF } from "jspdf"; 4 | import html2canvas from 'html2canvas'; 5 | 6 | export function ExportAs(as: string, filePath: string) { 7 | const pixelRatio = window.devicePixelRatio; 8 | const note = document.getElementById("note-content"); 9 | if (!note) return; 10 | 11 | html2canvas(note).then(async function (canvas) { 12 | const imgData = canvas.toDataURL("image/png"); 13 | requestAnimationFrame(() => { 14 | if (as === 'pdf') { 15 | handlePdf(imgData, canvas, pixelRatio, filePath); 16 | } else { 17 | handleImg(imgData, filePath); 18 | } 19 | }); 20 | await invoke('msg_dialog', { title: "Export", msg: filePath }); 21 | }); 22 | } 23 | 24 | async function handleImg(imgData: string, filePath: string) { 25 | const binaryData = atob(imgData.split("base64,")[1]); 26 | const data = []; 27 | for (let i = 0; i < binaryData.length; i++) { 28 | data.push(binaryData.charCodeAt(i)); 29 | } 30 | await invoke('download_file', { filePath, blob: data }); 31 | } 32 | 33 | async function handlePdf( 34 | imgData: string, 35 | canvas: HTMLCanvasElement, 36 | pixelRatio: number, 37 | filePath: string, 38 | ) { 39 | const orientation = canvas.width > canvas.height ? "l" : "p"; 40 | const pdf = new jsPDF(orientation, "pt", [ 41 | canvas.width / pixelRatio, 42 | canvas.height / pixelRatio, 43 | ]); 44 | const pdfWidth = pdf.internal.pageSize.getWidth(); 45 | const pdfHeight = pdf.internal.pageSize.getHeight(); 46 | pdf.addImage(imgData, "PNG", 0, 0, pdfWidth, pdfHeight, '', 'FAST'); 47 | 48 | const data = (pdf as any).__private__.getArrayBuffer( 49 | (pdf as any).__private__.buildDocument() 50 | ); 51 | await invoke('download_file', { filePath, blob: Array.from(new Uint8Array(data)) }); 52 | } 53 | -------------------------------------------------------------------------------- /src/editor/hooks/useHotkeys.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import isHotkey from 'is-hotkey'; 3 | 4 | /** 5 | mod+n: new note 6 | mod+p: new page(item) // reserve 7 | mod+s: save note // reserve 8 | Esc, esc quite 9 | ... 10 | **/ 11 | export default function useHotkeys( 12 | hotkeys: { hotkey: string; callback: () => void }[] 13 | ) { 14 | useEffect(() => { 15 | const handleKeyboardShortcuts = (event: KeyboardEvent) => { 16 | for (const { hotkey, callback } of hotkeys) { 17 | if (isHotkey(hotkey, event)) { 18 | event.preventDefault(); 19 | callback(); 20 | } 21 | } 22 | }; 23 | document.addEventListener('keydown', handleKeyboardShortcuts); 24 | return () => 25 | document.removeEventListener('keydown', handleKeyboardShortcuts); 26 | }, [hotkeys]); 27 | } 28 | -------------------------------------------------------------------------------- /src/editor/hooks/useOnNoteLinkClick.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useCurrentViewContext } from 'context/useCurrentView'; 3 | import { openFilePath } from 'file/open'; 4 | 5 | export default function useOnNoteLinkClick() { 6 | const currentView = useCurrentViewContext(); 7 | const dispatch = currentView.dispatch; 8 | 9 | const onClick = useCallback( 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | async (toId: string, highlightedPath?: any) => { 12 | const note = await openFilePath(toId, true); 13 | if (!note) return; 14 | const noteId = note.id; 15 | const hash = highlightedPath ? `0-${highlightedPath}` : ''; // TODO 16 | dispatch({view: 'md', params: {noteId, hash}}); 17 | return; 18 | }, 19 | [dispatch] 20 | ); 21 | 22 | return { onClick }; 23 | } 24 | -------------------------------------------------------------------------------- /src/editor/hooks/useTasks.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useEffect } from 'react'; 2 | import { parser, Task, getTasks } from "mdsmirror"; 3 | import { Notes, useStore } from 'lib/store'; 4 | import { loadDir } from 'file/open'; 5 | import { Note } from 'types/model'; 6 | 7 | type TaskWithID = { title: string} & Task; 8 | 9 | export type DocTask = { 10 | note: Note; 11 | tasks: TaskWithID[]; 12 | }; 13 | 14 | export default function useTasks() { 15 | const isLoaded = useStore((state) => state.isLoaded); 16 | const setIsLoaded = useStore((state) => state.setIsLoaded); 17 | const initDir = useStore((state) => state.initDir); 18 | // console.log("tk loaded?", isLoaded); 19 | useEffect(() => { 20 | if (!isLoaded && initDir) { 21 | loadDir(initDir).then(() => setIsLoaded(true)); 22 | } 23 | }, [initDir, isLoaded, setIsLoaded]); 24 | 25 | const notes = useStore((state) => state.notes); 26 | 27 | const docTasks = useMemo( 28 | () => computeTasks(notes), 29 | [notes] 30 | ); 31 | 32 | return docTasks; 33 | } 34 | 35 | export const computeTasks = (notes: Notes): DocTask[] => { 36 | const result: DocTask[] = []; 37 | const myNotes = Object.values(notes); 38 | for (const note of myNotes) { 39 | const tasks = computeNoteTasks(note.content); 40 | 41 | if (tasks.length > 0) { 42 | const newTasks: TaskWithID[] = tasks.map( 43 | t => {return {title: note.title, text: t.text, completed: t.completed}} 44 | ); 45 | result.push({ note, tasks: newTasks }); 46 | } 47 | } 48 | return result; 49 | }; 50 | 51 | const computeNoteTasks = (content: string) => { 52 | const doc = parser.parse(content); 53 | // console.log(">> doc: ", doc, content) 54 | const tasks = getTasks(doc); 55 | 56 | return tasks; 57 | }; 58 | -------------------------------------------------------------------------------- /src/file/process.ts: -------------------------------------------------------------------------------- 1 | import { NotesData } from 'lib/store'; 2 | import { regDateStr } from 'utils/helper'; 3 | import { Note, defaultNote } from 'types/model'; 4 | import { FileMetaData } from 'file/directory'; 5 | 6 | export function processJson(content: string): NotesData { 7 | try { 8 | const notesData: NotesData = JSON.parse(content); 9 | return notesData; 10 | } catch (e) { 11 | console.log('Please Check the JSON file: ', e); 12 | return {isloaded: false, notesobj: {}, notetree: {}}; 13 | } 14 | } 15 | 16 | /** 17 | * on Process Files: 18 | */ 19 | export function processFiles(fileList: FileMetaData[]) { 20 | const newNotesData: Note[] = []; 21 | const nonNotesData: Note[] = []; 22 | 23 | for (const file of fileList) { 24 | const fileName = file.file_name; 25 | 26 | if (!fileName || !file.is_file) { 27 | continue; 28 | } 29 | const fileContent = file.file_text; 30 | const filePath = file.file_path; 31 | 32 | const checkMd = checkFileIsMd(fileName); 33 | // new note from file 34 | const newNoteTitle = checkMd ? rmFileNameExt(fileName) : fileName; 35 | const lastModDate = new Date(file.last_modified.secs_since_epoch * 1000).toISOString(); 36 | const createdDate = new Date(file.created.secs_since_epoch * 1000).toISOString(); 37 | const isDaily = checkMd ? regDateStr.test(newNoteTitle) : false; 38 | const newNoteObj = { 39 | id: filePath, 40 | title: newNoteTitle, 41 | content: fileContent, 42 | created_at: createdDate, 43 | updated_at: lastModDate, 44 | is_daily: isDaily, 45 | file_path: filePath, 46 | }; 47 | const newProcessed = {...defaultNote, ...newNoteObj}; 48 | 49 | // push to Array 50 | checkMd ? newNotesData.push(newProcessed) : nonNotesData.push(newProcessed); 51 | } 52 | 53 | return [newNotesData, nonNotesData]; 54 | } 55 | 56 | export function processDirs(fileList: FileMetaData[]) { 57 | const newDirsData: Note[] = []; 58 | 59 | for (const file of fileList) { 60 | const fileName = file.file_name; 61 | 62 | if (!fileName || !file.is_dir ) { 63 | continue; 64 | } 65 | 66 | const filePath = file.file_path; 67 | const lastModDate = new Date(file.last_modified.secs_since_epoch * 1000).toISOString(); 68 | const createdDate = new Date(file.created.secs_since_epoch * 1000).toISOString(); 69 | const newDirObj = { 70 | id: filePath, 71 | title: fileName, 72 | created_at: createdDate, 73 | updated_at: lastModDate, 74 | file_path: filePath, 75 | is_dir: true, 76 | }; 77 | const newProcessedDir = {...defaultNote, ...newDirObj}; 78 | 79 | // push to Array 80 | newDirsData.push(newProcessedDir); 81 | } 82 | 83 | return newDirsData; 84 | } 85 | 86 | /* #endregion: import process */ 87 | 88 | /** 89 | * remove file name extension 90 | * 91 | * @param {string} fname, file name. 92 | */ 93 | export const rmFileNameExt = (fname: string) => { 94 | return fname.replace(/\.[^/.]+$/, ''); 95 | } 96 | 97 | export const getFileExt = (fname: string) => { 98 | return fname.slice((fname.lastIndexOf(".") - 1 >>> 0) + 2); 99 | } 100 | 101 | export const checkFileIsMd = (fname: string) => { 102 | const check = /\.(text|txt|md|mkdn|mdwn|mdown|markdown){1}$/i.test(fname); 103 | return check; 104 | } 105 | -------------------------------------------------------------------------------- /src/file/storage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { invoke } from '@tauri-apps/api/tauri' 3 | import { isTauri } from './util'; 4 | 5 | interface StorageData { 6 | status: boolean; 7 | data: JSON; 8 | } 9 | 10 | /** 11 | * Set data to local storage 12 | * @param {string} key 13 | * @param {any} value 14 | * @returns {Promise} 15 | */ 16 | export const set = async (key: string, value: any): Promise => { 17 | if (isTauri) { 18 | return await invoke('set_data', { key, value }); 19 | } else { 20 | localStorage.setItem(key, JSON.stringify(value)); 21 | } 22 | }; 23 | 24 | /** 25 | * Get data from local storage 26 | * @param {string} key 27 | * @returns {Promise} 28 | */ 29 | export const get = async (key: string): Promise => { 30 | if (isTauri) { 31 | const storeData: StorageData = await invoke('get_data', { key }); 32 | return storeData.status ? storeData.data : {}; 33 | } else { 34 | const storeData = localStorage.getItem(key); 35 | return storeData ? JSON.parse(storeData) : {}; 36 | } 37 | }; 38 | 39 | /** 40 | * Remove data in local storage 41 | * @param {string} key 42 | * @returns {any} 43 | */ 44 | export const remove = async (key: string): Promise => { 45 | if (isTauri) { 46 | await invoke('delete_data', { key }); 47 | } else { 48 | localStorage.removeItem(key); 49 | } 50 | }; 51 | 52 | // eslint-disable-next-line import/no-anonymous-default-export 53 | export default { set, get, remove }; 54 | 55 | 56 | // for Log 57 | // 58 | type LogItem = { 59 | ty: string; 60 | info: string; 61 | timestamp: string; // Date 62 | }; 63 | 64 | /** 65 | * Write a log 66 | * @param {string} ty - log type: info, error, warning, .. 67 | * @param {string} info - log information 68 | * @returns {Promise} 69 | */ 70 | export const setLog = async (ty: string, info: string): Promise => { 71 | const logData: LogItem[] = [{ ty, info, timestamp: new Date().toLocaleString() }]; 72 | return await invoke('set_log', { logData }); 73 | }; 74 | 75 | /** 76 | * Get the logs 77 | * @returns {Promise} 78 | */ 79 | export const getLog = async (): Promise => { 80 | return await invoke('get_log'); 81 | }; 82 | 83 | /** 84 | * clear the logs 85 | * @returns void 86 | */ 87 | export const clearLog = async (): Promise => { 88 | return await invoke('del_log'); 89 | }; 90 | -------------------------------------------------------------------------------- /src/file/write.ts: -------------------------------------------------------------------------------- 1 | // import { invoke } from '@tauri-apps/api'; 2 | import { Notes } from 'lib/store'; 3 | import { buildNotesJson, joinPaths } from './util'; 4 | import FileAPI from './files'; 5 | 6 | /** 7 | * Write file to disk 8 | * @param filePath 9 | * @param content 10 | */ 11 | export async function writeFile(filePath: string, content: string) { 12 | const file = new FileAPI(filePath); 13 | await file.writeFile(content); 14 | } 15 | 16 | /** 17 | * Write json containing all data to folder 18 | * @param parentDir 19 | * @param json optional 20 | */ 21 | export async function writeJsonFile(parentDir: string, json = '') { 22 | const jsonFile = new FileAPI('mdsilo.json', parentDir); 23 | const notesJson = json || buildNotesJson(); 24 | await jsonFile.writeFile(notesJson); 25 | // await invoke('save_notes', { dir: parentDir, content: notesJson }); 26 | } 27 | 28 | /** 29 | * Delete a file 30 | * @param filePath string 31 | */ 32 | export async function deleteFile(filePath: string) { 33 | const file = new FileAPI(filePath); 34 | await file.deleteFiles(); 35 | } 36 | 37 | /** 38 | * save all mds and json to a folder 39 | * @param dirPath string 40 | */ 41 | export async function writeAllFile(dirPath: string, notesObj: Notes) { 42 | // save mds 43 | const myNotes = Object.values(notesObj); 44 | for (const note of myNotes) { 45 | const fileName = `${note.title}.md`; 46 | const notePath = await joinPaths(dirPath, [fileName]); 47 | const content = note.content; 48 | await writeFile(notePath, content); 49 | } 50 | // save json with all notes, data 51 | const notesJson = buildNotesJson(); 52 | await writeJsonFile(dirPath, notesJson); 53 | } 54 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles/index.css'; 4 | import App from './components/App'; 5 | 6 | // eslint-disable-next-line import/no-named-as-default-member 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/styles/fonts/IBMPlexSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/IBMPlexSerif-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_AMS-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_AMS-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_AMS-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_AMS-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_AMS-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_AMS-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Caligraphic-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Caligraphic-Bold.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Caligraphic-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Caligraphic-Bold.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Caligraphic-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Caligraphic-Bold.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Caligraphic-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Caligraphic-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Caligraphic-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Caligraphic-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Caligraphic-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Caligraphic-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Fraktur-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Fraktur-Bold.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Fraktur-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Fraktur-Bold.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Fraktur-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Fraktur-Bold.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Fraktur-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Fraktur-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Fraktur-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Fraktur-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Fraktur-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Fraktur-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Bold.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Bold.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Bold.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-BoldItalic.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-BoldItalic.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-BoldItalic.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Italic.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Italic.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Italic.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Main-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Main-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Math-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Math-BoldItalic.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Math-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Math-BoldItalic.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Math-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Math-BoldItalic.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Math-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Math-Italic.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Math-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Math-Italic.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Math-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Math-Italic.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Bold.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Bold.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Bold.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Italic.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Italic.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Italic.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_SansSerif-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_SansSerif-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Script-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Script-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Script-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Script-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Script-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Script-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size1-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size1-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size1-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size1-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size1-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size1-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size2-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size2-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size2-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size2-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size2-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size2-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size3-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size3-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size3-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size3-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size3-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size3-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size4-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size4-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size4-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size4-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Size4-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Size4-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Typewriter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Typewriter-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Typewriter-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Typewriter-Regular.woff -------------------------------------------------------------------------------- /src/styles/fonts/KaTeX_Typewriter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/KaTeX_Typewriter-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /src/styles/fonts/Sniglet-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdSilo/mdSilo-app/a18ceddca4bbedfd7fd42d863bc1af99e2ceb74e/src/styles/fonts/Sniglet-Regular.ttf -------------------------------------------------------------------------------- /src/styles/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | h1 { 7 | font-size: 2.5em; 8 | } 9 | h2 { 10 | font-size: 2em; 11 | } 12 | h3 { 13 | font-size: 1.6em; 14 | } 15 | h4 { 16 | font-size: 1.2em; 17 | } 18 | } 19 | 20 | @layer components { 21 | .btn { 22 | @apply px-5 py-3 font-medium text-white transition duration-200 ease-in-out rounded shadow bg-primary-600 hover:bg-opacity-75; 23 | } 24 | 25 | .pop-btn { 26 | @apply px-2 py-2 text-sm text-black rounded shadow bg-green-200 hover:bg-green-100; 27 | } 28 | 29 | .m-btn0 { 30 | @apply px-2 py-1 text-xs text-black rounded shadow bg-green-200 hover:bg-green-100; 31 | } 32 | 33 | .m-btn1 { 34 | @apply px-2 py-1 text-xs text-black rounded shadow bg-blue-200 hover:bg-blue-100; 35 | } 36 | 37 | .m-btn2 { 38 | @apply px-2 py-1 text-xs text-black rounded shadow bg-orange-200 hover:bg-orange-100; 39 | } 40 | 41 | .card { 42 | @apply max-w-md p-8 overflow-hidden bg-white rounded shadow-md; 43 | } 44 | 45 | .input { 46 | @apply block py-1 text-gray-500 border-gray-300 rounded shadow-sm focus:ring-yellow-300 focus:border-primary-50; 47 | } 48 | 49 | .link { 50 | @apply cursor-pointer text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300; 51 | } 52 | .splash-bg { 53 | background: linear-gradient(55deg, #414b61 0 45%, #2d3343 45% 100%); 54 | } 55 | .img-effect { 56 | --m: 57 | radial-gradient(circle farthest-side at right,#000 99%,#0000) 0 100%/46% 92% no-repeat, 58 | radial-gradient(circle farthest-side at left,#000 99%,#0000) 100% 0/46% 92% no-repeat; 59 | -webkit-mask: var(--m); 60 | mask: var(--m); 61 | } 62 | 63 | .content { 64 | line-height: 1.8; 65 | } 66 | .content a { 67 | color: green; 68 | } 69 | .content a:hover { 70 | text-decoration: underline; 71 | } 72 | .content h1, .content h2, .content h3, .content h4, .content h5 { 73 | padding: 12px 0; 74 | line-height: 1.2em; 75 | } 76 | .content pre { 77 | white-space: break-spaces; 78 | } 79 | .content img, .content video { 80 | display: block; 81 | max-width: 100% !important; 82 | height: auto; 83 | margin: 10px auto; 84 | } 85 | .content blockquote { 86 | font-weight: 400; 87 | margin: 2rem 0; 88 | padding-left: 1rem; 89 | border-left: 6px solid greenyellow; 90 | } 91 | .content p { 92 | line-height: 1.6; 93 | font-weight: 400; 94 | font-size: 18px; 95 | margin: 1.2rem 0; 96 | } 97 | .content table, .content th, .content td { 98 | border: 1px solid; 99 | padding: 6px; 100 | } 101 | .content hr { 102 | margin: 2.4rem 0; 103 | } 104 | .content figcaption { 105 | text-align: center; 106 | color: #f4f4f5; 107 | } 108 | } 109 | 110 | :root { 111 | /* This is used for the spinner and progress bar */ 112 | --color-primary-500: #00bfd8; 113 | } 114 | 115 | @media (pointer: fine) { 116 | ::-webkit-scrollbar { 117 | width: 10px; 118 | height: 10px; 119 | } 120 | 121 | /* Track */ 122 | ::-webkit-scrollbar-track { 123 | @apply bg-gray-100 dark:bg-gray-700; 124 | } 125 | 126 | /* Handle */ 127 | ::-webkit-scrollbar-thumb { 128 | @apply bg-gray-300 dark:bg-gray-500; 129 | } 130 | 131 | /* Handle on hover */ 132 | ::-webkit-scrollbar-thumb:hover { 133 | @apply bg-gray-400 dark:bg-gray-400; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/types/model.ts: -------------------------------------------------------------------------------- 1 | // READING 2 | // 3 | export interface ChannelType { 4 | id: number; 5 | title: string; 6 | link: string; 7 | description?: string; 8 | published?: string; // iso date string 9 | ty: string; // podcast | rss 10 | unread: number; 11 | } 12 | 13 | export interface ArticleType { 14 | id: number; 15 | title: string; 16 | url: string; 17 | feed_link: string; 18 | audio_url: string; 19 | description: string; 20 | published?: Date; 21 | read_status: number; 22 | star_status: number; 23 | content?: string; 24 | author?: string; 25 | image?: string; 26 | source?: string; 27 | links?: string[]; 28 | ttr?: number; 29 | } 30 | 31 | export interface PodType { 32 | title: string; 33 | url: string; 34 | published?: Date; 35 | article_url: string; 36 | feed_link: string; 37 | } 38 | 39 | // WRITING 40 | // 41 | export type Note = { 42 | id: string; // !!Important!! id === file_path 43 | title: string; 44 | content: string; 45 | file_path: string; 46 | cover: string | null; 47 | created_at: string; 48 | updated_at: string; 49 | is_daily: boolean; 50 | is_dir?: boolean; 51 | }; 52 | 53 | export const defaultNote = { 54 | title: 'untitled', 55 | content: ' ', 56 | file_path: '', 57 | cover: '', 58 | created_at: new Date().toISOString(), 59 | updated_at: new Date().toISOString(), 60 | is_daily: false, 61 | }; 62 | -------------------------------------------------------------------------------- /src/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type PickPartial = Pick> & 2 | Partial>; 3 | -------------------------------------------------------------------------------- /src/utils/file-extensions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) Arthur Verschaeve 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | */ 24 | 25 | export const imageExtensions = [ 26 | 'ase', 27 | 'art', 28 | 'bmp', 29 | 'blp', 30 | 'cd5', 31 | 'cit', 32 | 'cpt', 33 | 'cr2', 34 | 'cut', 35 | 'dds', 36 | 'dib', 37 | 'djvu', 38 | 'egt', 39 | 'exif', 40 | 'gif', 41 | 'gpl', 42 | 'grf', 43 | 'icns', 44 | 'ico', 45 | 'iff', 46 | 'jng', 47 | 'jpeg', 48 | 'jpg', 49 | 'jfif', 50 | 'jp2', 51 | 'jps', 52 | 'lbm', 53 | 'max', 54 | 'miff', 55 | 'mng', 56 | 'msp', 57 | 'nitf', 58 | 'ota', 59 | 'pbm', 60 | 'pc1', 61 | 'pc2', 62 | 'pc3', 63 | 'pcf', 64 | 'pcx', 65 | 'pdn', 66 | 'pgm', 67 | 'PI1', 68 | 'PI2', 69 | 'PI3', 70 | 'pict', 71 | 'pct', 72 | 'pnm', 73 | 'pns', 74 | 'ppm', 75 | 'psb', 76 | 'psd', 77 | 'pdd', 78 | 'psp', 79 | 'px', 80 | 'pxm', 81 | 'pxr', 82 | 'qfx', 83 | 'raw', 84 | 'rle', 85 | 'sct', 86 | 'sgi', 87 | 'rgb', 88 | 'int', 89 | 'bw', 90 | 'tga', 91 | 'tiff', 92 | 'tif', 93 | 'vtf', 94 | 'xbm', 95 | 'xcf', 96 | 'xpm', 97 | '3dv', 98 | 'amf', 99 | 'ai', 100 | 'awg', 101 | 'cgm', 102 | 'cdr', 103 | 'cmx', 104 | 'dxf', 105 | 'e2d', 106 | 'egt', 107 | 'eps', 108 | 'fs', 109 | 'gbr', 110 | 'odg', 111 | 'svg', 112 | 'stl', 113 | 'vrml', 114 | 'x3d', 115 | 'sxd', 116 | 'v2d', 117 | 'vnd', 118 | 'wmf', 119 | 'emf', 120 | 'art', 121 | 'xar', 122 | 'png', 123 | 'webp', 124 | 'jxr', 125 | 'hdp', 126 | 'wdp', 127 | 'cur', 128 | 'ecw', 129 | 'iff', 130 | 'lbm', 131 | 'liff', 132 | 'nrrd', 133 | 'pam', 134 | 'pcx', 135 | 'pgf', 136 | 'sgi', 137 | 'rgb', 138 | 'rgba', 139 | 'bw', 140 | 'int', 141 | 'inta', 142 | 'sid', 143 | 'ras', 144 | 'sun', 145 | ]; 146 | 147 | export const docExtensions = [ 148 | 'pdf', 149 | 'doc', 150 | 'docx', 151 | 'xls', 152 | 'xlsx', 153 | 'ppt', 154 | 'pptx', 155 | 'odt', 156 | 'ods', 157 | 'odp', 158 | 'xps', 159 | 'pages', 160 | 'numbers', 161 | 'key', 162 | 'zip', 163 | ]; 164 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const colors = require('tailwindcss/colors'); 3 | const { gray } = require('tailwindcss/colors'); 4 | 5 | module.exports = { 6 | mode: 'jit', 7 | content: ['./src/**/*.{html,js,ts,jsx,tsx}', './public/**/*.{html,js,ts,jsx,tsx}'], 8 | darkMode: 'class', 9 | plugins: [ 10 | require('@tailwindcss/typography'), 11 | require('@tailwindcss/forms'), 12 | ], 13 | theme: { 14 | container: { 15 | center: true, 16 | screens: { 17 | sm: '640px', 18 | md: '768px', 19 | lg: '1024px', 20 | xl: '1280px', 21 | '2xl': '1280px', 22 | }, 23 | }, 24 | extend: { 25 | spacing: { 26 | 0.25: '0.0625rem', 27 | 128: '32rem', 28 | 160: '40rem', 29 | 176: '44rem', 30 | 192: '48rem', 31 | 240: '60rem', 32 | 'screen-10': '10vh', 33 | 'screen-80': '80vh', 34 | }, 35 | colors: { 36 | primary: colors.emerald, 37 | gray: colors.neutral, 38 | orange: colors.orange, 39 | }, 40 | boxShadow: { 41 | popover: 42 | 'rgb(15 15 15 / 10%) 0px 3px 6px, rgb(15 15 15 / 20%) 0px 9px 24px', 43 | }, 44 | opacity: { 45 | 0.1: '0.001', 46 | 85: '.85', 47 | }, 48 | zIndex: { 49 | '-10': '-10', 50 | }, 51 | cursor: { 52 | alias: 'alias', 53 | }, 54 | animation: { 55 | 'bounce-x': 'bounce-x 1s infinite', 56 | }, 57 | keyframes: { 58 | 'bounce-x': { 59 | '0%, 100%': { 60 | transform: 'translateX(0)', 61 | animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)', 62 | }, 63 | '50%': { 64 | transform: 'translateX(25%)', 65 | animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)', 66 | }, 67 | }, 68 | }, 69 | typography: { 70 | DEFAULT: { 71 | css: { 72 | b: { 73 | fontWeight: 600, 74 | }, 75 | h1: { 76 | fontWeight: 600, 77 | color: gray, 78 | }, 79 | h2: { 80 | fontWeight: 600, 81 | color: gray, 82 | }, 83 | h3: { 84 | fontWeight: 600, 85 | color: gray, 86 | }, 87 | h4: { 88 | fontWeight: 600, 89 | }, 90 | h5: { 91 | fontWeight: 600, 92 | }, 93 | h6: { 94 | fontWeight: 600, 95 | }, 96 | a: { 97 | textDecoration: 'none', 98 | fontWeight: 'normal', 99 | '&:hover': { 100 | color: colors.emerald[500], 101 | }, 102 | }, 103 | blockquote: { 104 | color: colors.emerald[100], 105 | } 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | 22 | "downlevelIteration": true, 23 | "incremental": true 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx" 29 | , "src/components/sidebar/SidebarTagstsx" ], 30 | "exclude": [ 31 | "node_modules", 32 | "**/*.spec.ts" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------