Initial commit

This commit is contained in:
Jakob Essbüchl 2022-01-10 12:13:06 +01:00
parent 49f02f5ca6
commit c36066ca70
45 changed files with 11097 additions and 0 deletions

13
LICENSE.txt Normal file
View file

@ -0,0 +1,13 @@
The MIT License (MIT)
Original code:
- Copyright (c) 2021 Sascha Ißbrücker
Adaptions and injector functionality:
- Copyright (c) 2022 Jakob Essbüchl
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.

68
README.md Normal file
View file

@ -0,0 +1,68 @@
![logo](/icons/logo_full.svg)
Community browser extension for the self-hosted [linkding](https://github.com/sissbruecker/linkding) bookmark service.
**Features**
- When searching on a search engine the search term is also sent to your linkding instance and results are added in a new box in the sidebar right to the search engine results.
- Supports [google](https://www.google.com/) and [duckduckgo](https://duckduckgo.com/) search engines
- Automatic light or dark theme detection
Works with: Firefox, Chrome
**Usage**
After installation the extension needs to be configured and connected to your linkding instance. Either open the extension options in the browser extension manager or follow the link in the new linkding injector box on the search page of google or duckduckgo.
Once the extension is properly configured linkding search results will show in the right sidebar. If there are no search results nothing will appear.
**Screenshots**
![duckduckgo](/docs/duckduckgo.png "Duckduckgo")
![google](/docs/google.png "google")
## Installation
Firefox: [Mozilla Addon Store](https://addons.mozilla.org/de/firefox/addon/linkding-extension/)
Chrome: [Chrome Web Store](https://chrome.google.com/webstore/detail/linkding-extension/beakmhbijpdhipnjhnclmhgjlddhidpe)
## Manual installation
### Firefox
Run the build as described below and then follow the instructions [here](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Your_first_WebExtension#installing) to load it into Firefox.
### Chrome
Run the build as described below and then follow the instructions [here](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest) to load it into Chrome.
## Build
**Requirements**
- Latest LTS Node version (v14)
- Latest LTS NPM version (v6)
- bash (on Linux) or powershell (on Windows)
- npx (included with npm v5.2+)
Internally, we use `web-ext` to bundle a distribution package for the extension for Firefox. You do not need to install `web-ext`. Note that `web-ext` will generate a zip file which can also be used for the Chrome Web Store.
Then run the following script to generate a build (might need to make the file executable on Linux using `chmod +x build.sh`):
```bash
./build.sh # Linux
```
```powershell
./build.ps1 # Windows
```
The script does:
- Install all dependencies using NPM
- Runs rollup to transpile and minify source files, with output written to the `build` directory
- Run web-ext to package the extension for uploading to the Mozilla addon store
After the build the root directory contains the complete, unpackaged extension. Use the `manifest.json` file to load it manually into the browser.
The packaged extension can be found in the `web-ext-artifacts` folder.
## Acknowledgments
This extension resuses and adapts code from the [official linkding extension](https://github.com/sissbruecker/linkding-extension).

12
build.ps1 Normal file
View file

@ -0,0 +1,12 @@
# Update dependencies
npm install
# Run rollup build
npm run build
# Lint extension, while excluding dev files
npx web-ext lint --ignore-files .idea dist docs src web-ext-artifacts .gitignore *.sh *.iml *.js *.lock
# Build extension, while excluding dev files
npx web-ext build --overwrite-dest --ignore-files .idea dist docs src web-ext-artifacts .gitignore *.sh *.iml *.js *.lock
echo "✅ Done"

14
build.sh Normal file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Update dependencies
npm install
# Run rollup build
npm run build
# Lint extension, while excluding dev files
npx web-ext lint --ignore-files .idea dist docs src web-ext-artifacts .gitignore *.sh *.iml *.js *.lock
# Build extension, while excluding dev files
npx web-ext build --overwrite-dest --ignore-files .idea dist docs src web-ext-artifacts .gitignore *.sh *.iml *.js *.lock
echo "✅ Done"

BIN
docs/duckduckgo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
docs/google.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

1
icons/cog.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24" style="fill: #a8b1ff"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
icons/ld_128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
icons/ld_19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
icons/ld_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
icons/ld_38.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
icons/ld_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
icons/ld_96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

25
icons/logo.svg Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 57 57" style="enable-background:new 0 0 57 57;" xml:space="preserve">
<style type="text/css">
.st0{fill:#5856E0;}
.st1{fill:#FFFFFF;}
.st2{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:2.3769;stroke-miterlimit:10;}
.st3{fill:#5B5EA9;stroke:#5856E0;stroke-width:2;stroke-miterlimit:10;}
</style>
<g id="Layer_1">
<circle class="st0" cx="28.5" cy="28.5" r="28.5"/>
</g>
<g id="Layer_2">
<g>
<polygon class="st1" points="33.2,14.2 18,29.3 18,39 27.7,39 42.8,23.8 "/>
<line class="st2" x1="25.7" y1="31.3" x2="11.3" y2="45.7"/>
<line class="st2" x1="33.7" y1="10.1" x2="47.3" y2="23.7"/>
<line class="st2" x1="40.5" y1="16.9" x2="45.3" y2="12"/>
<line class="st2" x1="41.6" y1="8.3" x2="49" y2="15.7"/>
<line class="st3" x1="20.6" y1="24.8" x2="25.2" y2="29.3"/>
<line class="st3" x1="26.2" y1="19.1" x2="30.8" y2="23.7"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/logo_128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
icons/logo_19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

BIN
icons/logo_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

BIN
icons/logo_38.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

BIN
icons/logo_48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icons/logo_96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

70
icons/logo_full.svg Normal file
View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 199 57" style="enable-background:new 0 0 199 57;" xml:space="preserve">
<style type="text/css">
.st0{fill:#5856E0;}
.st1{fill:#FFFFFF;}
.st2{fill:none;stroke:#FFFFFF;stroke-width:2.3769;stroke-miterlimit:10;}
.st3{fill:none;stroke:#5856E0;stroke-width:2;stroke-miterlimit:10;}
.st4{fill:none;}
</style>
<g>
<g id="Layer_1_1_">
<circle class="st0" cx="28.5" cy="28.5" r="28.5"/>
</g>
<g id="Layer_2_1_">
<g>
<polygon class="st1" points="33.2,14.2 18,29.3 18,39 27.7,39 42.8,23.8 "/>
<line class="st2" x1="25.7" y1="31.3" x2="11.3" y2="45.7"/>
<line class="st2" x1="33.7" y1="10.1" x2="47.3" y2="23.7"/>
<line class="st2" x1="40.5" y1="16.9" x2="45.3" y2="12"/>
<line class="st2" x1="41.6" y1="8.3" x2="49" y2="15.7"/>
<line class="st3" x1="20.6" y1="24.8" x2="25.2" y2="29.3"/>
<line class="st3" x1="26.2" y1="19.1" x2="30.8" y2="23.7"/>
</g>
</g>
</g>
<g>
<rect x="65.1" y="4.7" class="st4" width="172.5" height="47.6"/>
<path class="st0" d="M67.4,25.2V5.3h3.1v17.1h10.7v2.8H67.4z"/>
<path class="st0" d="M83.7,25.2V5.3h3.1v19.9H83.7z"/>
<path class="st0" d="M94.5,11.2v14h-3.1V5.3h2.5l11.4,14.3V5.3h3.2v19.9h-2.6L94.5,11.2z"/>
<path class="st0" d="M112.8,25.2V5.3h3.1v10.2l9.4-10.2h3.5l-7.8,8.8l8.3,11.1h-3.5L119,16l-3.1,3.2v6H112.8z"/>
<path class="st0" d="M131.3,25.2V5.3h7.1c1.6,0,3,0.3,4.2,0.8c1.2,0.5,2.2,1.2,3,2.1c0.8,0.9,1.4,1.9,1.8,3.1
c0.4,1.2,0.6,2.5,0.6,3.9c0,1.5-0.2,2.9-0.7,4.1c-0.5,1.2-1.1,2.3-1.9,3.1c-0.8,0.9-1.9,1.5-3.1,2s-2.5,0.7-4,0.7H131.3z
M144.8,15.2c0-1-0.1-2-0.4-2.9c-0.3-0.9-0.7-1.6-1.3-2.3c-0.6-0.6-1.2-1.1-2-1.5c-0.8-0.4-1.7-0.5-2.7-0.5h-3.9v14.3h3.9
c1,0,2-0.2,2.8-0.5c0.8-0.4,1.5-0.9,2-1.5s1-1.4,1.2-2.3S144.8,16.2,144.8,15.2z"/>
<path class="st0" d="M151.3,25.2V5.3h3.1v19.9H151.3z"/>
<path class="st0" d="M162.1,11.2v14H159V5.3h2.5l11.4,14.3V5.3h3.2v19.9h-2.6L162.1,11.2z"/>
<path class="st0" d="M194.4,22.7c-1.7,1.7-3.6,2.6-5.8,2.6c-1.3,0-2.6-0.3-3.7-0.8c-1.1-0.6-2.1-1.3-3-2.2c-0.8-0.9-1.5-2-2-3.2
c-0.5-1.2-0.7-2.5-0.7-3.9c0-1.3,0.2-2.6,0.7-3.8c0.5-1.2,1.1-2.3,2-3.2s1.8-1.6,3-2.2c1.2-0.5,2.4-0.8,3.8-0.8
c1.9,0,3.5,0.4,4.8,1.2c1.3,0.8,2.3,1.8,2.9,3.1l-2.4,1.7c-0.5-1.1-1.3-1.9-2.2-2.4c-1-0.5-2-0.8-3.1-0.8c-0.9,0-1.8,0.2-2.5,0.6
s-1.4,0.9-2,1.6c-0.5,0.7-1,1.4-1.2,2.3c-0.3,0.9-0.4,1.8-0.4,2.7c0,1,0.2,2,0.5,2.8c0.3,0.9,0.8,1.6,1.3,2.3s1.2,1.2,2,1.6
c0.8,0.4,1.6,0.6,2.5,0.6c1,0,2-0.2,2.9-0.7c0.9-0.5,1.8-1.2,2.6-2.2v-2.1h-4.2v-2.3h6.8v10.1h-2.6V22.7z"/>
<path class="st0" d="M67.4,51.2V31.3h3.1v19.9H67.4z"/>
<path class="st0" d="M78.1,37.2v14H75V31.3h2.5l11.4,14.3V31.3H92v19.9h-2.6L78.1,37.2z"/>
<path class="st0" d="M95.3,47.7c0.3,0.2,0.8,0.4,1.4,0.7c0.6,0.2,1.3,0.3,2.1,0.3c0.9,0,1.5-0.2,2.1-0.5c0.5-0.3,0.9-0.8,1.2-1.4
s0.4-1.4,0.5-2.3s0.1-2,0.1-3.2V31.3h3.2v10.1c0,1.5-0.1,2.9-0.2,4.1s-0.5,2.3-1,3.2c-0.5,0.9-1.2,1.6-2.1,2s-2.2,0.7-3.8,0.7
c-1.7,0-3.1-0.4-4.3-1.2L95.3,47.7z"/>
<path class="st0" d="M123.8,48.4v2.8h-13.6V31.3h13.4v2.8h-10.2v5.7h8.9v2.6h-8.9v6.1H123.8z"/>
<path class="st0" d="M125.3,41.1c0-1.2,0.2-2.4,0.6-3.6c0.4-1.2,1.1-2.2,1.9-3.2c0.8-0.9,1.8-1.7,3-2.3c1.2-0.6,2.5-0.9,4.1-0.9
c1.8,0,3.4,0.4,4.6,1.2c1.3,0.8,2.3,1.8,2.9,3.1l-2.5,1.7c-0.3-0.6-0.6-1.1-1-1.5c-0.4-0.4-0.8-0.7-1.3-1c-0.5-0.2-1-0.4-1.5-0.5
s-1-0.2-1.5-0.2c-1,0-2,0.2-2.7,0.6s-1.4,1-2,1.7c-0.5,0.7-0.9,1.5-1.2,2.3s-0.4,1.7-0.4,2.6c0,1,0.2,1.9,0.5,2.8
c0.3,0.9,0.8,1.7,1.3,2.3s1.2,1.2,2,1.6s1.6,0.6,2.6,0.6c0.5,0,1-0.1,1.5-0.2c0.5-0.1,1-0.3,1.5-0.6c0.5-0.3,0.9-0.6,1.3-1
c0.4-0.4,0.7-0.9,1-1.5l2.6,1.5c-0.3,0.7-0.8,1.4-1.3,2c-0.6,0.6-1.2,1-2,1.4c-0.7,0.4-1.5,0.7-2.3,0.9c-0.8,0.2-1.6,0.3-2.4,0.3
c-1.4,0-2.7-0.3-3.8-0.9c-1.2-0.6-2.2-1.4-3-2.3c-0.8-1-1.5-2.1-2-3.3S125.3,42.4,125.3,41.1z"/>
<path class="st0" d="M160.1,34.1h-6.6v17.1h-3.2V34.1h-6.6v-2.8h16.4V34.1z"/>
<path class="st0" d="M170.1,51.3c-1.4,0-2.7-0.3-3.9-0.9c-1.2-0.6-2.2-1.3-3-2.3c-0.8-0.9-1.5-2-1.9-3.2c-0.5-1.2-0.7-2.5-0.7-3.7
c0-1.3,0.2-2.6,0.7-3.8c0.5-1.2,1.2-2.3,2-3.2s1.9-1.7,3-2.2c1.2-0.5,2.4-0.8,3.8-0.8c1.4,0,2.7,0.3,3.9,0.9c1.2,0.6,2.2,1.4,3,2.3
c0.8,1,1.5,2,1.9,3.2c0.5,1.2,0.7,2.4,0.7,3.7c0,1.3-0.2,2.6-0.7,3.8c-0.5,1.2-1.1,2.3-2,3.2c-0.9,0.9-1.9,1.7-3,2.2
C172.8,51,171.5,51.3,170.1,51.3z M163.8,41.2c0,0.9,0.1,1.8,0.4,2.7c0.3,0.9,0.7,1.7,1.3,2.3c0.6,0.7,1.2,1.2,2,1.6
c0.8,0.4,1.7,0.6,2.6,0.6c1,0,1.9-0.2,2.7-0.6c0.8-0.4,1.4-1,2-1.7c0.5-0.7,1-1.5,1.2-2.3s0.4-1.7,0.4-2.6c0-1-0.1-1.9-0.4-2.7
s-0.7-1.6-1.3-2.3c-0.6-0.7-1.2-1.2-2-1.6c-0.8-0.4-1.6-0.6-2.6-0.6c-1,0-1.9,0.2-2.7,0.6s-1.4,1-2,1.6c-0.5,0.7-1,1.4-1.2,2.3
C164,39.4,163.8,40.3,163.8,41.2z"/>
<path class="st0" d="M182.9,51.2V31.3h8.7c0.9,0,1.7,0.2,2.5,0.6c0.8,0.4,1.4,0.9,1.9,1.5c0.5,0.6,1,1.3,1.3,2.1
c0.3,0.8,0.5,1.6,0.5,2.4c0,0.7-0.1,1.3-0.3,1.9c-0.2,0.6-0.4,1.2-0.8,1.7c-0.3,0.5-0.7,1-1.2,1.4c-0.5,0.4-1,0.7-1.6,0.9l4.7,7.6
H195l-4.3-6.9h-4.6v6.9H182.9z M186.1,41.5h5.5c0.4,0,0.9-0.1,1.2-0.3c0.4-0.2,0.7-0.5,0.9-0.8c0.3-0.3,0.5-0.7,0.6-1.2
c0.1-0.4,0.2-0.9,0.2-1.4s-0.1-1-0.3-1.4s-0.4-0.8-0.7-1.2c-0.3-0.3-0.6-0.6-1-0.8c-0.4-0.2-0.8-0.3-1.2-0.3h-5.3V41.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

25
icons/logo_small.svg Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 57 57" style="enable-background:new 0 0 57 57;" xml:space="preserve">
<style type="text/css">
.st0{fill:#5856E0;}
.st1{fill:#FFFFFF;}
.st2{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:3;stroke-miterlimit:10;}
.st3{fill:#5B5EA9;stroke:#5856E0;stroke-width:3;stroke-miterlimit:10;}
</style>
<g id="Layer_1">
<circle class="st0" cx="28.5" cy="28.5" r="28.5"/>
</g>
<g id="Layer_2">
<g>
<polygon class="st1" points="31.9,15.5 18,29.3 18,39 27.7,39 41.5,25.1 "/>
<line class="st2" x1="25.7" y1="31.3" x2="11.3" y2="45.7"/>
<line class="st2" x1="33.7" y1="10.1" x2="47.3" y2="23.7"/>
<line class="st2" x1="40.5" y1="16.9" x2="45.3" y2="12"/>
<line class="st2" x1="41.6" y1="8.3" x2="49" y2="15.7"/>
<line class="st3" x1="20.6" y1="24.8" x2="25.2" y2="29.3"/>
<line class="st3" x1="26.2" y1="19.1" x2="30.8" y2="23.7"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

231
manifest.json Normal file
View file

@ -0,0 +1,231 @@
{
"manifest_version": 2,
"name": "linkding injector",
"version": "1.0.0",
"description": "Injects search results from the linkding bookmark service into search pages like google and duckduckgo",
"homepage_url": "https://github.com/sissbruecker/linkding-extension/",
"icons": {
"19": "icons/logo_19.png",
"32": "icons/logo_32.png",
"48": "icons/logo_48.png",
"96": "icons/logo_96.png",
"128": "icons/logo_128.png"
},
"background": {
"scripts": ["build/background.js"]
},
"content_scripts": [
{
"matches": ["*://duckduckgo.com/*"],
"css": ["styles/searchInjection.css"],
"js": ["build/searchInjection.js"]
},
{
"matches": [
"*://*.google.com/*",
"*://*.google.ad/*",
"*://*.google.ae/*",
"*://*.google.com.af/*",
"*://*.google.com.ag/*",
"*://*.google.com.ai/*",
"*://*.google.al/*",
"*://*.google.am/*",
"*://*.google.co.ao/*",
"*://*.google.com.ar/*",
"*://*.google.as/*",
"*://*.google.at/*",
"*://*.google.com.au/*",
"*://*.google.az/*",
"*://*.google.ba/*",
"*://*.google.com.bd/*",
"*://*.google.be/*",
"*://*.google.bf/*",
"*://*.google.bg/*",
"*://*.google.com.bh/*",
"*://*.google.bi/*",
"*://*.google.bj/*",
"*://*.google.com.bn/*",
"*://*.google.com.bo/*",
"*://*.google.com.br/*",
"*://*.google.bs/*",
"*://*.google.bt/*",
"*://*.google.co.bw/*",
"*://*.google.by/*",
"*://*.google.com.bz/*",
"*://*.google.ca/*",
"*://*.google.cd/*",
"*://*.google.cf/*",
"*://*.google.cg/*",
"*://*.google.ch/*",
"*://*.google.ci/*",
"*://*.google.co.ck/*",
"*://*.google.cl/*",
"*://*.google.cm/*",
"*://*.google.cn/*",
"*://*.google.com.co/*",
"*://*.google.co.cr/*",
"*://*.google.com.cu/*",
"*://*.google.cv/*",
"*://*.google.com.cy/*",
"*://*.google.cz/*",
"*://*.google.de/*",
"*://*.google.dj/*",
"*://*.google.dk/*",
"*://*.google.dm/*",
"*://*.google.com.do/*",
"*://*.google.dz/*",
"*://*.google.com.ec/*",
"*://*.google.ee/*",
"*://*.google.com.eg/*",
"*://*.google.es/*",
"*://*.google.com.et/*",
"*://*.google.fi/*",
"*://*.google.com.fj/*",
"*://*.google.fm/*",
"*://*.google.fr/*",
"*://*.google.ga/*",
"*://*.google.ge/*",
"*://*.google.gg/*",
"*://*.google.com.gh/*",
"*://*.google.com.gi/*",
"*://*.google.gl/*",
"*://*.google.gm/*",
"*://*.google.gr/*",
"*://*.google.com.gt/*",
"*://*.google.gy/*",
"*://*.google.com.hk/*",
"*://*.google.hn/*",
"*://*.google.hr/*",
"*://*.google.ht/*",
"*://*.google.hu/*",
"*://*.google.co.id/*",
"*://*.google.ie/*",
"*://*.google.co.il/*",
"*://*.google.im/*",
"*://*.google.coIn/*",
"*://*.google.iq/*",
"*://*.google.is/*",
"*://*.google.it/*",
"*://*.google.je/*",
"*://*.google.com.jm/*",
"*://*.google.jo/*",
"*://*.google.co.jp/*",
"*://*.google.co.ke/*",
"*://*.google.com.kh/*",
"*://*.google.ki/*",
"*://*.google.kg/*",
"*://*.google.co.kr/*",
"*://*.google.com.kw/*",
"*://*.google.kz/*",
"*://*.google.la/*",
"*://*.google.com.lb/*",
"*://*.google.li/*",
"*://*.google.lk/*",
"*://*.google.co.ls/*",
"*://*.google.lt/*",
"*://*.google.lu/*",
"*://*.google.lv/*",
"*://*.google.com.ly/*",
"*://*.google.co.ma/*",
"*://*.google.md/*",
"*://*.google.me/*",
"*://*.google.mg/*",
"*://*.google.mk/*",
"*://*.google.ml/*",
"*://*.google.com.mm/*",
"*://*.google.mn/*",
"*://*.google.ms/*",
"*://*.google.com.mt/*",
"*://*.google.mu/*",
"*://*.google.mv/*",
"*://*.google.mw/*",
"*://*.google.com.mx/*",
"*://*.google.com.my/*",
"*://*.google.co.mz/*",
"*://*.google.com.na/*",
"*://*.google.com.ng/*",
"*://*.google.com.ni/*",
"*://*.google.ne/*",
"*://*.google.nl/*",
"*://*.google.no/*",
"*://*.google.com.np/*",
"*://*.google.nr/*",
"*://*.google.nu/*",
"*://*.google.co.nz/*",
"*://*.google.com.om/*",
"*://*.google.com.pa/*",
"*://*.google.com.pe/*",
"*://*.google.com.pg/*",
"*://*.google.com.ph/*",
"*://*.google.com.pk/*",
"*://*.google.pl/*",
"*://*.google.pn/*",
"*://*.google.com.pr/*",
"*://*.google.ps/*",
"*://*.google.pt/*",
"*://*.google.com.py/*",
"*://*.google.com.qa/*",
"*://*.google.ro/*",
"*://*.google.ru/*",
"*://*.google.rw/*",
"*://*.google.com.sa/*",
"*://*.google.com.sb/*",
"*://*.google.sc/*",
"*://*.google.se/*",
"*://*.google.com.sg/*",
"*://*.google.sh/*",
"*://*.google.si/*",
"*://*.google.sk/*",
"*://*.google.com.sl/*",
"*://*.google.sn/*",
"*://*.google.so/*",
"*://*.google.sm/*",
"*://*.google.sr/*",
"*://*.google.st/*",
"*://*.google.com.sv/*",
"*://*.google.td/*",
"*://*.google.tg/*",
"*://*.google.co.th/*",
"*://*.google.com.tj/*",
"*://*.google.tl/*",
"*://*.google.tm/*",
"*://*.google.tn/*",
"*://*.google.to/*",
"*://*.google.com.tr/*",
"*://*.google.tt/*",
"*://*.google.com.tw/*",
"*://*.google.co.tz/*",
"*://*.google.com.ua/*",
"*://*.google.co.ug/*",
"*://*.google.co.uk/*",
"*://*.google.com.uy/*",
"*://*.google.co.uz/*",
"*://*.google.com.vc/*",
"*://*.google.co.ve/*",
"*://*.google.vg/*",
"*://*.google.co.vi/*",
"*://*.google.com.vn/*",
"*://*.google.vu/*",
"*://*.google.ws/*",
"*://*.google.rs/*",
"*://*.google.co.za/*",
"*://*.google.co.zm/*",
"*://*.google.co.zw/*",
"*://*.google.cat/*"
],
"css": ["styles/searchInjection.css"],
"js": ["build/searchInjection.js"]
}
],
"options_ui": {
"page": "options/index.html"
},
"web_accessible_resources": ["icons/*.png", "icons/*.svg"],
"permissions": ["https://*/*", "http://*/*"]
}

15
options/index.html Normal file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Linkding Extension Options</title>
<link rel="stylesheet" href="../styles/spectre.css">
<link rel="stylesheet" href="../styles/spectre-icons.css">
<link rel="stylesheet" href="../styles/index.css">
</head>
<body>
<div id="app"></div>
<script src="../build/bundle.js"></script>
<script src="index.js"></script>
</body>
</html>

2
options/index.js Normal file
View file

@ -0,0 +1,2 @@
var appTarget = document.getElementById('app');
new linkding.Options({target: appTarget});

4332
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "linkding-injector",
"version": "1.0.0",
"description": "Injects search results from the linkding bookmark service into search pages like google and duckduckgo",
"license": "MIT",
"author": "Jakob Essbüchl",
"repository": {
"type": "git",
"url": "git+https://github.com/fivefold/linkding-injector.git"
},
"bugs": {
"url": "https://github.com/fivefold/linkding-injector/issues"
},
"homepage": "https://github.com/fivefold/linkding-injector#readme",
"scripts": {
"build": "rollup -c && sass scss/theme.scss styles/searchInjection.css",
"dev": "rollup -c -w && sass scss/theme.scss styles/searchInjection.css"
},
"dependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.1",
"rollup": "^2.35.1",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.2",
"spectre.css": "^0.5.9",
"svelte": "^3.31.0"
},
"devDependencies": {
"prettier": "^2.2.1",
"web-ext": "^6.1.0"
}
}

16
popup/index.html Normal file
View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Linkding Extension Popup</title>
<link rel="stylesheet" href="../styles/spectre.css">
<link rel="stylesheet" href="../styles/spectre-icons.css">
<link rel="stylesheet" href="../styles/spectre-exp.css">
<link rel="stylesheet" href="../styles/index.css">
</head>
<body>
<div id="app"></div>
<script src="../build/bundle.js"></script>
<script src="index.js"></script>
</body>
</html>

2
popup/index.js Normal file
View file

@ -0,0 +1,2 @@
var appTarget = document.getElementById('app');
new linkding.Popup({target: appTarget});

92
rollup.config.js Normal file
View file

@ -0,0 +1,92 @@
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import svelte from 'rollup-plugin-svelte';
import { terser } from 'rollup-plugin-terser';
const production = !process.env.ROLLUP_WATCH;
export default [
// Main bundle (options page)
{
input: 'src/index.js',
output: {
sourcemap: true,
format: 'iife',
name: 'linkding',
file: 'build/bundle.js'
},
plugins: [
svelte({
emitCss: false
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration —
// consult the documentation for details:
// https://github.com/rollup/rollup-plugin-commonjs
resolve({
browser: true,
dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/')
}),
commonjs(),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
},
// Background bundle
{
input: 'src/background.js',
output: {
sourcemap: true,
format: 'iife',
file: 'build/background.js'
},
plugins: [
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration —
// consult the documentation for details:
// https://github.com/rollup/rollup-plugin-commonjs
resolve({ browser: true }),
commonjs(),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
},
// searchInjection bundle
{
input: 'src/searchInjection.js',
output: {
sourcemap: true,
format: 'iife',
file: 'build/searchInjection.js'
},
plugins: [
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration —
// consult the documentation for details:
// https://github.com/rollup/rollup-plugin-commonjs
resolve({ browser: true }),
commonjs(),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
}
];

23
scss/bookmarks.scss Normal file
View file

@ -0,0 +1,23 @@
ul#bookmark-list {
list-style: none;
margin: 0;
padding: 0;
li {
margin-top: 0.4rem;
}
.description {
color: $gray-color;
.dark-bg &,
body[data-dt="1"] & {
color: $gray-color-dark;
}
a,
a:visited:hover {
color: $alternative-color;
}
}
}

105
scss/injectionBox.scss Normal file
View file

@ -0,0 +1,105 @@
div#bookmark-list-container {
padding: 20px 22px;
margin-bottom: 20px;
border-radius: 8px;
background-color: #fff;
color: $primary-color;
// duckduckgo-specific styles
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.06);
border: 1px solid $duckduckgo-border-color;
// .dark-bg is the class duckduckgo globally uses for the dark theme
// data-dt=1 is the attribute of the body tag google uses for the dark theme
.dark-bg &,
body[data-dt="1"] & {
background-color: $duckduckgo-bg;
border: none;
color: $primary-color-dark;
a {
color: $primary-color-dark;
}
#navbar #results_amount span {
color: $alternative-color !important;
}
.tags a {
color: $alternative-color;
}
}
body[data-dt="1"] & {
background-color: $google-bg;
border: 1px solid $google-border-color-dark;
}
&.google {
width: 369px;
box-sizing: border-box;
box-shadow: none;
h1 {
font-size: 0.9em;
}
#navbar #ld-logo {
width: 45%;
}
}
a {
color: $primary-color;
:hover {
color: $link-color-dark;
cursor: pointer;
}
}
.tags a:hover {
text-decoration: none;
cursor: default;
}
#error-message a,
#error-message a:visited {
color: $alternative-color !important;
cursor: pointer;
}
ul > li {
margin-top: 0.4rem;
}
#navbar {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
#ld-logo {
display: flex;
align-items: center;
font-size: 1.2rem;
color: inherit;
h1 {
text-transform: uppercase;
font-size: inherit;
display: inline-block;
margin: 0 0 0 8px;
}
img {
width: 28px;
height: 28px;
}
}
#results_amount {
span {
color: $alternative-color-light-theme;
}
}
}
}

6
scss/theme.scss Normal file
View file

@ -0,0 +1,6 @@
// Import custom variables
@import "variables";
// Import style modules
@import "injectionBox";
@import "bookmarks";

40
scss/variables.scss Normal file
View file

@ -0,0 +1,40 @@
$html-font-size: 18px !default;
$body-bg: #161822 !default;
$bg-color: lighten($body-bg, 5%) !default;
$bg-color-light: lighten($body-bg, 5%) !default;
$border-color: #4c4e53 !default;
$border-color-dark: $border-color !default;
$body-font-color: #b5bec8 !default;
$light-color: #fafafa !default;
$gray-color: #7f879b !default;
$gray-color-dark: lighten($gray-color, 20%) !default;
$primary-color: #a8b1ff !default;
$primary-color-dark: saturate($primary-color, 5%) !default;
$secondary-color: lighten($body-bg, 10%) !default;
$link-color: $primary-color !default;
$link-color-dark: darken($link-color, 5%) !default;
$link-color-light: $link-color !default;
$alternative-color: #59bdb9;
$alternative-color-dark: #73f1eb;
/* Dark theme specific */
$dt-primary-button-color: #5761cb !default;
/* Custom additions to linkding variables */
$alternative-color-light-theme: #05a6a3;
$alternative-color-dark-light-theme: darken($alternative-color, 5%);
$duckduckgo-bg: #282828;
$duckduckgo-border-color: #dfe1e5;
$google-bg: #202124;
$google-border-color-dark: #3c4043;
$primary-color: #5755d9;

48
src/background.js Normal file
View file

@ -0,0 +1,48 @@
import { getBrowser, openOptions } from "./browser";
import { getConfiguration, isConfigurationComplete } from "./configuration";
import { search } from "./linkding";
const browser = getBrowser();
// Connection to search injection content script
let portFromCS;
function connected(p) {
portFromCS = p;
// When the content script sends the search term, search on linkding and
// return results
portFromCS.onMessage.addListener(function (m) {
if (m.action == "openOptions") {
// Open the add on options if the user clicks on the options link in the
// injected box
openOptions();
} else if (isConfigurationComplete() == false) {
portFromCS.postMessage({
message:
"Connection to your linkding instance is not configured yet! " +
"Please configure the extension in the <a class='openOptions'>options</a>.",
});
} else {
let config = getConfiguration();
// Configuration is complete, execute a search on linkding
search(m.searchTerm, { limit: config.resultNum })
.then((results) => {
const bookmarkSuggestions = results.map((bookmark) => ({
url: bookmark.url,
title: bookmark.title || bookmark.website_title || bookmark.url,
description: bookmark.description || bookmark.website_description,
tags: bookmark.tag_names,
date: bookmark.date_modified,
}));
portFromCS.postMessage({ results: bookmarkSuggestions });
})
.catch((error) => {
console.error(error);
});
}
});
}
browser.runtime.onConnect.addListener(connected);

35
src/browser.js Normal file
View file

@ -0,0 +1,35 @@
function isChrome() {
return typeof chrome !== "undefined";
}
export function getBrowser() {
return isChrome() ? chrome : browser;
}
export async function getCurrentTabInfo() {
const tabsPromise = isChrome() ? new Promise(resolve => getBrowser().tabs.query({
active: true,
currentWindow: true
}, resolve)) : getBrowser().tabs.query({ active: true, currentWindow: true });
const tabs = await tabsPromise;
const tab = tabs && tabs[0];
return {
url: tab ? tab.url : "",
title: tab ? tab.title : ""
};
}
export function openOptions() {
getBrowser().runtime.openOptionsPage();
//window.close();
/*
keeping window.close() introduces a bug in chrome if the options page is
opened and closed without saving options. The background script port closes
seemingly indefinitely. The extension needs to be reloaded to fix it.
Since linkding injector does not use the svelte popup from linkding extension
window.close can be safely omitted because there's no window to close
*/
}

20
src/configuration.js Normal file
View file

@ -0,0 +1,20 @@
const CONFIG_KEY = "ld_ext_config";
export function getConfiguration() {
const configJson = localStorage.getItem(CONFIG_KEY);
const config = configJson
? JSON.parse(configJson)
: { baseUrl: "", token: "", resultNum: 10 };
return config;
}
export function saveConfiguration(config) {
const configJson = JSON.stringify(config);
localStorage.setItem(CONFIG_KEY, configJson);
}
export function isConfigurationComplete() {
const config = getConfiguration();
return config.baseUrl && config.token;
}

5
src/index.js Normal file
View file

@ -0,0 +1,5 @@
import Options from "./options.svelte";
export default {
Options,
};

49
src/linkding.js Normal file
View file

@ -0,0 +1,49 @@
import { getConfiguration } from "./configuration";
export async function getTags() {
const configuration = getConfiguration();
return fetch(`${configuration.baseUrl}/api/tags/?limit=1000`, {
headers: {
Authorization: `Token ${configuration.token}`,
},
}).then((response) => {
if (response.status === 200) {
return response.json().then((body) => body.results);
}
return Promise.reject(`Error loading tags: ${response.statusText}`);
});
}
export async function search(text, options) {
const configuration = getConfiguration();
const q = encodeURIComponent(text);
const limit = options.limit || 100;
return fetch(
`${configuration.baseUrl}/api/bookmarks/?q=${q}&limit=${limit}`,
{
headers: {
Authorization: `Token ${configuration.token}`,
},
}
).then((response) => {
if (response.status === 200) {
return response.json().then((body) => body.results);
}
return Promise.reject(`Error searching bookmarks: ${response.statusText}`);
});
}
export async function testConnection(configuration) {
return fetch(`${configuration.baseUrl}/api/bookmarks/?limit=1`, {
headers: {
Authorization: `Token ${configuration.token}`,
},
})
.then((response) =>
response.status === 200 ? response.json() : Promise.reject(response)
)
.then((body) => !!body.results)
.catch(() => false);
}

91
src/options.svelte Normal file
View file

@ -0,0 +1,91 @@
<script>
import { getConfiguration, saveConfiguration } from "./configuration";
import { testConnection } from "./linkding";
let baseUrl = "";
let token = "";
let resultNum = 10;
let isSuccess = false;
let isError = false;
function init() {
const config = getConfiguration();
baseUrl = config.baseUrl;
token = config.token;
resultNum = config.resultNum;
}
init();
async function handleSubmit() {
const config = {
baseUrl,
token,
resultNum
};
const testResult = await testConnection(config);
if (testResult) {
saveConfiguration(config);
isError = false;
isSuccess = true;
} else {
isSuccess = false;
isError = true;
}
}
</script>
<h6>Configuration</h6>
<div class="divider"></div>
<p>This is a companion extension for the <a href="https://github.com/sissbruecker/linkding">linkding</a> bookmark
service. Before you can start using it you have to configure some basic settings, so that the extension can
communicate with your linkding installation.</p>
<form class="form" on:submit|preventDefault={handleSubmit}>
<div class="form-group">
<label class="form-label" for="input-base-url">Base URL</label>
<input class="form-input" type="text" id="input-base-url" placeholder="https://linkding.mydomain.com"
bind:value={baseUrl}>
<div class="form-input-hint">The base URL of your linkding installation, <b>without</b> the <samp>/bookmark</samp> path or a trailing slash</div>
</div>
<div class="form-group">
<label class="form-label" for="input-token">API Authentication Token</label>
<input class="form-input" type="password" id="input-token" placeholder="Token" bind:value={token}>
<div class="form-input-hint">Used to authenticate against the linkding API. You can find this on your linkding
settings page.
</div>
</div>
<div class="form-group">
<label class="form-label" for="input-token">Maximum number of search results</label>
<input class="form-input" type="mi,ner" id="input-search-num" placeholder="10" bind:value={resultNum}>
<div class="form-input-hint">The maximum number of search results. High numbers could lead to worse performance.
</div>
</div>
<div class="divider"></div>
<div class="button-row">
{#if isSuccess}
<div class="form-group has-success mr-2">
<span class="form-input-hint"><i class="icon icon-check"></i> Connection successful</span>
</div>
{/if}
{#if isError}
<div class="form-group has-error mr-2">
<span class="form-input-hint"><i class="icon icon-cross"></i> Connection failed</span>
</div>
{/if}
<button type="submit" class="btn btn-primary ml-2" disabled={!(baseUrl && token)}>
Save
</button>
</div>
</form>
<style>
.button-row {
display: flex;
justify-content: flex-end;
align-items: baseline;
}
.button-row button {
padding-left: 32px;
padding-right: 32px;
}
</style>

133
src/searchInjection.js Normal file
View file

@ -0,0 +1,133 @@
function isChrome() {
return typeof chrome !== "undefined";
}
function getBrowser() {
return isChrome() ? chrome : browser;
}
/* Sanitise input to prevent unwanted injection of html or even javascript
through linkding search results, e.g. in the bookmark title or description
*/
function escapeHTML(str) {
let p = document.createElement("p");
p.appendChild(document.createTextNode(str));
return p.innerHTML;
}
const browser = getBrowser();
let port = browser.runtime.connect({ name: "port-from-cs" });
let searchEngine;
if (document.location.hostname.match(/duckduckgo/)) {
searchEngine = "duckduckgo";
} else if (document.location.hostname.match(/google/)) {
searchEngine = "google";
}
// When background script answers with results, construct html for the result box
port.onMessage.addListener(function (m) {
let sidebar, html;
// In case we don't get results, but a message from the background script,
// display it. This is the case before proper configuration
if ("message" in m) {
html = `
<div id="bookmark-list-container" class="${searchEngine}">
<div id="navbar">
<a id="ld-logo">
<img src=${browser.runtime.getURL("icons/logo.svg")} />
<h1>linkding injector</h1>
</a>
<a id="ld-options" class="openOptions">
<img class="ld-settings" src=${browser.runtime.getURL(
"icons/cog.svg"
)} />
</a>
</div>
<div id="error-message">
${m.message}
</div>
</div>
`;
}
// If there is no message and there are actual results display them
else if (m.results.length > 0) {
html = `
<div id="bookmark-list-container" class="${searchEngine}">
<div id="navbar">
<a id="ld-logo">
<img src=${browser.runtime.getURL("icons/logo.svg")} />
<h1>linkding injector</h1>
</a>
<div id="results_amount">
Found <span>${m.results.length}</span> ${
m.results.length == 1 ? "result" : "results"
}.
</div>
<a id="ld-options" class="openOptions">
<img class="ld-settings" src=${browser.runtime.getURL(
"icons/cog.svg"
)} />
</a>
</div>
`;
html += `<ul id="bookmark-list">`;
m.results.forEach((bookmark) => {
html += `
<li>
<div class="title">
<a
href="${bookmark.url}"
target="_blank"
rel="noopener"
>${escapeHTML(bookmark.title)}</a
>
</div>
<div class="description">
<span class="tags">
${bookmark.tags.map((tag) => {
return "<a>#" + escapeHTML(tag) + "</a>";
}).join(" ")}
</a>
</span>
${bookmark.tags.length > 0 ? "|" : ""}
<span>
${escapeHTML(bookmark.description)}
</span>
</div>
</li>`;
});
html += `</ul></div>`;
} else {
console.error("linkding injector: no message and no search results");
}
// querySelectors for finding the sidebar in the search engine websites
if (searchEngine == "duckduckgo") {
sidebar = document.querySelector(".sidebar-modules");
} else if (searchEngine == "google") {
sidebar = document.querySelector("#rhs");
}
// The actual injection
sidebar.insertAdjacentHTML("afterbegin", html);
// Event listeners for opening the extension options. These can only be opened
// by the background script, so we need to send a message to it
document.querySelectorAll(".openOptions").forEach((el) => {
el.addEventListener("click", () => {
port.postMessage({ action: "openOptions" });
});
});
});
// Start the search by sending a message to background.js with the search term
let queryString = location.search;
let urlParams = new URLSearchParams(queryString);
let searchTerm = urlParams.get("q");
port.postMessage({ searchTerm: searchTerm });

4
styles/index.css Normal file
View file

@ -0,0 +1,4 @@
body {
padding: 16px;
min-width: 400px;
}

1231
styles/spectre-exp.css Normal file

File diff suppressed because it is too large Load diff

597
styles/spectre-icons.css Normal file
View file

@ -0,0 +1,597 @@
/*! Spectre.css Icons v0.5.9 | MIT License | github.com/picturepan2/spectre */
.icon {
box-sizing: border-box;
display: inline-block;
font-size: inherit;
font-style: normal;
height: 1em;
position: relative;
text-indent: -9999px;
vertical-align: middle;
width: 1em;
}
.icon::before,
.icon::after {
content: "";
display: block;
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.icon.icon-2x {
font-size: 1.6rem;
}
.icon.icon-3x {
font-size: 2.4rem;
}
.icon.icon-4x {
font-size: 3.2rem;
}
.accordion .icon,
.btn .icon,
.toast .icon,
.menu .icon {
vertical-align: -10%;
}
.btn-lg .icon {
vertical-align: -15%;
}
.icon-arrow-down::before,
.icon-arrow-left::before,
.icon-arrow-right::before,
.icon-arrow-up::before,
.icon-downward::before,
.icon-back::before,
.icon-forward::before,
.icon-upward::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .65em;
width: .65em;
}
.icon-arrow-down::before {
transform: translate(-50%, -75%) rotate(225deg);
}
.icon-arrow-left::before {
transform: translate(-25%, -50%) rotate(-45deg);
}
.icon-arrow-right::before {
transform: translate(-75%, -50%) rotate(135deg);
}
.icon-arrow-up::before {
transform: translate(-50%, -25%) rotate(45deg);
}
.icon-back::after,
.icon-forward::after {
background: currentColor;
height: .1rem;
width: .8em;
}
.icon-downward::after,
.icon-upward::after {
background: currentColor;
height: .8em;
width: .1rem;
}
.icon-back::after {
left: 55%;
}
.icon-back::before {
transform: translate(-50%, -50%) rotate(-45deg);
}
.icon-downward::after {
top: 45%;
}
.icon-downward::before {
transform: translate(-50%, -50%) rotate(-135deg);
}
.icon-forward::after {
left: 45%;
}
.icon-forward::before {
transform: translate(-50%, -50%) rotate(135deg);
}
.icon-upward::after {
top: 55%;
}
.icon-upward::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon-caret::before {
border-left: .3em solid transparent;
border-right: .3em solid transparent;
border-top: .3em solid currentColor;
height: 0;
transform: translate(-50%, -25%);
width: 0;
}
.icon-menu::before {
background: currentColor;
box-shadow: 0 -.35em, 0 .35em;
height: .1rem;
width: 100%;
}
.icon-apps::before {
background: currentColor;
box-shadow: -.35em -.35em, -.35em 0, -.35em .35em, 0 -.35em, 0 .35em, .35em -.35em, .35em 0, .35em .35em;
height: 3px;
width: 3px;
}
.icon-resize-horiz::before,
.icon-resize-horiz::after,
.icon-resize-vert::before,
.icon-resize-vert::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .45em;
width: .45em;
}
.icon-resize-horiz::before,
.icon-resize-vert::before {
transform: translate(-50%, -90%) rotate(45deg);
}
.icon-resize-horiz::after,
.icon-resize-vert::after {
transform: translate(-50%, -10%) rotate(225deg);
}
.icon-resize-horiz::before {
transform: translate(-90%, -50%) rotate(-45deg);
}
.icon-resize-horiz::after {
transform: translate(-10%, -50%) rotate(135deg);
}
.icon-more-horiz::before,
.icon-more-vert::before {
background: currentColor;
border-radius: 50%;
box-shadow: -.4em 0, .4em 0;
height: 3px;
width: 3px;
}
.icon-more-vert::before {
box-shadow: 0 -.4em, 0 .4em;
}
.icon-plus::before,
.icon-minus::before,
.icon-cross::before {
background: currentColor;
height: .1rem;
width: 100%;
}
.icon-plus::after,
.icon-cross::after {
background: currentColor;
height: 100%;
width: .1rem;
}
.icon-cross::before {
width: 100%;
}
.icon-cross::after {
height: 100%;
}
.icon-cross::before,
.icon-cross::after {
transform: translate(-50%, -50%) rotate(45deg);
}
.icon-check::before {
border: .1rem solid currentColor;
border-right: 0;
border-top: 0;
height: .5em;
transform: translate(-50%, -75%) rotate(-45deg);
width: .9em;
}
.icon-stop {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-stop::before {
background: currentColor;
height: .1rem;
transform: translate(-50%, -50%) rotate(45deg);
width: 1em;
}
.icon-shutdown {
border: .1rem solid currentColor;
border-radius: 50%;
border-top-color: transparent;
}
.icon-shutdown::before {
background: currentColor;
content: "";
height: .5em;
top: .1em;
width: .1rem;
}
.icon-refresh::before {
border: .1rem solid currentColor;
border-radius: 50%;
border-right-color: transparent;
height: 1em;
width: 1em;
}
.icon-refresh::after {
border: .2em solid currentColor;
border-left-color: transparent;
border-top-color: transparent;
height: 0;
left: 80%;
top: 20%;
width: 0;
}
.icon-search::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .75em;
left: 5%;
top: 5%;
transform: translate(0, 0) rotate(45deg);
width: .75em;
}
.icon-search::after {
background: currentColor;
height: .1rem;
left: 80%;
top: 80%;
transform: translate(-50%, -50%) rotate(45deg);
width: .4em;
}
.icon-edit::before {
border: .1rem solid currentColor;
height: .4em;
transform: translate(-40%, -60%) rotate(-45deg);
width: .85em;
}
.icon-edit::after {
border: .15em solid currentColor;
border-right-color: transparent;
border-top-color: transparent;
height: 0;
left: 5%;
top: 95%;
transform: translate(0, -100%);
width: 0;
}
.icon-delete::before {
border: .1rem solid currentColor;
border-bottom-left-radius: .1rem;
border-bottom-right-radius: .1rem;
border-top: 0;
height: .75em;
top: 60%;
width: .75em;
}
.icon-delete::after {
background: currentColor;
box-shadow: -.25em .2em, .25em .2em;
height: .1rem;
top: .05rem;
width: .5em;
}
.icon-share {
border: .1rem solid currentColor;
border-radius: .1rem;
border-right: 0;
border-top: 0;
}
.icon-share::before {
border: .1rem solid currentColor;
border-left: 0;
border-top: 0;
height: .4em;
left: 100%;
top: .25em;
transform: translate(-125%, -50%) rotate(-45deg);
width: .4em;
}
.icon-share::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: 75% 0;
border-right: 0;
height: .5em;
width: .6em;
}
.icon-flag::before {
background: currentColor;
height: 1em;
left: 15%;
width: .1rem;
}
.icon-flag::after {
border: .1rem solid currentColor;
border-bottom-right-radius: .1rem;
border-left: 0;
border-top-right-radius: .1rem;
height: .65em;
left: 60%;
top: 35%;
width: .8em;
}
.icon-bookmark::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-top-left-radius: .1rem;
border-top-right-radius: .1rem;
height: .9em;
width: .8em;
}
.icon-bookmark::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-left: 0;
border-radius: .1rem;
height: .5em;
transform: translate(-50%, 35%) rotate(-45deg) skew(15deg, 15deg);
width: .5em;
}
.icon-download,
.icon-upload {
border-bottom: .1rem solid currentColor;
}
.icon-download::before,
.icon-upload::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-right: 0;
height: .5em;
transform: translate(-50%, -60%) rotate(-135deg);
width: .5em;
}
.icon-download::after,
.icon-upload::after {
background: currentColor;
height: .6em;
top: 40%;
width: .1rem;
}
.icon-upload::before {
transform: translate(-50%, -60%) rotate(45deg);
}
.icon-upload::after {
top: 50%;
}
.icon-copy::before {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: .1rem;
border-right: 0;
height: .8em;
left: 40%;
top: 35%;
width: .8em;
}
.icon-copy::after {
border: .1rem solid currentColor;
border-radius: .1rem;
height: .8em;
left: 60%;
top: 60%;
width: .8em;
}
.icon-time {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-time::before {
background: currentColor;
height: .4em;
transform: translate(-50%, -75%);
width: .1rem;
}
.icon-time::after {
background: currentColor;
height: .3em;
transform: translate(-50%, -75%) rotate(90deg);
transform-origin: 50% 90%;
width: .1rem;
}
.icon-mail::before {
border: .1rem solid currentColor;
border-radius: .1rem;
height: .8em;
width: 1em;
}
.icon-mail::after {
border: .1rem solid currentColor;
border-right: 0;
border-top: 0;
height: .5em;
transform: translate(-50%, -90%) rotate(-45deg) skew(10deg, 10deg);
width: .5em;
}
.icon-people::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .45em;
top: 25%;
width: .45em;
}
.icon-people::after {
border: .1rem solid currentColor;
border-radius: 50% 50% 0 0;
height: .4em;
top: 75%;
width: .9em;
}
.icon-message {
border: .1rem solid currentColor;
border-bottom: 0;
border-radius: .1rem;
border-right: 0;
}
.icon-message::before {
border: .1rem solid currentColor;
border-bottom-right-radius: .1rem;
border-left: 0;
border-top: 0;
height: .8em;
left: 65%;
top: 40%;
width: .7em;
}
.icon-message::after {
background: currentColor;
border-radius: .1rem;
height: .3em;
left: 10%;
top: 100%;
transform: translate(0, -90%) rotate(45deg);
width: .1rem;
}
.icon-photo {
border: .1rem solid currentColor;
border-radius: .1rem;
}
.icon-photo::before {
border: .1rem solid currentColor;
border-radius: 50%;
height: .25em;
left: 35%;
top: 35%;
width: .25em;
}
.icon-photo::after {
border: .1rem solid currentColor;
border-bottom: 0;
border-left: 0;
height: .5em;
left: 60%;
transform: translate(-50%, 25%) rotate(-45deg);
width: .5em;
}
.icon-link::before,
.icon-link::after {
border: .1rem solid currentColor;
border-radius: 5em 0 0 5em;
border-right: 0;
height: .5em;
width: .75em;
}
.icon-link::before {
transform: translate(-70%, -45%) rotate(-45deg);
}
.icon-link::after {
transform: translate(-30%, -55%) rotate(135deg);
}
.icon-location::before {
border: .1rem solid currentColor;
border-radius: 50% 50% 50% 0;
height: .8em;
transform: translate(-50%, -60%) rotate(-45deg);
width: .8em;
}
.icon-location::after {
border: .1rem solid currentColor;
border-radius: 50%;
height: .2em;
transform: translate(-50%, -80%);
width: .2em;
}
.icon-emoji {
border: .1rem solid currentColor;
border-radius: 50%;
}
.icon-emoji::before {
border-radius: 50%;
box-shadow: -.17em -.1em, .17em -.1em;
height: .15em;
width: .15em;
}
.icon-emoji::after {
border: .1rem solid currentColor;
border-bottom-color: transparent;
border-radius: 50%;
border-right-color: transparent;
height: .5em;
transform: translate(-50%, -40%) rotate(-135deg);
width: .5em;
}

3760
styles/spectre.css Normal file

File diff suppressed because it is too large Load diff