├── 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 | 
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 |
121 | ${photographer}
122 | `
123 |
124 | resultItem.querySelector("img").addEventListener("click", () => {
125 | logseq.Editor.insertAtEditingCursor(``)
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 |
--------------------------------------------------------------------------------