├── demo.gif ├── icon.png ├── index.html ├── package.json ├── .gitignore ├── LICENSE ├── README.md ├── .github └── workflows │ └── release.yml ├── index.css └── index.ts /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clearlysid/logseq-unsplash/HEAD/demo.gif -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clearlysid/logseq-unsplash/HEAD/icon.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | Document 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logseq-unsplash", 3 | "version": "0.0.1", 4 | "description": "A Logseq plugin to add Unsplash images via slash commands 📷", 5 | "main": "dist/index.html", 6 | "author": "@clearlysid", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "parcel ./index.html --public-url ./", 10 | "build": "parcel build --public-url . --no-source-maps index.html" 11 | }, 12 | "devDependencies": { 13 | "@logseq/libs": "^0.0.1-alpha.19", 14 | "parcel": "^2.0.0-beta.2" 15 | }, 16 | "dependencies": {}, 17 | "logseq": { 18 | "id": "unsplash_image_yw5wnkfga", 19 | "icon": "./icon.png" 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # node-waf configuration 11 | .lock-wscript 12 | 13 | # Compiled binary addons (https://nodejs.org/api/addons.html) 14 | build/Release 15 | 16 | # Dependency directories 17 | node_modules/ 18 | jspm_packages/ 19 | 20 | # TypeScript cache 21 | *.tsbuildinfo 22 | 23 | # Optional npm cache directory 24 | .npm 25 | 26 | # Optional eslint cache 27 | .eslintcache 28 | 29 | # Output of 'npm pack' 30 | *.tgz 31 | 32 | # Yarn Integrity file 33 | .yarn-integrity 34 | 35 | # dotenv environment variables file 36 | .env 37 | .env.test 38 | .env.production 39 | 40 | # parcel-bundler cache (https://parceljs.org/) 41 | .cache 42 | .parcel-cache 43 | dist/ 44 | 45 | # Stores VSCode versions used for testing VSCode extensions 46 | .vscode-test 47 | 48 | # yarn v2 49 | .yarn/cache 50 | .yarn/unplugged 51 | .yarn/build-state.yml 52 | .yarn/install-state.gz 53 | .pnp.* 54 | 55 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Siddharth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unsplash Image Plugin for Logseq 2 | 3 | I've been enjoying Logseq more and more lately, but found myself missing some features from Notion, Framer, Figma, etc. The ability to quickly add a relevant image anywhere was one such feature. 4 | 5 | This plugin adds `/unsplash` command to Logseq, and let's you search the entire Unsplash library of 3 Million+ images within a couple of keystrokes. 6 | 7 | You can add them to your notes in one click, then resize them if you wish. 8 | 9 | ## Demo 10 | 11 | ![demo](./demo.gif) 12 | 13 | ## Installation 14 | 15 | This plugin will hopefully be available in the Logseq store soon, once I've fixed a few quirks and run this by their devs. Installing it then will be as simple as clicking "Install" from the Plugins Marketplace in the Logseq app. 16 | 17 | If you're adventurous and want to help refine this until then, see development instructions below. 18 | 19 | ## For development / local installation 20 | 21 | 1. clone this repo 22 | 2. `npm install && npm run build` in terminal to install dependencies. 23 | 3. `Load unpacked plugin` in Logseq Desktop client, and select the repo directory 24 | 25 | ## Someday / Maybe 26 | 27 | - [ ] Pagination 28 | - [ ] Infinite Scrolling 29 | - [ ] Option to store images locally 30 | - [ ] Keyboard navigation 31 | - [ ] Tags / Suggestions 32 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and release plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | env: 10 | PLUGIN_NAME: logseq-unsplash 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" 22 | - name: Build 23 | id: build 24 | run: | 25 | npm install -g yarn 26 | yarn 27 | yarn build 28 | mkdir ${{ env.PLUGIN_NAME }} 29 | cp LICENSE README.md package.json icon.png demo.gif ${{ env.PLUGIN_NAME }} 30 | mv dist ${{ env.PLUGIN_NAME }} 31 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 32 | ls 33 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 34 | - name: Create Release 35 | id: create_release 36 | uses: actions/create-release@v1 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | VERSION: ${{ github.ref }} 40 | with: 41 | tag_name: ${{ github.ref }} 42 | release_name: ${{ github.ref }} 43 | draft: false 44 | prerelease: false 45 | 46 | - name: Upload zip file 47 | id: upload_zip 48 | uses: actions/upload-release-asset@v1 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | with: 52 | upload_url: ${{ steps.create_release.outputs.upload_url }} 53 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 54 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 55 | asset_content_type: application/zip 56 | 57 | - name: Upload package.json 58 | id: upload_metadata 59 | uses: actions/upload-release-asset@v1 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | with: 63 | upload_url: ${{ steps.create_release.outputs.upload_url }} 64 | asset_path: ./package.json 65 | asset_name: package.json 66 | asset_content_type: application/json 67 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | padding: 0; 9 | width: 100%; 10 | height: 100%; 11 | background-color: rgba(0, 0, 0, 0); 12 | overflow-x: hidden; 13 | overflow-y: auto; 14 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 15 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", 16 | "Segoe UI Symbol"; 17 | } 18 | 19 | :root { 20 | --background-color: #ffffff; 21 | --button-color: #000000; 22 | --text-color: #a9a9a9; 23 | --border-color: #e5e5e5; 24 | transition: all 0.6s ease; 25 | } 26 | 27 | .dark { 28 | --background-color: #222222; 29 | --button-color: #444444; 30 | --text-color: #9b9b9b; 31 | --border-color: #555555; 32 | } 33 | 34 | .unsplash-wrapper { 35 | position: absolute; 36 | min-width: 20px; 37 | min-height: 20px; 38 | background-color: var(--background-color); 39 | border: 1px solid var(--border-color); 40 | left: 20%; 41 | top: 20%; 42 | z-index: 1; 43 | border-radius: 4px; 44 | height: max-content; 45 | width: 400px; 46 | box-shadow: 2px 4px 20px rgb(0 0 0 / 10%); 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | .search-form { 52 | display: flex; 53 | padding: 16px; 54 | } 55 | 56 | .search-input { 57 | width: 100%; 58 | height: 40px; 59 | padding: 12px; 60 | font-size: 13px; 61 | border: 1px solid var(--border-color); 62 | border-radius: 4px; 63 | background: transparent; 64 | color: var(--text-color); 65 | outline-color: var(--text-color); 66 | } 67 | 68 | ::placeholder { 69 | color: var(--text-color); 70 | opacity: 0.8; 71 | } 72 | 73 | .search-button { 74 | height: 40px; 75 | margin-left: 12px; 76 | display: flex; 77 | align-items: center; 78 | justify-content: center; 79 | color: white; 80 | background: var(--button-color); 81 | border-radius: 4px; 82 | font-size: 13px; 83 | padding: 12px 16px; 84 | border: none; 85 | } 86 | 87 | .result-container { 88 | border-top: 1px solid var(--border-color); 89 | overflow-y: scroll; 90 | overflow-x: hidden; 91 | height: max-content; 92 | max-height: 400px; 93 | padding: 16px; 94 | display: grid; 95 | grid-template-columns: 1fr 1fr; 96 | column-gap: 16px; 97 | } 98 | 99 | .show-more { 100 | grid-column: 1 / span 2; 101 | width: 100%; 102 | padding: 12px 16px; 103 | font-size: 13px; 104 | margin: 0 -16px; 105 | border: none; 106 | border-radius: 4px; 107 | } 108 | 109 | .result-item { 110 | margin-bottom: 20px; 111 | overflow: hidden; 112 | } 113 | 114 | .result-image { 115 | width: 100%; 116 | height: auto; 117 | cursor: pointer; 118 | transition: opacity 0.2s ease; 119 | } 120 | 121 | .result-image:hover { 122 | opacity: 0.6; 123 | } 124 | 125 | .result-link { 126 | margin-top: 8px; 127 | font-size: 11px; 128 | color: var(--text-color); 129 | } 130 | 131 | .hidden { 132 | display: none; 133 | } 134 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import '@logseq/libs' 2 | 3 | const apiKey = "TjzU95V4JHVry7D_iigag7nKd940el7fMBB9KgBLpRY"; // Unsplash Dummy API key 4 | 5 | const removeChildren = (el: HTMLElement) => { 6 | while (el.firstChild) { 7 | el.removeChild(el.lastChild) 8 | } 9 | } 10 | 11 | /** 12 | * main entry 13 | */ 14 | async function main() { 15 | let elementsCreated = false 16 | const container = document.createElement('div') 17 | container.classList.add('unsplash-wrapper') 18 | document.getElementById('app').appendChild(container) 19 | 20 | const appUserConfig = await logseq.App.getUserConfigs() 21 | 22 | appUserConfig.preferredThemeMode === "dark" && container.classList.add('dark') 23 | 24 | logseq.App.onThemeModeChanged(({ mode }) => { 25 | mode === "dark" 26 | ? container.classList.add('dark') 27 | : container.classList.remove('dark') 28 | }) 29 | 30 | const createDomElements = () => { 31 | // Create input field 32 | const form = document.createElement("form") 33 | form.classList.add('search-form') 34 | form.innerHTML = ` 35 | 36 | 37 | ` 38 | container.appendChild(form) 39 | 40 | // Create image grid for search results 41 | const resultContainer = document.createElement("div") 42 | resultContainer.classList.add("result-container") 43 | 44 | resultContainer.innerHTML = ` 45 |
46 |
47 | 48 | ` 49 | container.appendChild(resultContainer) 50 | } 51 | 52 | const cleanupResults = () => { 53 | removeChildren(document.querySelector('.col-1')) 54 | removeChildren(document.querySelector('.col-2')) 55 | } 56 | 57 | const initUnsplash = () => { 58 | 59 | let currentPage = 1 60 | let searchTerm = "" 61 | 62 | if (!elementsCreated) { 63 | createDomElements() 64 | elementsCreated = true 65 | } 66 | 67 | // Hide results section on start 68 | document.querySelector(".result-container").classList.add("hidden"); 69 | ((document.querySelector(".search-input"))).focus(); 70 | 71 | // Handle form submission 72 | document.querySelector('.search-form').addEventListener("submit", (event: Event) => { 73 | event.preventDefault() 74 | currentPage = 1 75 | 76 | const inputValue = ((document.querySelector(".search-input"))).value 77 | 78 | cleanupResults() 79 | 80 | searchTerm = inputValue.trim() 81 | fetchResults(searchTerm) 82 | document.querySelector(".result-container").classList.remove("hidden") 83 | }) 84 | 85 | document.querySelector(".show-more").addEventListener("click", () => { 86 | currentPage++ 87 | fetchResults(searchTerm) 88 | }) 89 | 90 | const fetchDataFromUnsplash = async (searchTerm: string) => { 91 | const endpoint = `https://api.unsplash.com/search/photos` 92 | const params = `?query=${encodeURIComponent(searchTerm)}&per_page=30&page=${currentPage}&client_id=${apiKey}` 93 | const response = await fetch(endpoint + params) 94 | if (!response.ok) throw Error(response.statusText) 95 | const json = await response.json() 96 | return json 97 | } 98 | 99 | async function fetchResults(searchTerm: string) { 100 | try { 101 | const results = await fetchDataFromUnsplash(searchTerm) 102 | addToResults(results) 103 | } catch (err) { 104 | console.log(err) 105 | } 106 | } 107 | 108 | const addToResults = (json) => { 109 | 110 | json.results.forEach((result, index) => { 111 | const imageDesc = result.alt_description 112 | const imageUrl = result.urls.small 113 | const photographer = result.user.name 114 | const photographerUrl = result.user.links.html 115 | 116 | const resultItem = document.createElement("div") 117 | resultItem.classList.add("result-item") 118 | 119 | resultItem.innerHTML = ` 120 | ${imageDesc} 121 | ${photographer} 122 | ` 123 | 124 | resultItem.querySelector("img").addEventListener("click", () => { 125 | logseq.Editor.insertAtEditingCursor(`![${imageDesc}](${imageUrl})`) 126 | logseq.Editor.exitEditingMode() 127 | logseq.hideMainUI() 128 | 129 | cleanupResults(); 130 | ((document.querySelector(".search-input"))).value = "" 131 | }) 132 | 133 | index % 2 === 0 134 | ? document.querySelector(".col-1").appendChild(resultItem) 135 | : document.querySelector(".col-2").appendChild(resultItem) 136 | }) 137 | } 138 | 139 | // Handle escape keypress 140 | document.addEventListener('keydown', (e) => { 141 | e.stopPropagation() 142 | 143 | if (e.key === "Escape") { 144 | logseq.hideMainUI({ restoreEditingCursor: true }) 145 | cleanupResults(); 146 | ((document.querySelector(".search-input"))).value = "" 147 | } 148 | }, false) 149 | 150 | // Handle click outside window 151 | document.addEventListener('click', (e) => { 152 | if (!(e.target as HTMLElement).closest('.unsplash-wrapper')) { 153 | logseq.hideMainUI({ restoreEditingCursor: true }) 154 | cleanupResults(); 155 | ((document.querySelector(".search-input"))).value = "" 156 | } 157 | }) 158 | } 159 | 160 | // Adds slash command for unsplash 161 | logseq.Editor.registerSlashCommand( 162 | 'Unsplash', async () => { 163 | const { left, top, rect } = await logseq.Editor.getEditingCursorPosition() 164 | Object.assign(container.style, { 165 | top: top + rect.top + 'px', 166 | left: left + rect.left + 'px', 167 | }) 168 | logseq.showMainUI() 169 | 170 | setTimeout(() => initUnsplash(), 100) 171 | }, 172 | ) 173 | } 174 | 175 | // bootstrap 176 | logseq.ready(main).catch(console.error) 177 | --------------------------------------------------------------------------------