├── .github └── workflows │ ├── pr-packager.yml │ ├── release.yml │ └── test-plugin.yml ├── .gitignore ├── README.md ├── SettingsTemplate.yaml ├── icon.png ├── plugin.json ├── plugin ├── main.py ├── raindrop.py └── results.py ├── requirements.in ├── requirements.txt └── run.py /.github/workflows/pr-packager.yml: -------------------------------------------------------------------------------- 1 | name: "PR-Packager" 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | env: 6 | PYTHON_VER: 3.8 7 | jobs: 8 | deps: 9 | name: "Build" 10 | runs-on: windows-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Set up Python ${{ env.PYTHON_VER }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ env.PYTHON_VER }} 18 | - uses: actions/cache@v2 19 | with: 20 | path: ~\AppData\Local\pip\Cache 21 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install wheel 28 | pip install -r ./requirements.txt -t ./lib 29 | - name: Upload 30 | uses: actions/upload-artifact@v2 31 | with: 32 | name: artifact 33 | path: | 34 | ./** 35 | !./.git/ 36 | !./README.md/ 37 | !./.github/ 38 | !./assets/ -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release-Builder" 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | tags-ignore: 7 | - 'v*' 8 | paths-ignore: 9 | - .github/workflows/* 10 | - README.md 11 | - assets/* 12 | env: 13 | PYTHON_VER: 3.11 14 | jobs: 15 | deps: 16 | if: ${{ github.ref == 'refs/heads/main' }} 17 | name: "Build" 18 | runs-on: windows-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 24 | - name: Set up Python ${{ env.PYTHON_VER }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ env.PYTHON_VER }} 28 | - uses: actions/cache@v2 29 | if: startsWith(runner.os, 'Windows') 30 | with: 31 | path: ~\AppData\Local\pip\Cache 32 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pip- 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install wheel 39 | pip install -r ./requirements.txt -t ./lib 40 | - name: Get Plugin's version 41 | id: version 42 | uses: notiz-dev/github-action-json-property@release 43 | with: 44 | path: 'plugin.json' 45 | prop_path: 'Version' 46 | - name: Package files 47 | run: | 48 | git clone https://github.com/Garulf/flow_commands/ bin 49 | pip install -r ./bin/requirements.txt 50 | python ./bin/commands.py package -n "${{github.event.repository.name}}.zip" 51 | - name: Publish 52 | uses: softprops/action-gh-release@v1 53 | with: 54 | draft: false 55 | files: "./${{github.event.repository.name}}.zip" 56 | tag_name: "v${{steps.version.outputs.prop}}" 57 | generate_release_notes: true 58 | env: 59 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/test-plugin.yml: -------------------------------------------------------------------------------- 1 | name: "Test Plugin" 2 | on: 3 | pull_request: 4 | workflow_dispatch: 5 | push: 6 | tags-ignore: 7 | - 'v*' 8 | paths-ignore: 9 | - .github/workflows/* 10 | - README.md 11 | - assets/* 12 | env: 13 | PYTHON_VER: 3.8 14 | BRANCH: 'main' 15 | jobs: 16 | test_run: 17 | name: "Test Run" 18 | runs-on: windows-latest 19 | strategy: 20 | matrix: 21 | flow_tags: ['latest'] 22 | python_ver: ['3.8'] 23 | steps: 24 | - name: Checkout Plugin Repo 25 | uses: actions/checkout@v2 26 | with: 27 | path: ${{github.event.repository.name}} 28 | - name: Get Plugin's version 29 | if: ${{ github.ref != 'refs/heads/main' }} 30 | id: version 31 | uses: notiz-dev/github-action-json-property@release 32 | with: 33 | path: './${{github.event.repository.name}}/plugin.json' 34 | prop_path: 'Version' 35 | - name: Assert Version updated 36 | if: ${{ github.ref != 'refs/heads/main' }} 37 | run: | 38 | $release_ver = (Invoke-WebRequest -Uri "https://raw.githubusercontent.com/${{github.repository}}/main/plugin.json" | ConvertFrom-Json).Version 39 | $release_ver = $release_ver -replace "v", "" 40 | $this_ver = "${{steps.version.outputs.prop}}" 41 | echo "This version:" $this_ver 42 | echo "Release version:" $release_ver 43 | if ([System.Version]$this_ver -gt [System.Version]$release_ver) { 44 | exit 0 45 | } else { 46 | exit 1 47 | } 48 | - name: Get latest Version tag 49 | run: | 50 | if ("${{matrix.flow_tags}}" -eq 'latest') { 51 | $url = "https://api.github.com/repos/Flow-Launcher/Flow.Launcher/releases/latest" 52 | } else { 53 | $url = "https://api.github.com/repos/Flow-Launcher/Flow.Launcher/releases/tags/${{matrix.flow_tags}}" 54 | } 55 | $release = Invoke-WebRequest -Uri $url | ConvertFrom-Json 56 | $tag_name = $release.tag_name 57 | foreach ($asset in $release.assets) 58 | { 59 | if($asset.name -like '*setup.exe') { 60 | $download_url = $asset.browser_download_url 61 | $file_name = $asset.name 62 | break 63 | } 64 | } 65 | echo "DOWNLOAD_URL=$download_url" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 66 | echo "FILE_NAME=$file_name" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 67 | echo "TAG_NAME=$tag_name" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 68 | - name: Flow Launcher Cache 69 | uses: actions/cache@v2 70 | id: flow_cache 71 | with: 72 | path: | 73 | ~\AppData\Roaming\FlowLauncher\* 74 | !~\AppData\Roaming\FlowLauncher\Plugins\* 75 | ~\AppData\Local\FlowLauncher\** 76 | key: ${{ runner.os }}-flow-${{ env.TAG_NAME }} 77 | - name: Download Flow Launcher 78 | id: download 79 | if: steps.flow_cache.outputs.cache-hit != 'true' 80 | run: | 81 | curl.exe -L -o ${{ env.FILE_NAME }} ${{ env.DOWNLOAD_URL }} 82 | - name: Install Flow Launcher 83 | if: steps.flow_cache.outputs.cache-hit != 'true' 84 | run: .\${{ env.FILE_NAME }} 85 | shell: cmd 86 | - name: Move Plugin to plugins directory 87 | run: | 88 | $repo_path = Join-Path -Path $pwd -ChildPath ${{github.event.repository.name}} 89 | $plugin_path = Join-Path -Path $env:APPDATA -ChildPath 'FlowLauncher' | Join-Path -ChildPath 'Plugins' | Join-Path -ChildPath ${{github.event.repository.name}} 90 | if (Test-Path $plugin_path) 91 | { 92 | echo "Removing cached directory" 93 | Remove-Item $plugin_path 94 | } 95 | New-Item -ItemType SymbolicLink -Path $plugin_path -Target $repo_path 96 | echo "PLUGIN_PATH=$plugin_path" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 97 | - name: Set up Python 98 | uses: actions/setup-python@v2 99 | with: 100 | python-version: ${{ matrix.python_ver }} 101 | - uses: actions/cache@v2 102 | with: 103 | path: ~\AppData\Local\pip\Cache 104 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 105 | restore-keys: | 106 | ${{ runner.os }}-pip-${{ matrix.python_ver }} 107 | - name: Install dependencies 108 | run: | 109 | cd ${{ env.PLUGIN_PATH }} 110 | python -m pip install --upgrade pip 111 | pip install wheel 112 | pip install -r ./requirements.txt -t ./lib 113 | - name: Get Plugin's Execute file 114 | id: exe 115 | uses: notiz-dev/github-action-json-property@release 116 | with: 117 | path: '${{ env.PLUGIN_PATH }}/plugin.json' 118 | prop_path: 'ExecuteFileName' 119 | - name: Test Run 120 | run: | 121 | cd ${{ env.PLUGIN_PATH }} 122 | python ${{steps.exe.outputs.prop}} 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | 116 | # Flox library folder 117 | flox 118 | 119 | # VScode 120 | 121 | .vscode/ 122 | bin/ 123 | 124 | .history/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flow-Raindrop 2 | Access Raindrop.io bookmarks with Flow Launcher/Wox 3 | 4 | ## 5 | Buy Me A Coffee 6 | -------------------------------------------------------------------------------- /SettingsTemplate.yaml: -------------------------------------------------------------------------------- 1 | body: 2 | - type: input 3 | attributes: 4 | name: api_token 5 | label: 'API Token:' 6 | defaultValue: "" 7 | - type: dropdown 8 | attributes: 9 | name: cache_ttl 10 | label: 'Cache duration:' 11 | defaultValue: '5 minutes' 12 | options: 13 | - '5 minutes' 14 | - '15 minutes' 15 | - '30 minutes' 16 | - '1 hour' 17 | - '6 hours' 18 | - '12 hours' 19 | - '1 day' 20 | - '3 days' 21 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Garulf/Flow-Raindrop/8fdd00868936039db88f094ab8b987c6c3b169aa/icon.png -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "ID": "B9B8174812E4474D96C148317BE2BFB7", 3 | "ActionKeyword": "rd", 4 | "Name": "FlowRaindrop", 5 | "Description": "Access Raindrop.io bookmarks with Flow Launcher/Wox", 6 | "Author": "Garulf", 7 | "Version": "1.0.0", 8 | "Language": "python", 9 | "Website": "https://github.com/Garulf/flow-raindrop", 10 | "IcoPath": "./icon.png", 11 | "ExecuteFileName": "run.py" 12 | } -------------------------------------------------------------------------------- /plugin/main.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from typing import List 3 | 4 | from pyflowlauncher import Plugin, ResultResponse, send_results 5 | from pyflowlauncher.settings import settings 6 | from raindrop import Raindrop 7 | 8 | from results import query_results, context_menu_results, init_results 9 | 10 | 11 | CACHE_TTL = { 12 | "5 minutes": 300, 13 | "15 minutes": 900, 14 | "30 minutes": 1800, 15 | "1 hour": 3600, 16 | "6 hours": 21600, 17 | "12 hours": 43200, 18 | "1 day": 86400, 19 | "3 days": 259200, 20 | } 21 | 22 | DEFAULT_CACHE_TTL = CACHE_TTL["5 minutes"] 23 | 24 | 25 | plugin = Plugin() 26 | 27 | 28 | @plugin.on_method 29 | def query(query: str) -> ResultResponse: 30 | api_token = settings().get('api_token') 31 | cache_ttl = settings().get('cache_ttl', DEFAULT_CACHE_TTL) 32 | if not api_token: 33 | return send_results([init_results()]) 34 | if not query: 35 | return send_results([]) 36 | rd = Raindrop(api_token, CACHE_TTL[cache_ttl]) 37 | return send_results( 38 | query_results(rd, query) 39 | ) 40 | 41 | 42 | @plugin.on_method 43 | def context_menu(data: List) -> ResultResponse: 44 | return send_results(context_menu_results()) 45 | 46 | 47 | @plugin.on_method 48 | def clear_cache() -> None: 49 | shutil.rmtree('.cache') 50 | -------------------------------------------------------------------------------- /plugin/raindrop.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Dict, Generator, List, TypedDict 3 | 4 | import hishel 5 | 6 | 7 | BASE_URL = 'https://api.raindrop.io/rest/v1' 8 | DEFAULT_CACHE_TTL = 300 9 | 10 | 11 | class CollectionRef(Enum): 12 | ALL = 0 13 | UNSORTED = -1 14 | TRASH = -99 15 | 16 | 17 | class Item(TypedDict): 18 | _id: int 19 | excerpt: str 20 | note: str 21 | type: str 22 | cover: str 23 | tags: list 24 | removed: bool 25 | title: str 26 | collection: Dict 27 | link: str 28 | created: str 29 | lastUpdate: str 30 | important: bool 31 | media: Dict 32 | user: Dict 33 | highlights: List 34 | domain: str 35 | creatorRef: Dict 36 | sort: int 37 | collectionId: int 38 | highlight: Dict 39 | 40 | 41 | class Raindrop: 42 | 43 | def __init__(self, api_token: str, cache_ttl: int = DEFAULT_CACHE_TTL): 44 | self._api_token = api_token 45 | self._cache_ttl = cache_ttl 46 | 47 | def _request(self, method: str, endpoint: str, **kwargs) -> Dict: 48 | storage = hishel.FileStorage(ttl=self._cache_ttl) 49 | with hishel.CacheClient(storage=storage) as client: 50 | response = client.request( 51 | method, 52 | f'{BASE_URL}/{endpoint}', 53 | headers={'Authorization': f"Bearer {self._api_token}"}, 54 | extensions={"force_cache": True}, 55 | **kwargs 56 | ) 57 | response.raise_for_status() 58 | return response.json() 59 | 60 | def _search( 61 | self, search: str, 62 | collection: CollectionRef = CollectionRef.ALL 63 | ) -> Dict: 64 | return self._request( 65 | 'GET', 66 | f'raindrops/{collection.value}', 67 | params={ 68 | 'search': search 69 | } 70 | ) 71 | 72 | def search( 73 | self, search: str, 74 | collection: CollectionRef = CollectionRef.ALL 75 | ) -> Generator[Item, None, None]: 76 | yield from self._search(search, collection)['items'] 77 | -------------------------------------------------------------------------------- /plugin/results.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import TYPE_CHECKING, Generator 3 | 4 | from pyflowlauncher import Result 5 | from pyflowlauncher.api import open_url, open_setting_dialog 6 | from pyflowlauncher.icons import RECYCLEBIN, SETTINGS, LINK 7 | 8 | 9 | if TYPE_CHECKING: 10 | from raindrop import Raindrop, Item 11 | 12 | 13 | def init_results() -> Result: 14 | return Result( 15 | Title="No API token found!", 16 | SubTitle="Please enter your Raindrop.io API token in plugin settings.", 17 | IcoPath=SETTINGS, 18 | JsonRPCAction=open_setting_dialog() 19 | ) 20 | 21 | 22 | def query_result(item: Item) -> Result: 23 | return Result( 24 | Title=item["title"], 25 | SubTitle=item["excerpt"], 26 | IcoPath=item["cover"], 27 | ContextData=[], 28 | JsonRPCAction=open_url(item["link"]) 29 | ) 30 | 31 | 32 | def query_results( 33 | raindrop: Raindrop, 34 | query: str 35 | ) -> Generator[Result, None, None]: 36 | search = raindrop.search(query) 37 | for item in search: 38 | yield query_result(item) 39 | 40 | 41 | def context_menu_results() -> Generator[Result, None, None]: 42 | yield Result( 43 | Title="Open Raindrop.io", 44 | SubTitle="Open Raindrop.io in your browser", 45 | IcoPath=LINK, 46 | JsonRPCAction=open_url("https://raindrop.io") 47 | ) 48 | yield Result( 49 | Title="Clear cached data", 50 | SubTitle="All cached data will be removed", 51 | IcoPath=RECYCLEBIN, 52 | JsonRPCAction={ 53 | "method": "clear_cache", 54 | "parameters": [], 55 | } 56 | ) 57 | -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | pyflowlauncher[all] 2 | hishel -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyflowlauncher[all]==0.8.3.dev0 2 | hishel==0.0.24 -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | plugindir = os.path.abspath(os.path.dirname(__file__)) 5 | sys.path.append(plugindir) 6 | sys.path.append(os.path.join(plugindir, "lib")) 7 | sys.path.append(os.path.join(plugindir, "plugin")) 8 | 9 | 10 | if __name__ == "__main__": 11 | from plugin.main import plugin 12 | plugin.run() 13 | --------------------------------------------------------------------------------