├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .postcssrc ├── LICENSE.md ├── README.md ├── icon.png ├── package-lock.json ├── package.json ├── screenshots ├── demo.gif ├── inline-random.gif ├── renderer.gif ├── sample-highlights.png ├── sample-tweet.png ├── sample-tweetthread.png ├── settings.png └── sync.png ├── src ├── App.css ├── App.tsx ├── components │ ├── Basics.tsx │ ├── Customise.tsx │ ├── ProgressBar.tsx │ ├── RandomHighlight.tsx │ └── Sync.tsx ├── index.html ├── index.tsx ├── randomHighlight-old.tsx ├── services │ ├── bookView.ts │ ├── checkOrgOrMarkdown.ts │ ├── handleClosePopup.ts │ ├── pluginBar.tsx │ ├── randomHighlightsUtilities.ts │ └── utilities.ts └── tailwind.css └── tailwind.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-vendored -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [hkgnp] 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: logseq-readwise-plugin 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '16.x' # You might need to adjust this value to your own version 22 | - name: Build 23 | id: build 24 | run: | 25 | npm i && npm run build 26 | mkdir ${{ env.PLUGIN_NAME }} 27 | cp README.md package.json icon.png ${{ env.PLUGIN_NAME }} 28 | mv dist ${{ env.PLUGIN_NAME }} 29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 30 | ls 31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 32 | - name: Create Release 33 | uses: ncipollo/release-action@v1 34 | id: create_release 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | VERSION: ${{ github.ref }} 38 | with: 39 | allowUpdates: true 40 | draft: false 41 | prerelease: false 42 | 43 | - name: Upload zip file 44 | id: upload_zip 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} 50 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 51 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 52 | asset_content_type: application/zip 53 | 54 | - name: Upload package.json 55 | id: upload_metadata 56 | uses: actions/upload-release-asset@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | upload_url: ${{ steps.create_release.outputs.upload_url }} 61 | asset_path: ./package.json 62 | asset_name: package.json 63 | asset_content_type: application/json 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | misc 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | .env.production 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | .parcel-cache 83 | 84 | # Next.js build output 85 | .next 86 | out 87 | 88 | # Nuxt.js build / generate output 89 | .nuxt 90 | dist 91 | 92 | # Gatsby files 93 | .cache/ 94 | # Comment in the public line in if your project uses Gatsby and not Next.js 95 | # https://nextjs.org/blog/next-9-1#public-directory-support 96 | # public 97 | 98 | # vuepress build output 99 | .vuepress/dist 100 | 101 | # Serverless directories 102 | .serverless/ 103 | 104 | # FuseBox cache 105 | .fusebox/ 106 | 107 | # DynamoDB Local files 108 | .dynamodb/ 109 | 110 | # TernJS port file 111 | .tern-port 112 | 113 | # Stores VSCode versions used for testing VSCode extensions 114 | .vscode-test 115 | 116 | # yarn v2 117 | .yarn/cache 118 | .yarn/unplugged 119 | .yarn/build-state.yml 120 | .yarn/install-state.gz 121 | .pnp.* -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "tailwindcss": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 hkgnp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [:gift_heart: Sponsor this project on Github](https://github.com/sponsors/hkgnp) or [:coffee: Get me a coffee](https://www.buymeacoffee.com/hkgnp.dev) if you like this plugin! 2 | 3 | > README below is outdated since the major change on 14/2/2022. Pending a rewrite. 4 | 5 | # Overview 6 | 7 | This is a simple Readwise plugin to: 8 | 9 | 1. Pull in all your highlights from Readwise 10 | 2. For subsequent pulls, it only pulls in those not in the graph 11 | 12 | There is now a [FAQ](https://github.com/hkgnp/logseq-readwise-plugin#detailed-instructions) that you may find helpful to read before you use this plugin. 13 | 14 | # Samples 15 | 16 | ### Sample of a book highlight 17 | 18 | ![](/screenshots/sample-highlights.png) 19 | 20 | ### Sample of a tweet highlight 21 | 22 | ![](/screenshots/sample-tweet.png) 23 | 24 | ### Sample of a tweet thread 25 | 26 | ![](/screenshots/sample-tweetthread.png) 27 | 28 | # Random Highlights 29 | 30 | ![](/screenshots/demo.gif) 31 | 32 | ![](/screenshots/inline-random.gif) 33 | 34 | # Book View 35 | 36 | If you would like to view all your imported books as cards, you can use the Book Renderer function. Simply go to any block and trigger it by typing `/Book Renderer`. 37 | 38 | ![](/screenshots/renderer.gif) 39 | 40 | # Disclaimer 41 | 42 | If you have multiple sources (e.g. books, tweets, instapaper) and thousands of highlights, the initial pull can take a while. You will have a progress bar to keep track on what's happening, and can terminate the pull process at any time. 43 | 44 | Each source will have its own page in Logseq. If there has been an error, just remove the necessary pages and refresh your graph. Assuming that you do not have any filenames containing `(Readwise)`, you can use the following command in MacOS to remove all the pages added by the plugin. Be sure to refresh your graph in Logseq before attempting any new synchronisation. 45 | 46 | `find . -name "*(Readwise)*" -delete` 47 | 48 | New highlights are found by comparing the date of the highlight against the date of the latest highlight in your last synchronisation. When using the plugin for the first time, the initial date is set to `1970-01-01T00:00:00Z`. 49 | 50 | # Usage 51 | 52 | ### Migrating from manual loading to marketplace 53 | 54 | **BEFORE YOU INSTALL FROM THE MARKETPLACE**, please follow the instructions below to avoid synchronising duplicate highlights: 55 | 56 | 1. Go to the settings folder of the manually loaded plugin (Windows: `C:\Users\Peter\.logseq\settings` or MacOS: `~/.logseq/settings). 57 | 2. Open the file `logseq-readwise-plugin.json` and copy the contents of the file somewhere. 58 | 3. Uninstall the manually loaded plugin. 59 | 4. Install the plugin from the marketplace. 60 | 5. Click on the settings icon in the plugins page and click `Open settings`. 61 | ![](/screenshots/settings.png). 62 | 6. Copy the contents in Step 2 and paste it in the file that opens up. Save and close the file. 63 | 7. Restart Logseq. 64 | 8. You can start to use the plugin after! 65 | 66 | ### First time (from the marketplace - preferred) 67 | 68 | 1. Go to your [Readwise Access Token](https://readwise.io/access_token) page and obtain a new token. Keep this token somewhere safe. 69 | 2. Download the logseq-readwise-plugin from the Logseq marketplace. 70 | 3. Click on the icon (📖) in the plugins bar. 71 | 4. If you are using the plugin for the first time, do remember to click the button `Click here if you are using this plugin for the first time`. 72 | 5. Key in the token that you obtained in (1) and click `Save Token`. 73 | 6. Review the number of sources and highlights that you have. 74 | 7. Click button to sync highlights. 75 | 76 | ![](/screenshots/sync.png) 77 | 78 | ### Subsequent times 79 | 80 | 1. Click refresh to retrieve the recent number of changes. 81 | 2. Click button to sync highlights. 82 | 83 | # Detailed Instructions 84 | 85 | ## How do I reset the export of my whole library, e.g. to start afresh? 86 | 87 | As this plugin is still new, you may encounter situations where you want to reset your export. Firstly, thank you so much for trying this plugin out and reporting the bugs that I've missed! Secondly, you can reset by one of 2 approaches: 88 | 89 | **Please backup your graph before attempting any of the below** 90 | 91 | 1. Using a script to find all files ending with `(Readwise)` and deleting them. Naturally, this \*\*assumes that you do not have any other pages whose name includes `(Readwise)` if not they will be deleted as well. After deleting the files, please restart Logseq and refresh your graph. A MacOS script example would be: 92 | 93 | `find . -name "*(Readwise)*" -delete` 94 | 95 | 2. Using File Explorer (Windows) or Finder (MacOS) to find these files and delete them manually. Files created by this plugin have `(Readwise)` added to the end of their filenames. 96 | 97 | ## What happens when I take new highlights? Do they sync automatically with Logseq? 98 | 99 | Not really. When you open Logseq and click on the plugin button (📖), you will see the new number of sources that you took highlights from since your last sync. If you would like to sync those sources, you can proceed to click on the `Sync New Sources` button. 100 | 101 | ## What is a source? 102 | 103 | A source is basically a book, a twitter account, etc that contains highlights. 104 | 105 | ## Why are new pages being created? 106 | 107 | If the new highlight(s) is from a new source, a new page in Logseq will be created. If it is from an _existing_ source, the highlight(s) will be appended to the top of your current highlights list, right under the `Readwise Highlights` block. 108 | 109 | Certain services such as Amazon Kindle, Instapaper do not provide automatic synchronisation with Readwise. In these cases, you can either wait for it to appear as a new source in the plugin, or proceed to your Readwise dashboard to manually sync them, and then initiate the sync from the plugin button (📖). 110 | 111 | ## How do I trigger a new sync from Logseq? 112 | 113 | By default, the plugin will automatically sync when you open the Logseq app and look for new highlights. You can then click the `Sync New Sources` button to initiate the sync. 114 | 115 | Without clicking `Sync New Sources` button, the plugin will not automatically create pages or pull highlights in for you. 116 | 117 | ## What happens when I update highlights in Readwise? Will those changes automatically sync with Logseq (or vice versa)? 118 | 119 | Unfortunately not. It will be technically challenging to look for that specific highlight within a page, and make changes to it without the possibility to accidentally removing edits made by the user. 120 | 121 | ## Can I rename the page in Logseq? 122 | 123 | Unfortunately not as well. The plugin uses the original name given to the source to find and add subsequent highlights. If you rename it, when there are new highlights, a new page will be created for the source (with the old name). Hence, you should only rename when you do not expect any more highlights from that source, e.g. a book that you know you will not make any more highlights for it. 124 | 125 | ## Can I edit the page that the plugin created for a source? 126 | 127 | Yes and no. As long as you **do not** change any of the below, you may edit the page, e.g. add in your own thoughts as child blocks under the highlights. 128 | 129 | - Rename the block `[[Readwise Highlights]]`. 130 | - Convert the block `[[Readwise Highlights]]` to a child block. 131 | 132 | The above is because `[[Readwise Highlights]]` is used when syncing new highlights to that source. If you have done any of the above, a new block called `[[Readwise Highlights]]` will be created and the highlights added under it. You will then need to clean it up after. 133 | 134 | ## Where does the Location link in each Kindle highlight take me? 135 | 136 | If you have the Kindle app installed on your desktop, you will be brought directly to the highlight in the Kindle app when you click on the link. 137 | 138 | ## What do I do if I have other Feature Requests to suggest or bugs to report? 139 | 140 | Feel free to look for me on Discord, or just opening an issue in this repository. 141 | 142 | Thanks for trying out the plugin! 143 | 144 | _Adapted from [Readwise's help article for Obsidian](https://help.readwise.io/article/125-how-does-the-readwise-to-obsidian-export-integration-work)_ 145 | 146 | # Future 147 | 148 | - [x] Change style of popup. 149 | - [x] Fix issue of pulling new highlights removing old blocks from existing pages. 150 | - [x] Fix source of non-Kindle highlights. 151 | - [x] Account for cases that have more than 1000 cases. 152 | - [ ] Refactor code. 153 | - [ ] Will be incorporating the possibility of only pulling highlights for specific sources. 154 | 155 | # Credits 156 | 157 | Big thanks to [@MattHulse](https://github.com/MattHulse) for helping to contributing to the code (duplicate source with same name, but has no highlights)! 158 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logseq-readwise-plugin", 3 | "version": "4.0.17", 4 | "author": "hkgnp", 5 | "description": "Unofficial Readwise plugin to pull in all your highlights from Readwise and track new ones!", 6 | "logseq": { 7 | "id": "logseq-readwise-plugin", 8 | "title": "logseq-readwise-plugin", 9 | "icon": "./icon.png" 10 | }, 11 | "main": "dist/index.html", 12 | "targets": { 13 | "main": false 14 | }, 15 | "scripts": { 16 | "test": "echo \"Error: no test specified\" && exit 1", 17 | "dev": "parcel src/index.html", 18 | "build": "postcss src/tailwind.css -o src/App.css && parcel build src/index.html --public-url ./" 19 | }, 20 | "license": "MIT", 21 | "dependencies": { 22 | "@logseq/libs": "^0.0.6", 23 | "axios": "^0.24.0", 24 | "logseq-dateutils": "^0.0.21", 25 | "postcss": "^8.4.13", 26 | "postcss-cli": "^9.1.0", 27 | "postcss-import": "^14.1.0", 28 | "react": "^17.0.2", 29 | "react-dom": "^17.0.2", 30 | "tailwindcss": "^3.0.24" 31 | }, 32 | "devDependencies": { 33 | "@types/react": "17.0.34", 34 | "@types/react-dom": "17.0.11", 35 | "parcel": "^2.5.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /screenshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/demo.gif -------------------------------------------------------------------------------- /screenshots/inline-random.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/inline-random.gif -------------------------------------------------------------------------------- /screenshots/renderer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/renderer.gif -------------------------------------------------------------------------------- /screenshots/sample-highlights.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/sample-highlights.png -------------------------------------------------------------------------------- /screenshots/sample-tweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/sample-tweet.png -------------------------------------------------------------------------------- /screenshots/sample-tweetthread.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/sample-tweetthread.png -------------------------------------------------------------------------------- /screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/settings.png -------------------------------------------------------------------------------- /screenshots/sync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benjypng/logseq-readwise-plugin/643ce111fc90ed68df2ab6ea0aa026108f290f7e/screenshots/sync.png -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.0.24 | MIT License | https://tailwindcss.com 3 | *//* 4 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 5 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 6 | */ 7 | 8 | *, 9 | ::before, 10 | ::after { 11 | box-sizing: border-box; /* 1 */ 12 | border-width: 0; /* 2 */ 13 | border-style: solid; /* 2 */ 14 | border-color: #e5e7eb; /* 2 */ 15 | } 16 | 17 | ::before, 18 | ::after { 19 | --tw-content: ''; 20 | } 21 | 22 | /* 23 | 1. Use a consistent sensible line-height in all browsers. 24 | 2. Prevent adjustments of font size after orientation changes in iOS. 25 | 3. Use a more readable tab size. 26 | 4. Use the user's configured `sans` font-family by default. 27 | */ 28 | 29 | html { 30 | line-height: 1.5; /* 1 */ 31 | -webkit-text-size-adjust: 100%; /* 2 */ 32 | -moz-tab-size: 4; /* 3 */ 33 | tab-size: 4; /* 3 */ 34 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ 35 | } 36 | 37 | /* 38 | 1. Remove the margin in all browsers. 39 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 40 | */ 41 | 42 | body { 43 | margin: 0; /* 1 */ 44 | line-height: inherit; /* 2 */ 45 | } 46 | 47 | /* 48 | 1. Add the correct height in Firefox. 49 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 50 | 3. Ensure horizontal rules are visible by default. 51 | */ 52 | 53 | hr { 54 | height: 0; /* 1 */ 55 | color: inherit; /* 2 */ 56 | border-top-width: 1px; /* 3 */ 57 | } 58 | 59 | /* 60 | Add the correct text decoration in Chrome, Edge, and Safari. 61 | */ 62 | 63 | abbr:where([title]) { 64 | text-decoration: underline dotted; 65 | } 66 | 67 | /* 68 | Remove the default font size and weight for headings. 69 | */ 70 | 71 | h1, 72 | h2, 73 | h3, 74 | h4, 75 | h5, 76 | h6 { 77 | font-size: inherit; 78 | font-weight: inherit; 79 | } 80 | 81 | /* 82 | Reset links to optimize for opt-in styling instead of opt-out. 83 | */ 84 | 85 | a { 86 | color: inherit; 87 | text-decoration: inherit; 88 | } 89 | 90 | /* 91 | Add the correct font weight in Edge and Safari. 92 | */ 93 | 94 | b, 95 | strong { 96 | font-weight: bolder; 97 | } 98 | 99 | /* 100 | 1. Use the user's configured `mono` font family by default. 101 | 2. Correct the odd `em` font sizing in all browsers. 102 | */ 103 | 104 | code, 105 | kbd, 106 | samp, 107 | pre { 108 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /* 113 | Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /* 121 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 122 | */ 123 | 124 | sub, 125 | sup { 126 | font-size: 75%; 127 | line-height: 0; 128 | position: relative; 129 | vertical-align: baseline; 130 | } 131 | 132 | sub { 133 | bottom: -0.25em; 134 | } 135 | 136 | sup { 137 | top: -0.5em; 138 | } 139 | 140 | /* 141 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 142 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 143 | 3. Remove gaps between table borders by default. 144 | */ 145 | 146 | table { 147 | text-indent: 0; /* 1 */ 148 | border-color: inherit; /* 2 */ 149 | border-collapse: collapse; /* 3 */ 150 | } 151 | 152 | /* 153 | 1. Change the font styles in all browsers. 154 | 2. Remove the margin in Firefox and Safari. 155 | 3. Remove default padding in all browsers. 156 | */ 157 | 158 | button, 159 | input, 160 | optgroup, 161 | select, 162 | textarea { 163 | font-family: inherit; /* 1 */ 164 | font-size: 100%; /* 1 */ 165 | line-height: inherit; /* 1 */ 166 | color: inherit; /* 1 */ 167 | margin: 0; /* 2 */ 168 | padding: 0; /* 3 */ 169 | } 170 | 171 | /* 172 | Remove the inheritance of text transform in Edge and Firefox. 173 | */ 174 | 175 | button, 176 | select { 177 | text-transform: none; 178 | } 179 | 180 | /* 181 | 1. Correct the inability to style clickable types in iOS and Safari. 182 | 2. Remove default button styles. 183 | */ 184 | 185 | button, 186 | [type='button'], 187 | [type='reset'], 188 | [type='submit'] { 189 | -webkit-appearance: button; /* 1 */ 190 | background-color: transparent; /* 2 */ 191 | background-image: none; /* 2 */ 192 | } 193 | 194 | /* 195 | Use the modern Firefox focus style for all focusable elements. 196 | */ 197 | 198 | :-moz-focusring { 199 | outline: auto; 200 | } 201 | 202 | /* 203 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 204 | */ 205 | 206 | :-moz-ui-invalid { 207 | box-shadow: none; 208 | } 209 | 210 | /* 211 | Add the correct vertical alignment in Chrome and Firefox. 212 | */ 213 | 214 | progress { 215 | vertical-align: baseline; 216 | } 217 | 218 | /* 219 | Correct the cursor style of increment and decrement buttons in Safari. 220 | */ 221 | 222 | ::-webkit-inner-spin-button, 223 | ::-webkit-outer-spin-button { 224 | height: auto; 225 | } 226 | 227 | /* 228 | 1. Correct the odd appearance in Chrome and Safari. 229 | 2. Correct the outline style in Safari. 230 | */ 231 | 232 | [type='search'] { 233 | -webkit-appearance: textfield; /* 1 */ 234 | outline-offset: -2px; /* 2 */ 235 | } 236 | 237 | /* 238 | Remove the inner padding in Chrome and Safari on macOS. 239 | */ 240 | 241 | ::-webkit-search-decoration { 242 | -webkit-appearance: none; 243 | } 244 | 245 | /* 246 | 1. Correct the inability to style clickable types in iOS and Safari. 247 | 2. Change font properties to `inherit` in Safari. 248 | */ 249 | 250 | ::-webkit-file-upload-button { 251 | -webkit-appearance: button; /* 1 */ 252 | font: inherit; /* 2 */ 253 | } 254 | 255 | /* 256 | Add the correct display in Chrome and Safari. 257 | */ 258 | 259 | summary { 260 | display: list-item; 261 | } 262 | 263 | /* 264 | Removes the default spacing and border for appropriate elements. 265 | */ 266 | 267 | blockquote, 268 | dl, 269 | dd, 270 | h1, 271 | h2, 272 | h3, 273 | h4, 274 | h5, 275 | h6, 276 | hr, 277 | figure, 278 | p, 279 | pre { 280 | margin: 0; 281 | } 282 | 283 | fieldset { 284 | margin: 0; 285 | padding: 0; 286 | } 287 | 288 | legend { 289 | padding: 0; 290 | } 291 | 292 | ol, 293 | ul, 294 | menu { 295 | list-style: none; 296 | margin: 0; 297 | padding: 0; 298 | } 299 | 300 | /* 301 | Prevent resizing textareas horizontally by default. 302 | */ 303 | 304 | textarea { 305 | resize: vertical; 306 | } 307 | 308 | /* 309 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 310 | 2. Set the default placeholder color to the user's configured gray 400 color. 311 | */ 312 | 313 | input::placeholder, 314 | textarea::placeholder { 315 | opacity: 1; /* 1 */ 316 | color: #9ca3af; /* 2 */ 317 | } 318 | 319 | /* 320 | Set the default cursor for buttons. 321 | */ 322 | 323 | button, 324 | [role="button"] { 325 | cursor: pointer; 326 | } 327 | 328 | /* 329 | Make sure disabled buttons don't get the pointer cursor. 330 | */ 331 | :disabled { 332 | cursor: default; 333 | } 334 | 335 | /* 336 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 337 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 338 | This can trigger a poorly considered lint error in some tools but is included by design. 339 | */ 340 | 341 | img, 342 | svg, 343 | video, 344 | canvas, 345 | audio, 346 | iframe, 347 | embed, 348 | object { 349 | display: block; /* 1 */ 350 | vertical-align: middle; /* 2 */ 351 | } 352 | 353 | /* 354 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 355 | */ 356 | 357 | img, 358 | video { 359 | max-width: 100%; 360 | height: auto; 361 | } 362 | 363 | /* 364 | Ensure the default browser behavior of the `hidden` attribute. 365 | */ 366 | 367 | [hidden] { 368 | display: none; 369 | } 370 | 371 | *, ::before, ::after { 372 | --tw-translate-x: 0; 373 | --tw-translate-y: 0; 374 | --tw-rotate: 0; 375 | --tw-skew-x: 0; 376 | --tw-skew-y: 0; 377 | --tw-scale-x: 1; 378 | --tw-scale-y: 1; 379 | --tw-pan-x: ; 380 | --tw-pan-y: ; 381 | --tw-pinch-zoom: ; 382 | --tw-scroll-snap-strictness: proximity; 383 | --tw-ordinal: ; 384 | --tw-slashed-zero: ; 385 | --tw-numeric-figure: ; 386 | --tw-numeric-spacing: ; 387 | --tw-numeric-fraction: ; 388 | --tw-ring-inset: ; 389 | --tw-ring-offset-width: 0px; 390 | --tw-ring-offset-color: #fff; 391 | --tw-ring-color: rgb(59 130 246 / 0.5); 392 | --tw-ring-offset-shadow: 0 0 #0000; 393 | --tw-ring-shadow: 0 0 #0000; 394 | --tw-shadow: 0 0 #0000; 395 | --tw-shadow-colored: 0 0 #0000; 396 | --tw-blur: ; 397 | --tw-brightness: ; 398 | --tw-contrast: ; 399 | --tw-grayscale: ; 400 | --tw-hue-rotate: ; 401 | --tw-invert: ; 402 | --tw-saturate: ; 403 | --tw-sepia: ; 404 | --tw-drop-shadow: ; 405 | --tw-backdrop-blur: ; 406 | --tw-backdrop-brightness: ; 407 | --tw-backdrop-contrast: ; 408 | --tw-backdrop-grayscale: ; 409 | --tw-backdrop-hue-rotate: ; 410 | --tw-backdrop-invert: ; 411 | --tw-backdrop-opacity: ; 412 | --tw-backdrop-saturate: ; 413 | --tw-backdrop-sepia: ; 414 | } 415 | .container { 416 | width: 100%; 417 | } 418 | @media (min-width: 640px) { 419 | 420 | .container { 421 | max-width: 640px; 422 | } 423 | } 424 | @media (min-width: 768px) { 425 | 426 | .container { 427 | max-width: 768px; 428 | } 429 | } 430 | @media (min-width: 1024px) { 431 | 432 | .container { 433 | max-width: 1024px; 434 | } 435 | } 436 | @media (min-width: 1280px) { 437 | 438 | .container { 439 | max-width: 1280px; 440 | } 441 | } 442 | @media (min-width: 1536px) { 443 | 444 | .container { 445 | max-width: 1536px; 446 | } 447 | } 448 | .absolute { 449 | position: absolute; 450 | } 451 | .relative { 452 | position: relative; 453 | } 454 | .top-3 { 455 | top: 0.75rem; 456 | } 457 | .z-50 { 458 | z-index: 50; 459 | } 460 | .my-2 { 461 | margin-top: 0.5rem; 462 | margin-bottom: 0.5rem; 463 | } 464 | .-mt-8 { 465 | margin-top: -2rem; 466 | } 467 | .mr-3 { 468 | margin-right: 0.75rem; 469 | } 470 | .mr-2 { 471 | margin-right: 0.5rem; 472 | } 473 | .mb-3 { 474 | margin-bottom: 0.75rem; 475 | } 476 | .mb-5 { 477 | margin-bottom: 1.25rem; 478 | } 479 | .mt-3 { 480 | margin-top: 0.75rem; 481 | } 482 | .block { 483 | display: block; 484 | } 485 | .inline-block { 486 | display: inline-block; 487 | } 488 | .flex { 489 | display: flex; 490 | } 491 | .h-2 { 492 | height: 0.5rem; 493 | } 494 | .h-full { 495 | height: 100%; 496 | } 497 | .w-2\/3 { 498 | width: 66.666667%; 499 | } 500 | .w-full { 501 | width: 100%; 502 | } 503 | .w-2\/6 { 504 | width: 33.333333%; 505 | } 506 | .flex-auto { 507 | flex: 1 1 auto; 508 | } 509 | .transform { 510 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 511 | } 512 | .cursor-pointer { 513 | cursor: pointer; 514 | } 515 | .flex-row { 516 | flex-direction: row; 517 | } 518 | .flex-col { 519 | flex-direction: column; 520 | } 521 | .flex-wrap { 522 | flex-wrap: wrap; 523 | } 524 | .content-center { 525 | align-content: center; 526 | } 527 | .items-center { 528 | align-items: center; 529 | } 530 | .justify-center { 531 | justify-content: center; 532 | } 533 | .justify-between { 534 | justify-content: space-between; 535 | } 536 | .rounded-lg { 537 | border-radius: 0.5rem; 538 | } 539 | .rounded-full { 540 | border-radius: 9999px; 541 | } 542 | .rounded { 543 | border-radius: 0.25rem; 544 | } 545 | .border { 546 | border-width: 1px; 547 | } 548 | .border-gray-200 { 549 | --tw-border-opacity: 1; 550 | border-color: rgb(229 231 235 / var(--tw-border-opacity)); 551 | } 552 | .border-black { 553 | --tw-border-opacity: 1; 554 | border-color: rgb(0 0 0 / var(--tw-border-opacity)); 555 | } 556 | .bg-white { 557 | --tw-bg-opacity: 1; 558 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 559 | } 560 | .bg-blue-500 { 561 | --tw-bg-opacity: 1; 562 | background-color: rgb(59 130 246 / var(--tw-bg-opacity)); 563 | } 564 | .bg-gray-300 { 565 | --tw-bg-opacity: 1; 566 | background-color: rgb(209 213 219 / var(--tw-bg-opacity)); 567 | } 568 | .bg-yellow-300 { 569 | --tw-bg-opacity: 1; 570 | background-color: rgb(253 224 71 / var(--tw-bg-opacity)); 571 | } 572 | .bg-green-600 { 573 | --tw-bg-opacity: 1; 574 | background-color: rgb(22 163 74 / var(--tw-bg-opacity)); 575 | } 576 | .bg-red-500 { 577 | --tw-bg-opacity: 1; 578 | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); 579 | } 580 | .fill-current { 581 | fill: currentColor; 582 | } 583 | .p-3 { 584 | padding: 0.75rem; 585 | } 586 | .p-2 { 587 | padding: 0.5rem; 588 | } 589 | .py-2 { 590 | padding-top: 0.5rem; 591 | padding-bottom: 0.5rem; 592 | } 593 | .px-3 { 594 | padding-left: 0.75rem; 595 | padding-right: 0.75rem; 596 | } 597 | .py-1 { 598 | padding-top: 0.25rem; 599 | padding-bottom: 0.25rem; 600 | } 601 | .px-2 { 602 | padding-left: 0.5rem; 603 | padding-right: 0.5rem; 604 | } 605 | .pl-3 { 606 | padding-left: 0.75rem; 607 | } 608 | .pt-2 { 609 | padding-top: 0.5rem; 610 | } 611 | .pt-1 { 612 | padding-top: 0.25rem; 613 | } 614 | .text-left { 615 | text-align: left; 616 | } 617 | .text-center { 618 | text-align: center; 619 | } 620 | .text-right { 621 | text-align: right; 622 | } 623 | .align-middle { 624 | vertical-align: middle; 625 | } 626 | .font-serif { 627 | font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif; 628 | } 629 | .text-2xl { 630 | font-size: 1.5rem; 631 | line-height: 2rem; 632 | } 633 | .text-xs { 634 | font-size: 0.75rem; 635 | line-height: 1rem; 636 | } 637 | .text-lg { 638 | font-size: 1.125rem; 639 | line-height: 1.75rem; 640 | } 641 | .text-sm { 642 | font-size: 0.875rem; 643 | line-height: 1.25rem; 644 | } 645 | .font-bold { 646 | font-weight: 700; 647 | } 648 | .font-semibold { 649 | font-weight: 600; 650 | } 651 | .uppercase { 652 | text-transform: uppercase; 653 | } 654 | .text-black { 655 | --tw-text-opacity: 1; 656 | color: rgb(0 0 0 / var(--tw-text-opacity)); 657 | } 658 | .text-blue-500 { 659 | --tw-text-opacity: 1; 660 | color: rgb(59 130 246 / var(--tw-text-opacity)); 661 | } 662 | .text-white { 663 | --tw-text-opacity: 1; 664 | color: rgb(255 255 255 / var(--tw-text-opacity)); 665 | } 666 | .text-red-400 { 667 | --tw-text-opacity: 1; 668 | color: rgb(248 113 113 / var(--tw-text-opacity)); 669 | } 670 | .filter { 671 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); 672 | } 673 | .transition { 674 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 675 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 676 | transition-duration: 150ms; 677 | } 678 | .hover\:border-blue-500:hover { 679 | --tw-border-opacity: 1; 680 | border-color: rgb(59 130 246 / var(--tw-border-opacity)); 681 | } 682 | .focus\:border-b-4:focus { 683 | border-bottom-width: 4px; 684 | } 685 | .focus\:border-blue-500:focus { 686 | --tw-border-opacity: 1; 687 | border-color: rgb(59 130 246 / var(--tw-border-opacity)); 688 | } 689 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | getTotalNumberOfHighlightsAndBooks, 4 | loadFromReadwise, 5 | } from "./services/utilities"; 6 | import Basics from "./components/Basics"; 7 | import Customise from "./components/Customise"; 8 | import Sync from "./components/Sync"; 9 | import RandomHighlight from "./components/RandomHighlight"; 10 | 11 | interface PluginSettings { 12 | pageSize: number; 13 | noOfBooks: number; 14 | noOfHighlights: number; 15 | noOfNewSources: number; 16 | bookList: any[]; 17 | sync: boolean; 18 | loaded: boolean; 19 | isRefreshing: boolean; 20 | errorLoading: boolean; 21 | } 22 | 23 | const App = () => { 24 | const { token } = logseq.settings; 25 | 26 | const [pluginSettings, setPluginSettings] = useState({ 27 | pageSize: 1000, 28 | noOfBooks: 0, 29 | noOfHighlights: 0, 30 | noOfNewSources: 0, 31 | bookList: [], 32 | sync: false, 33 | loaded: false, 34 | isRefreshing: false, 35 | errorLoading: false, 36 | }); 37 | 38 | const { 39 | pageSize, 40 | noOfBooks, 41 | noOfHighlights, 42 | noOfNewSources, 43 | bookList, 44 | sync, 45 | loaded, 46 | } = pluginSettings; 47 | 48 | useEffect(() => { 49 | // Get total number of highlights to show on dashboard 50 | getTotalNumberOfHighlightsAndBooks(token, setPluginSettings); 51 | 52 | // Load latest books 53 | loadFromReadwise(token, pageSize, setPluginSettings); 54 | }, []); 55 | 56 | const terminate = () => { 57 | window.location.reload(); 58 | logseq.hideMainUI(); 59 | }; 60 | 61 | return ( 62 |
63 |
67 | {/* BASICS START */} 68 | 75 | {/* BASICS END */} 76 |
77 | {/* CUSTOMISE START */} 78 | 79 | {/* CUSTOMISE END */} 80 |
81 | {/* SYNC START */} 82 | 90 | {/* SYNC END */} 91 | 92 | {/* RANDOM HIGHLIGHT START */} 93 | 94 | {/* RANDOM HIGHLIGHT END */} 95 |
96 |
97 | ); 98 | }; 99 | 100 | export default App; 101 | -------------------------------------------------------------------------------- /src/components/Basics.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { getDateForPageWithoutBrackets } from 'logseq-dateutils'; 3 | import { 4 | getTotalNumberOfHighlightsAndBooks, 5 | loadFromReadwise, 6 | } from '../services/utilities'; 7 | 8 | interface LogseqSettings { 9 | token: string; 10 | } 11 | 12 | const Basics = (props: { 13 | noOfBooks: number; 14 | noOfHighlights: number; 15 | noOfNewSources: number; 16 | pageSize: number; 17 | setPluginSettings: Function; 18 | }) => { 19 | const { 20 | noOfBooks, 21 | noOfHighlights, 22 | noOfNewSources, 23 | pageSize, 24 | setPluginSettings, 25 | } = props; 26 | 27 | const [logseqSettings, setLogseqSettings] = useState({ 28 | token: logseq.settings.token, 29 | }); 30 | 31 | const { preferredDateFormat, latestRetrieved } = logseq.settings; 32 | 33 | const handleLogseqSettingsInput = (e: any) => { 34 | setLogseqSettings((currSettings) => ({ 35 | ...currSettings, 36 | [e.target.name]: e.target.value, 37 | })); 38 | }; 39 | 40 | const saveToken = () => { 41 | logseq.updateSettings({ token: logseqSettings.token }); 42 | logseq.App.showMsg('Token saved!', 'success'); 43 | 44 | window.setTimeout(() => { 45 | getTotalNumberOfHighlightsAndBooks( 46 | logseq.settings.token, 47 | setPluginSettings 48 | ); 49 | loadFromReadwise(logseq.settings.token, pageSize, setPluginSettings); 50 | }, 2000); 51 | }; 52 | 53 | const hide = () => { 54 | logseq.hideMainUI(); 55 | }; 56 | 57 | const refresh = () => { 58 | getTotalNumberOfHighlightsAndBooks( 59 | logseq.settings.token, 60 | setPluginSettings 61 | ); 62 | loadFromReadwise(logseq.settings.token, pageSize, setPluginSettings); 63 | }; 64 | 65 | return ( 66 |
67 |
68 |

Step 1: The Basics

69 | 80 |
81 | {!latestRetrieved.startsWith('1970') && ( 82 |

83 | Last Retrieved:{' '} 84 | {getDateForPageWithoutBrackets( 85 | new Date(latestRetrieved), 86 | preferredDateFormat 87 | )}{' '} 88 | @ {new Date(latestRetrieved).getHours()}: 89 | {new Date(latestRetrieved).getMinutes()}{' '} 90 |

91 | )} 92 | 93 |
94 | 102 | 105 |
106 | {/* Sources and highlights row */} 107 |
108 |
109 |
110 | 111 | {noOfBooks} 112 | 113 | 114 | Total number of sources 115 | 116 |
117 |
118 | 119 |
120 |
121 | 122 | {noOfHighlights} 123 | 124 | 125 | Total number of highlights 126 | 127 |
128 |
129 | 130 |
131 |
132 | 133 | {noOfNewSources} 134 | 135 | 136 | Number of new sources to sync 137 | 138 | 141 |
142 |
143 |
144 | {/* End information row */} 145 |
146 | ); 147 | }; 148 | 149 | export default Basics; 150 | -------------------------------------------------------------------------------- /src/components/Customise.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const Customise = () => { 4 | const [template, setTemplate] = useState({ 5 | customTitle: logseq.settings.template?.customTitle, 6 | metaData: logseq.settings.template?.metaData, 7 | height: logseq.settings.template?.height, 8 | width: logseq.settings.template?.width, 9 | sectionHeader: logseq.settings.template?.sectionHeader, 10 | }); 11 | 12 | const [hiding, setHiding] = useState(logseq.settings.hiding); 13 | 14 | const toggleHiding = () => { 15 | if (hiding) { 16 | setHiding(false); 17 | logseq.updateSettings({ hiding: false }); 18 | } else if (!hiding) { 19 | setHiding(true); 20 | logseq.updateSettings({ hiding: true }); 21 | } 22 | }; 23 | 24 | const handleInput = (e: any) => { 25 | setTemplate((currTemplate) => ({ 26 | ...currTemplate, 27 | [e.target.name]: e.target.value, 28 | })); 29 | }; 30 | 31 | const { customTitle, metaData, height, width, sectionHeader } = template; 32 | 33 | const submitTemplate = () => { 34 | if (customTitle === '' || sectionHeader === '') { 35 | logseq.App.showMsg( 36 | 'Please ensure all customisation fields are completed!', 37 | 'error' 38 | ); 39 | return; 40 | } 41 | logseq.updateSettings({ template: template }); 42 | logseq.App.showMsg('Customisation saved!'); 43 | }; 44 | 45 | return ( 46 |
47 |
48 |

Step 2: Start Customising

49 | {!hiding && ( 50 |

54 | hide 55 |

56 | )} 57 | {hiding && ( 58 |

62 | show 63 |

64 | )} 65 |
66 | 67 | {!hiding && ( 68 | 69 | {/* TITLE */} 70 |
71 |

Title

72 |

73 | Use %title% (e.g. To Kill a Mockingbird), %category% (e.g. books), 74 | %source% (e.g. kindle) or %author% (e.g. Harper Lee) to indicate 75 | the title, category, source or author in the name of the 76 | highlights page and add your preferred prefixes or suffixes. 77 |

78 | 87 |
88 | 89 | {/* PAGE PROPERTIES */} 90 |
91 |

Additional Metadata

92 |

93 | In addition to the core properties (retrieved date, author, 94 | category, source, tags), you can indicate other meta properties 95 | that will go to the top of the page. Acceps both markdown and 96 | org-mode formats. For org, use the format :key: value to define 97 | the metadata. 98 |

99 | 107 |
108 | 109 | {/* IMAGE SIZE */} 110 |
111 |

Image Size

112 |

Set the height and width of the book/source image in pixels.

113 |
114 | Height: 115 | 123 | Width: 124 | 132 |
133 |
134 | 135 | {/* READWISE HIGHLIGHTS TITLE */} 136 |
137 |

Title of Highlights Section

138 |

139 | Set the title of the highlights section. Accepts both markdown and 140 | org-mode formats. Note: In order to use the Random Highlights 141 | function, please surround your title with '[[ ]]'. 142 |

143 | 152 |
153 | 154 |
155 | 161 |
162 |
163 | )} 164 |
165 | ); 166 | }; 167 | 168 | export default Customise; 169 | -------------------------------------------------------------------------------- /src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ProgressBar = ({ progressPercentage }) => { 4 | return ( 5 | 6 |

{progressPercentage.toFixed(2)}%

7 |
8 |
14 |
15 |
16 | ); 17 | }; 18 | 19 | export default ProgressBar; 20 | -------------------------------------------------------------------------------- /src/components/RandomHighlight.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { scrollToRandomHighlight } from '../services/randomHighlightsUtilities'; 3 | 4 | const RandomHighlight = () => { 5 | const gotoRandomHighlight = () => { 6 | scrollToRandomHighlight(); 7 | }; 8 | 9 | return ( 10 |
11 |
12 |

Optional:

13 | 19 |
20 |
21 | ); 22 | }; 23 | 24 | export default RandomHighlight; 25 | -------------------------------------------------------------------------------- /src/components/Sync.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { getHighlightsForBook, sleep } from "../services/utilities"; 3 | import { 4 | returnKindleHighlights, 5 | returnOtherHighlights, 6 | returnPageMetaData, 7 | returnImage, 8 | } from "../services/checkOrgOrMarkdown"; 9 | import ProgressBar from "./ProgressBar"; 10 | import { BlockEntity } from "@logseq/libs/dist/LSPlugin.user"; 11 | 12 | const Sync = (props: { 13 | loaded: boolean; 14 | sync: boolean; 15 | terminate: Function; 16 | bookList: any[]; 17 | token: string; 18 | setPluginSettings: Function; 19 | }) => { 20 | const { sync, terminate, bookList, token, setPluginSettings } = props; 21 | 22 | const [progressPercentage, setProgressPercentage] = useState(0); 23 | const [coolingOff, setCoolingOff] = useState(false); 24 | 25 | const { customTitle, metaData, height, width, sectionHeader } = 26 | logseq.settings.template; 27 | 28 | const { preferredDateFormat, orgOrMarkdown } = logseq.settings; 29 | 30 | const getHighlightsForEachBook = async () => { 31 | if (customTitle === "" || sectionHeader === "") { 32 | logseq.App.showMsg("Your template is not set up yet!", "error"); 33 | return; 34 | } else if (bookList.length === 0) { 35 | logseq.App.showMsg("There are no new sources to sync!", "success"); 36 | return; 37 | } 38 | 39 | // Change sync status 40 | setPluginSettings((currSettings) => ({ ...currSettings, sync: true })); 41 | 42 | // Handle progress bar 43 | // If there are 200 books, each interval will be 0.5 44 | const interval: number = parseFloat((100 / bookList.length).toFixed(2)); 45 | 46 | for (const b of bookList) { 47 | setProgressPercentage( 48 | (progressPercentage) => progressPercentage + interval 49 | ); 50 | 51 | // Get highlights for book 52 | const bookHighlights = await getHighlightsForBook( 53 | b.id, 54 | token, 55 | setCoolingOff 56 | ); 57 | 58 | sleep(2000); 59 | 60 | // Filter only the latest highlights 61 | const latestHighlights = bookHighlights.data.results.filter( 62 | (b) => new Date(b.updated) > new Date(logseq.settings.latestRetrieved) 63 | ); 64 | 65 | if (latestHighlights.length === 0) { 66 | console.log(`No highlights found for '${b.title}'`); 67 | continue; 68 | } 69 | 70 | // Prepare latest highlights for logeq insertion 71 | let latestHighlightsArr: any[] = []; 72 | if (b.source === "kindle") { 73 | for (const h of latestHighlights) { 74 | latestHighlightsArr.push( 75 | returnKindleHighlights( 76 | orgOrMarkdown, 77 | h.location, 78 | b.asin, 79 | h.highlighted_at, 80 | preferredDateFormat, 81 | h.tags, 82 | h.text 83 | ) 84 | ); 85 | } 86 | } else { 87 | for (const h of latestHighlights) { 88 | latestHighlightsArr.push( 89 | returnOtherHighlights( 90 | orgOrMarkdown, 91 | h.url, 92 | h.highlighted_at, 93 | preferredDateFormat, 94 | h.tags, 95 | h.text, 96 | height, 97 | width 98 | ) 99 | ); 100 | } 101 | } 102 | 103 | // remove speccial characters 104 | const bookTitle = b.title 105 | .replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, "") 106 | .replace( 107 | /([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, 108 | "" 109 | ) 110 | .replace(/\s+/g, " ") 111 | .trim(); 112 | 113 | console.log(`Updating ${bookTitle}`); 114 | 115 | const pageName = customTitle 116 | .replace("%title%", bookTitle) 117 | .replace("%author%", b.author) 118 | .replace("%category%", b.category) 119 | .replace("%source%", b.source); 120 | 121 | await logseq.Editor.createPage(pageName); 122 | 123 | // Set Title 124 | logseq.App.pushState("page", { 125 | name: customTitle 126 | .replace("%title%", bookTitle) 127 | .replace("%author%", b.author) 128 | .replace("%category%", b.category) 129 | .replace("%source%", b.source), 130 | }); 131 | 132 | sleep(1000); 133 | 134 | const currPage = await logseq.Editor.getCurrentPage(); 135 | 136 | // Check if page is empty. If empty, create the basic template. If not empty, update only the Readwise Highlights section 137 | const pageBlocksTree = await logseq.Editor.getCurrentPageBlocksTree(); 138 | 139 | // Create new page 140 | if (pageBlocksTree.length === 0 || pageBlocksTree[0].content === "") { 141 | // Set metaData 142 | await logseq.Editor.insertBlock( 143 | currPage.name, 144 | returnPageMetaData( 145 | orgOrMarkdown, 146 | preferredDateFormat, 147 | b.author, 148 | b.category, 149 | b.source, 150 | b.tags, 151 | metaData 152 | ), 153 | { isPageBlock: true } 154 | ); 155 | 156 | // Set image 157 | await logseq.Editor.insertBlock( 158 | currPage.name, 159 | returnImage(orgOrMarkdown, b.cover_image_url, height, width), 160 | { sibling: true, isPageBlock: true } 161 | ); 162 | 163 | // Set Section Header 164 | const highlightsBlock = await logseq.Editor.insertBlock( 165 | currPage.name, 166 | `${sectionHeader}`, 167 | { sibling: true, isPageBlock: true } 168 | ); 169 | 170 | if (logseq.settings.sortRecentFirst) { 171 | await logseq.Editor.insertBatchBlock( 172 | highlightsBlock.uuid, 173 | latestHighlightsArr, 174 | { 175 | sibling: false, 176 | } 177 | ); 178 | } else { 179 | await logseq.Editor.insertBatchBlock( 180 | highlightsBlock.uuid, 181 | latestHighlightsArr.reverse(), 182 | { 183 | sibling: false, 184 | } 185 | ); 186 | } 187 | if (pageBlocksTree[0].content === "") { 188 | await logseq.Editor.removeBlock(pageBlocksTree[0].uuid); 189 | } 190 | } else { 191 | // Add to highlights section 192 | const highlightsBlock = pageBlocksTree.filter( 193 | (b) => b.content === sectionHeader 194 | ); 195 | 196 | // Check if section header is on the page. If not, create it. 197 | if (!highlightsBlock[0] || highlightsBlock[0].children.length === 0) { 198 | console.log( 199 | `${b.title} had changes made to its [[Readwise Highlights]] block.` 200 | ); 201 | const highlightsBlock = await logseq.Editor.insertBlock( 202 | currPage.name, 203 | sectionHeader, 204 | { 205 | isPageBlock: true, 206 | } 207 | ); 208 | 209 | await logseq.Editor.insertBatchBlock( 210 | highlightsBlock.uuid, 211 | latestHighlightsArr.reverse(), 212 | { 213 | sibling: false, 214 | before: false, 215 | } 216 | ); 217 | } 218 | 219 | const lastBlk = highlightsBlock[0].children[ 220 | highlightsBlock[0].children.length - 1 221 | ] as BlockEntity; 222 | 223 | await logseq.Editor.insertBatchBlock( 224 | lastBlk.uuid, 225 | latestHighlightsArr.reverse(), 226 | { 227 | sibling: true, 228 | before: false, 229 | } 230 | ); 231 | } 232 | } 233 | 234 | // Reset bootkList 235 | await setPluginSettings((currSettings) => ({ 236 | ...currSettings, 237 | sync: false, 238 | noOfNewSources: 0, 239 | })); 240 | 241 | logseq.App.showMsg( 242 | "Highlights imported! If you made any changes to the [[Readwise Highlights]] block before you synced, you may need to revist those pages to remove duplicate higlights. Please refer to the console in Developer Tools for these pages.", 243 | "success" 244 | ); 245 | 246 | // Reset progress bar 247 | setProgressPercentage(0); 248 | 249 | // Update settings with latest retrieved date 250 | logseq.updateSettings({ 251 | latestRetrieved: bookList[0].last_highlight_at, 252 | }); 253 | }; 254 | 255 | return ( 256 |
257 |

Step 3: Sync!

258 |

259 | Syncing more than 20 sources will take a longer time because of 260 | Readwise's API limits. 261 |

262 |
263 | {/* Only show when setState has completed. */} 264 | {!sync && ( 265 | 272 | )} 273 | 274 | {/* Only show when Syncing */} 275 | {sync && ( 276 | 284 | )} 285 | 286 | {/* Only show when cooling off */} 287 | {coolingOff && ( 288 |

289 | Please wait for Readwise's cooling off period to lapse. 290 |

291 | )} 292 | 293 | {/* Start progress bar */} 294 |
295 | 296 |
297 | {/* End progress bar */} 298 |
299 |
300 | ); 301 | }; 302 | 303 | export default Sync; 304 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | logseq-readwise-tmp-plugin 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "@logseq/libs"; 2 | import "./App.css"; 3 | import { bookView } from "./services/bookView"; 4 | import { pluginBar } from "./services/pluginBar"; 5 | import { getRandomHighlight } from "./services/randomHighlightsUtilities"; 6 | 7 | const main = async () => { 8 | console.log("Readwise plugin loaded"); 9 | 10 | if (!logseq.settings.template) { 11 | logseq.updateSettings({ 12 | template: { 13 | customTitle: "%title% (Readwise)", 14 | metaData: "", 15 | height: "200", 16 | width: "200", 17 | sectionHeader: "## [[Readwise Highlights]]", 18 | }, 19 | }); 20 | } 21 | 22 | if (!logseq.settings.latestRetrieved) { 23 | logseq.updateSettings({ 24 | latestRetrieved: "1970-01-01T00:00:00Z", 25 | }); 26 | } 27 | 28 | if (!logseq.settings.token) { 29 | logseq.updateSettings({ 30 | token: "12345", 31 | }); 32 | } 33 | 34 | // Set preferred date format 35 | window.setTimeout(async () => { 36 | const userConfigs = await logseq.App.getUserConfigs(); 37 | 38 | const preferredDateFormat: string = userConfigs.preferredDateFormat; 39 | const orgOrMarkdown: string = userConfigs.preferredFormat; 40 | 41 | logseq.updateSettings({ 42 | preferredDateFormat: preferredDateFormat, 43 | orgOrMarkdown: orgOrMarkdown, 44 | }); 45 | 46 | console.log( 47 | `Settings updated to ${preferredDateFormat} and ${orgOrMarkdown}}` 48 | ); 49 | 50 | // PLUGIN BAR ICON 51 | pluginBar(); 52 | }, 3000); 53 | 54 | // RANDOM HIGHLIGHT 55 | const randomHighlight = async () => { 56 | const hl = await getRandomHighlight(); 57 | const page = await logseq.Editor.getPage(hl.pageId); 58 | 59 | if (hl.content === logseq.settings.template.sectionHeader) { 60 | await randomHighlight(); 61 | } else { 62 | return { content: hl.content, pageName: page.originalName }; 63 | } 64 | }; 65 | 66 | logseq.Editor.registerSlashCommand("Random highlight", async () => { 67 | const highlight = await randomHighlight(); 68 | 69 | await logseq.Editor.insertAtEditingCursor( 70 | `> **${highlight.pageName}** 71 | ${highlight.content}` 72 | ); 73 | }); 74 | 75 | // BOOK RENDERER 76 | const uniqueIdentifier = () => 77 | Math.random() 78 | .toString(36) 79 | .replace(/[^a-z]+/g, ""); 80 | 81 | logseq.Editor.registerSlashCommand("Book renderer", async () => { 82 | await logseq.Editor.insertAtEditingCursor( 83 | `{{renderer :bookRenderer_${uniqueIdentifier()}}}` 84 | ); 85 | }); 86 | 87 | bookView(); 88 | }; 89 | 90 | logseq.ready(main).catch(console.error); 91 | -------------------------------------------------------------------------------- /src/randomHighlight-old.tsx: -------------------------------------------------------------------------------- 1 | const getRandomHighlight = async () => { 2 | const getAllReferencesFromPages = await logseq.DB 3 | .datascriptQuery(`[:find ?page ?ref-page-name 4 | :where 5 | [?p :block/journal? false] 6 | [?p :block/name ?page] 7 | [?block :block/page ?p] 8 | [?block :block/refs ?ref-page] 9 | [?ref-page :block/name ?ref-page-name]])]`); 10 | 11 | // Filter out arrays where they mention readwise highlights or where they mention kindle 12 | const tweetSources = getAllReferencesFromPages.filter( 13 | (a) => a[1] == 'readwise highlights' 14 | ); 15 | 16 | const kindleSources = getAllReferencesFromPages.filter( 17 | (a) => a[1] == 'kindle' 18 | ); 19 | 20 | const allSources = kindleSources.concat(tweetSources); 21 | 22 | const getRandomNumber = (min, max) => { 23 | return Math.floor(Math.random() * (max - min + 1)) + min; 24 | }; 25 | 26 | // Find page that randomSource is pointing to 27 | const randomSourcePage = await logseq.DB.datascriptQuery(`[:find ?block-name 28 | :where 29 | [?block-name :block/name "${ 30 | allSources[getRandomNumber(0, allSources.length - 1)][0] 31 | }"]]`); 32 | 33 | // Get logseq page based on above 34 | const page = await logseq.Editor.getPage(randomSourcePage[0][0], { 35 | includeChildren: true, 36 | }); 37 | 38 | // Go to page 39 | logseq.App.pushState('page', { name: page.name }); 40 | 41 | // Get page blocks tree to find highlights block "## Readwise Highlights" 42 | const pbt = await logseq.Editor.getCurrentPageBlocksTree(); 43 | 44 | // Find highlights block 45 | const highlightsBlock = pbt.filter( 46 | (a) => a.content === '## [[Readwise Highlights]]' 47 | ); 48 | 49 | const randomHighlight = getRandomNumber( 50 | 0, 51 | highlightsBlock[0].children.length - 1 52 | ); 53 | 54 | // Scroll to random highlight 55 | await logseq.Editor.scrollToBlockInPage( 56 | page.name, 57 | highlightsBlock[0].children[randomHighlight]['uuid'] 58 | ); 59 | 60 | logseq.hideMainUI(); 61 | }; 62 | 63 | export default getRandomHighlight; 64 | -------------------------------------------------------------------------------- /src/services/bookView.ts: -------------------------------------------------------------------------------- 1 | export const bookView = () => { 2 | logseq.App.onMacroRendererSlotted(async ({ slot, payload }) => { 3 | const uuid = payload.uuid; 4 | const [type] = payload.arguments; 5 | if (!type.startsWith(":bookRenderer_")) return; 6 | 7 | const id = type.split("_")[1]?.trim(); 8 | const bookRendererId = `bookRenderer_${id}`; 9 | 10 | // Get list of books using query 11 | let bookPropertyList = await logseq.DB.datascriptQuery(`[:find (pull ?p [*]) 12 | :where 13 | [?p :block/name _] 14 | [?p :block/properties ?prop] 15 | [(get ?prop :category) ?t] 16 | [(contains? ?t "books")] 17 | ]`); 18 | 19 | bookPropertyList = bookPropertyList.map((i: any) => ({ 20 | originalName: i[0]["original-name"], 21 | properties: i[0]["properties"], 22 | pageUUID: i[0]["uuid"]["$uuid$"], 23 | pageName: i[0]["name"], 24 | })); 25 | 26 | for (const b of bookPropertyList) { 27 | const pbt = await logseq.Editor.getPageBlocksTree(b.pageName); 28 | const imageUrl = pbt[1].content; 29 | 30 | const regExp = /\((.*?)\)/; 31 | const matched = regExp.exec(imageUrl); 32 | 33 | b["imageUrl"] = matched[1]; 34 | } 35 | 36 | // Function to go to Block 37 | const goTo = async (x: string) => { 38 | logseq.App.pushState("page", { name: x }); 39 | }; 40 | 41 | // Create model for each section so as to enable events 42 | let models = {}; 43 | for (let m = 0; m < bookPropertyList.length; m++) { 44 | models["goToPage" + m] = function () { 45 | goTo(bookPropertyList[m].pageName); 46 | }; 47 | } 48 | logseq.provideModel(models); 49 | 50 | let html: string = ""; 51 | for (let i = 0; i < bookPropertyList.length; i++) { 52 | let { author, source } = bookPropertyList[i].properties; 53 | author = author[0]; 54 | source = source[0]; 55 | let { imageUrl } = bookPropertyList[i]; 56 | if (imageUrl.includes("_SL200_.") || imageUrl.includes("_SY160.")) { 57 | imageUrl = imageUrl.replace("_SL200_.", ""); 58 | imageUrl = imageUrl.replace("_SY160.", ""); 59 | } 60 | 61 | html += `
62 |
63 |

${source.substring( 64 | 2, 65 | source.length - 2 66 | )}

67 |
68 |
69 |

${ 70 | bookPropertyList[i].originalName 71 | } 72 |

${author}

73 |
74 |
`; 75 | } 76 | 77 | const bookBoard = (board: string) => { 78 | return `
${board}
`; 79 | }; 80 | 81 | logseq.provideStyle(` 82 | .container { 83 | display: flex; 84 | justify-content: center; 85 | padding: 10px 0; 86 | flex-direction: row; 87 | flex-wrap: wrap; 88 | gap: 40px; 89 | background-color: #eee; 90 | border-radius: 8px; 91 | } 92 | 93 | .card { 94 | display: flex; 95 | align-items: top; 96 | flex-direction: column; 97 | max-width: 200px; 98 | box-sizing: border-box; 99 | border-radius: 8px; 100 | background-color: #fff; 101 | box-shadow: -5px 5px 10px #aaaaaa; 102 | } 103 | 104 | .card:hover { 105 | box-shadow: -5px 10px 15px #aaaaaa; 106 | transform: translateY(-8px); 107 | transition: all 0.1s; 108 | } 109 | 110 | .card:hover { 111 | cursor: pointer; 112 | } 113 | 114 | .image { 115 | height: 250px; 116 | width: 200px; 117 | background-repeat: no-repeat; 118 | background-size: 100% 100%; 119 | 120 | display: flex; 121 | justify-content: right; 122 | align-items: top; 123 | } 124 | 125 | .desc { 126 | word-wrap: break-word; 127 | display: flex; 128 | flex-direction: column; 129 | align-items: top; 130 | justify-content: left; 131 | padding: 3px 5px; 132 | } 133 | 134 | .source { 135 | margin: 0; 136 | padding: 0 0 0 4px; 137 | background-color: navy; 138 | height: 23px; 139 | color: white; 140 | border-radius: 0 0 0 12px; 141 | font-size: 70%; 142 | } 143 | 144 | .originalName { 145 | font-weight: 700; 146 | } 147 | 148 | .author { 149 | font-size: 80%; 150 | } 151 | `); 152 | 153 | logseq.provideUI({ 154 | key: `${bookRendererId}`, 155 | slot, 156 | reset: true, 157 | template: bookBoard(html), 158 | }); 159 | }); 160 | }; 161 | -------------------------------------------------------------------------------- /src/services/checkOrgOrMarkdown.ts: -------------------------------------------------------------------------------- 1 | import { getDateForPage } from 'logseq-dateutils'; 2 | 3 | const prepareTags = (t: any[]) => { 4 | const tagArr = t.map((t) => `[[${t.name}]]`); 5 | return tagArr.join(', '); 6 | }; 7 | 8 | export const returnKindleHighlights = ( 9 | orgOrMarkdown: string, 10 | location: string, 11 | asin: string, 12 | highlighted_at: string, 13 | preferredDateFormat: string, 14 | tags: any[], 15 | text: string 16 | ) => { 17 | if (orgOrMarkdown === 'markdown') { 18 | return { 19 | content: `location:: [${location}](kindle://book?action=open&asin=${asin}&location=${location}) 20 | on:: ${getDateForPage(new Date(highlighted_at), preferredDateFormat)} 21 | tags:: ${prepareTags(tags)} 22 | ${text}`, 23 | }; 24 | } else if (orgOrMarkdown === 'org') { 25 | return { 26 | content: `:PROPERTIES: 27 | :location: [[kindle://book?action=open&asin=${asin}&location=${location}][${location}]] 28 | :on: ${getDateForPage(new Date(highlighted_at), preferredDateFormat)} 29 | :tags: ${prepareTags(tags)} 30 | :END: 31 | ${text}`, 32 | }; 33 | } 34 | }; 35 | 36 | export const returnOtherHighlights = ( 37 | orgOrMarkdown: string, 38 | url: string, 39 | highlighted_at: string, 40 | preferredDateFormat: string, 41 | tags: any[], 42 | text: string, 43 | height: string, 44 | width: string 45 | ) => { 46 | if (orgOrMarkdown === 'markdown') { 47 | return { 48 | content: `link:: [${url}](${url}) 49 | on:: ${getDateForPage(new Date(highlighted_at), preferredDateFormat)} 50 | tags:: ${prepareTags(tags)} 51 | ${ 52 | text.includes('![](') ? text + `{:height ${height} :width ${width}}` : text 53 | }`, 54 | }; 55 | } else if (orgOrMarkdown === 'org') { 56 | return { 57 | content: `:PROPERTIES: 58 | :link: ${url} 59 | :on: ${getDateForPage(new Date(highlighted_at), preferredDateFormat)} 60 | :tags: ${prepareTags(tags)} 61 | :END: 62 | ${text}`, 63 | }; 64 | } 65 | }; 66 | 67 | export const returnPageMetaData = ( 68 | orgOrMarkdown: string, 69 | preferredDateFormat: string, 70 | author: string, 71 | category: string, 72 | source: string, 73 | tags: any[], 74 | metaData: string 75 | ) => { 76 | if (orgOrMarkdown === 'markdown') { 77 | if (metaData === '') { 78 | return `retrieved:: ${getDateForPage(new Date(), preferredDateFormat)} 79 | author:: [[${author}]] 80 | category:: [[${category}]] 81 | source:: [[${source}]] 82 | tags:: ${prepareTags(tags)}`; 83 | } else { 84 | return `retrieved:: ${getDateForPage(new Date(), preferredDateFormat)} 85 | author:: [[${author}]] 86 | category:: [[${category}]] 87 | source:: [[${source}]] 88 | tags:: ${prepareTags(tags)} 89 | ${metaData}`; 90 | } 91 | } else if (orgOrMarkdown === 'org') { 92 | if (metaData === '') { 93 | return `:PROPERTIES: 94 | :retrieved: ${getDateForPage(new Date(), preferredDateFormat)} 95 | :author: [[${author}]] 96 | :category: [[${category}]] 97 | :source: [[${source}]] 98 | :tags: ${prepareTags(tags)} 99 | :END:`; 100 | } else { 101 | return `:PROPERTIES: 102 | :retrieved: ${getDateForPage(new Date(), preferredDateFormat)} 103 | :author: [[${author}]] 104 | :category: [[${category}]] 105 | :source: [[${source}]] 106 | :tags: ${prepareTags(tags)} 107 | ${metaData} 108 | :END:`; 109 | } 110 | } 111 | }; 112 | 113 | export const returnImage = ( 114 | orgOrMarkdown: string, 115 | cover_image_url: string, 116 | height: string, 117 | width: string 118 | ) => { 119 | if (orgOrMarkdown === 'markdown') { 120 | return `![book_image](${cover_image_url}){:height ${height} :width ${width}}`; 121 | } else if (orgOrMarkdown === 'org') { 122 | return `[[${cover_image_url}][${cover_image_url}]]`; 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /src/services/handleClosePopup.ts: -------------------------------------------------------------------------------- 1 | export const handleClosePopup = () => { 2 | //ESC 3 | document.addEventListener( 4 | 'keydown', 5 | function (e) { 6 | if (e.key === 'Escape') { 7 | logseq.hideMainUI({ restoreEditingCursor: true }); 8 | } 9 | e.stopPropagation(); 10 | }, 11 | false 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/pluginBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from '../App'; 4 | import { handleClosePopup } from './handleClosePopup'; 5 | 6 | export const pluginBar = () => { 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('app') 12 | ); 13 | 14 | logseq.provideModel({ 15 | show() { 16 | logseq.showMainUI(); 17 | }, 18 | }); 19 | 20 | handleClosePopup(); 21 | 22 | // Register UI 23 | logseq.App.registerUIItem('toolbar', { 24 | key: 'logseq-readwise-plugin', 25 | template: ` 26 | 27 | 28 | 29 | `, 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/services/randomHighlightsUtilities.ts: -------------------------------------------------------------------------------- 1 | export const getRandomHighlight = async () => { 2 | const getRandomNumber = (min: number, max: number) => { 3 | return Math.floor(Math.random() * (max - min + 1)) + min; 4 | }; 5 | 6 | const sectionHeader = logseq.settings.template.sectionHeader 7 | .replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '') 8 | .toLowerCase() 9 | .trim(); 10 | 11 | const readwiseHighlights = await logseq.DB 12 | .datascriptQuery(`[:find (pull ?b [*]) 13 | :where 14 | [?b :block/path-refs [:block/name "${sectionHeader}"]] 15 | ]`); 16 | 17 | const readwiseHighlightsArr = readwiseHighlights.map((a) => ({ 18 | pageId: a[0].page['id'], 19 | content: a[0].content, 20 | uuid: a[0].uuid['$uuid$'], 21 | })); 22 | 23 | const randomNumber = getRandomNumber(0, readwiseHighlightsArr.length - 1); 24 | 25 | return readwiseHighlightsArr[randomNumber]; 26 | }; 27 | 28 | export const scrollToRandomHighlight = async () => { 29 | logseq.hideMainUI(); 30 | 31 | const randomHighlight = await getRandomHighlight(); 32 | 33 | if (randomHighlight.content === logseq.settings.template.sectionHeader) { 34 | await scrollToRandomHighlight(); 35 | } else { 36 | const page = await logseq.Editor.getPage(randomHighlight['pageId']); 37 | 38 | console.log(page.name); 39 | 40 | logseq.Editor.scrollToBlockInPage(page.name, randomHighlight['uuid']); 41 | } 42 | }; 43 | 44 | export default { getRandomHighlight, scrollToRandomHighlight }; 45 | -------------------------------------------------------------------------------- /src/services/utilities.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | interface PluginSettings { 4 | pageSize: number; 5 | noOfBooks: number; 6 | noOfHighlights: number; 7 | noOfNewSources: number; 8 | bookList: any[]; 9 | sync: boolean; 10 | loaded: boolean; 11 | isRefreshing: boolean; 12 | errorLoading: boolean; 13 | } 14 | 15 | export const sleep = (ms: number) => { 16 | return new Promise((resolve) => setTimeout(resolve, ms)); 17 | }; 18 | 19 | export const getTotalNumberOfHighlightsAndBooks = async ( 20 | token: string, 21 | setPluginSettings: Function 22 | ) => { 23 | const response = await axios({ 24 | method: "get", 25 | url: "https://readwise.io/api/v2/highlights/", 26 | headers: { 27 | Authorization: `Token ${token}`, 28 | }, 29 | }); 30 | 31 | const response2 = await axios({ 32 | method: "get", 33 | url: "https://readwise.io/api/v2/books/", 34 | headers: { 35 | Authorization: `Token ${token}`, 36 | }, 37 | }); 38 | 39 | setPluginSettings((currSettings: PluginSettings) => ({ 40 | ...currSettings, 41 | noOfHighlights: response.data.count, 42 | noOfBooks: response2.data.count, 43 | })); 44 | }; 45 | 46 | export const loadFromReadwise = async ( 47 | token: string, 48 | pageSize: number, 49 | setPluginSettings: Function 50 | ) => { 51 | console.log("Loading from Readwise..."); 52 | // Log when is the latest retrieved 53 | console.log(`Latest retrieved date: ${logseq.settings.latestRetrieved}`); 54 | 55 | const promiseGetBooks = async (i: number) => { 56 | const response = await axios({ 57 | method: "get", 58 | url: `https://readwise.io/api/v2/books/?page=${i}`, 59 | headers: { 60 | Authorization: `Token ${token}`, 61 | }, 62 | params: { 63 | page_size: pageSize, 64 | last_highlight_at__gt: logseq.settings.latestRetrieved, 65 | }, 66 | }); 67 | 68 | return response; 69 | }; 70 | 71 | try { 72 | let i = 1; 73 | while (true) { 74 | try { 75 | const response = await promiseGetBooks(i); 76 | 77 | setPluginSettings((currSettings: PluginSettings) => ({ 78 | ...currSettings, 79 | noOfNewSources: response.data.count, 80 | bookList: response.data.results, 81 | })); 82 | 83 | if (i * pageSize < response.data.count) { 84 | i++; 85 | } else { 86 | return; 87 | } 88 | } catch (e) { 89 | if (e.response.status === 404) { 90 | return; 91 | } else { 92 | const retryAfter = 93 | parseInt(e.response.headers["retry-after"]) * 1000 + 5000; 94 | 95 | await sleep(retryAfter); 96 | 97 | console.log("Trying a second time..."); 98 | 99 | const response = await promiseGetBooks(i); 100 | 101 | if (response.data.detail === "Invalid page.") { 102 | break; 103 | } else { 104 | setPluginSettings((currSettings: PluginSettings) => ({ 105 | ...currSettings, 106 | bookList: response.data.results, 107 | })); 108 | 109 | if (i * pageSize < response.data.count) { 110 | i++; 111 | } else { 112 | return; 113 | } 114 | 115 | i++; 116 | } 117 | } 118 | } 119 | } 120 | } catch (e) { 121 | setPluginSettings((currSettings: PluginSettings) => ({ 122 | ...currSettings, 123 | errorLoading: true, 124 | })); 125 | } 126 | }; 127 | 128 | export const getHighlightsForBook = async ( 129 | id: number, 130 | token: string, 131 | setCoolingOff: Function 132 | ) => { 133 | let response: any; 134 | try { 135 | response = await axios({ 136 | method: "get", 137 | url: "https://readwise.io/api/v2/highlights/", 138 | headers: { 139 | Authorization: `Token ${token}`, 140 | }, 141 | params: { 142 | book_id: id, 143 | }, 144 | }); 145 | } catch (e) { 146 | console.log(e); 147 | 148 | if (e.response.status === 429) { 149 | setCoolingOff(true); 150 | } 151 | 152 | const retryAfter = 153 | parseInt(e.response.headers["retry-after"]) * 1000 + 5000; 154 | 155 | await sleep(retryAfter); 156 | 157 | console.log("Trying a second time..."); 158 | 159 | response = await axios({ 160 | method: "get", 161 | url: "https://readwise.io/api/v2/highlights/", 162 | headers: { 163 | Authorization: `Token ${token}`, 164 | }, 165 | params: { 166 | book_id: id, 167 | }, 168 | }); 169 | 170 | setCoolingOff(false); 171 | } 172 | 173 | return response; 174 | }; 175 | -------------------------------------------------------------------------------- /src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{vue,js,ts,jsx,tsx,hbs,html}"], 3 | theme: { 4 | extend: { 5 | spacing: { 6 | 100: "32rem", 7 | }, 8 | }, 9 | }, 10 | plugins: [], 11 | }; 12 | --------------------------------------------------------------------------------