Skip to content

Commit 415c53f

Browse files
committed
Prepare for Chrome Web Store: 0.3.0, build script, release workflow
- Bump manifest to 0.3.0 - Add build.sh to package a clean dist/immich-companion-<version>.zip with only runtime files (no Markdown, no Python, no .DS_Store) - Add .github/workflows/release.yml: tag v*.*.* triggers a build and attaches the zip to a GitHub Release. Optionally publishes to the Chrome Web Store when WEBSTORE_* secrets are configured. - PRIVACY.md: explicit section on the Google search-page integration — no Immich data ever travels through Google's network; only the injected DOM contains a server hostname and asset UUIDs in link hrefs. - README.md: link the new feature, document publishing and the optional Web Store auto-upload secrets. - STORE_LISTING.md: complete listing copy (name, summary, detailed description, single-purpose statement, per-permission justifications) and three options for hosting the public privacy policy URL.
1 parent 29a3ff1 commit 415c53f

7 files changed

Lines changed: 364 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Release
2+
3+
# Triggered when you push a tag like v0.3.0. Builds the extension zip and
4+
# attaches it to a GitHub Release. If the WEBSTORE_* secrets are set, also
5+
# uploads the zip to the Chrome Web Store.
6+
7+
on:
8+
push:
9+
tags:
10+
- 'v*.*.*'
11+
workflow_dispatch: # allow manual runs from the Actions tab
12+
inputs:
13+
publish_to_webstore:
14+
description: 'Also publish to Chrome Web Store (requires secrets)'
15+
type: boolean
16+
default: false
17+
18+
permissions:
19+
contents: write # to create the GitHub Release
20+
21+
jobs:
22+
build:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Read version from manifest.json
28+
id: meta
29+
run: |
30+
VERSION=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
31+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
32+
echo "zip=immich-companion-${VERSION}.zip" >> "$GITHUB_OUTPUT"
33+
34+
- name: Verify tag matches manifest version
35+
if: startsWith(github.ref, 'refs/tags/v')
36+
run: |
37+
TAG="${GITHUB_REF#refs/tags/v}"
38+
if [ "$TAG" != "${{ steps.meta.outputs.version }}" ]; then
39+
echo "Tag v$TAG does not match manifest version ${{ steps.meta.outputs.version }}" >&2
40+
exit 1
41+
fi
42+
43+
- name: Build zip
44+
run: |
45+
chmod +x build.sh
46+
./build.sh
47+
48+
- name: Upload artifact
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: ${{ steps.meta.outputs.zip }}
52+
path: dist/${{ steps.meta.outputs.zip }}
53+
if-no-files-found: error
54+
55+
- name: Create GitHub Release
56+
if: startsWith(github.ref, 'refs/tags/v')
57+
uses: softprops/action-gh-release@v2
58+
with:
59+
files: dist/${{ steps.meta.outputs.zip }}
60+
generate_release_notes: true
61+
62+
# ----------------------------------------------------------------
63+
# Optional: publish to Chrome Web Store. Skipped unless the four
64+
# secrets below are set in the repo (Settings → Secrets → Actions):
65+
# WEBSTORE_EXTENSION_ID — printed in the dashboard after the
66+
# first manual upload
67+
# WEBSTORE_CLIENT_ID — Google Cloud OAuth client ID
68+
# WEBSTORE_CLIENT_SECRET — Google Cloud OAuth client secret
69+
# WEBSTORE_REFRESH_TOKEN — long-lived OAuth refresh token
70+
# See README.md "Publishing" for how to obtain these.
71+
# ----------------------------------------------------------------
72+
- name: Publish to Chrome Web Store
73+
if: >-
74+
(startsWith(github.ref, 'refs/tags/v') || inputs.publish_to_webstore == true)
75+
&& env.WEBSTORE_EXTENSION_ID != ''
76+
env:
77+
WEBSTORE_EXTENSION_ID: ${{ secrets.WEBSTORE_EXTENSION_ID }}
78+
WEBSTORE_CLIENT_ID: ${{ secrets.WEBSTORE_CLIENT_ID }}
79+
WEBSTORE_CLIENT_SECRET: ${{ secrets.WEBSTORE_CLIENT_SECRET }}
80+
WEBSTORE_REFRESH_TOKEN: ${{ secrets.WEBSTORE_REFRESH_TOKEN }}
81+
uses: mnao305/chrome-extension-upload@v5.0.0
82+
with:
83+
file-path: dist/${{ steps.meta.outputs.zip }}
84+
extension-id: ${{ env.WEBSTORE_EXTENSION_ID }}
85+
client-id: ${{ env.WEBSTORE_CLIENT_ID }}
86+
client-secret: ${{ env.WEBSTORE_CLIENT_SECRET }}
87+
refresh-token: ${{ env.WEBSTORE_REFRESH_TOKEN }}
88+
publish: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.DS_Store
22
Thumbs.db
33
*.zip
4+
dist/
45
node_modules/
56
__pycache__/
67
*.pyc

PRIVACY.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,29 @@ The extension talks **only to the Immich server URL you configure**. Specificall
2828
- No advertising or tracking SDKs.
2929
- No browsing history is read or sent anywhere.
3030

31+
## Google search integration
32+
33+
The extension has an optional feature ("Show Immich matches on Google", on by default, toggleable in Settings → Features) that displays an inline result card on Google search pages with photos from your library that match the query.
34+
35+
**No Immich data ever travels through Google's network.** When you search Google, the extension's content script reads your query from the URL and asks the extension's background service worker to perform a CLIP smart search against your Immich server. The HTTP request goes directly from the background service worker to your Immich server — Google's servers are not involved.
36+
37+
**What appears in Google's page DOM** (theoretically inspectable by their JavaScript, though their analytics do not in practice scrape arbitrary DOM nodes injected by extensions):
38+
39+
- The HTML of the result card.
40+
- Each thumbnail as a `blob:` URL — these reference image bytes held only in your browser's memory, not transmitted anywhere.
41+
- A "View all in Immich" link with your Immich server URL in the `href`.
42+
- Each thumbnail link with the Immich asset UUID in the `href`.
43+
44+
**What does *not* appear in Google's page or travel to Google:**
45+
46+
- Your API key. It stays in `chrome.storage.local` and is only added to the request header inside the background service worker.
47+
- Image bytes, file names, EXIF, location, dates, or any other metadata.
48+
- Any network traffic to your Immich server.
49+
50+
**Worst-case theoretical exposure**, if Google were to actively scrape extension-injected DOM (which they don't): your Immich server's hostname and the opaque UUIDs of matching assets. Nothing more.
51+
52+
To turn the feature off entirely (no requests fired on Google pages, no DOM injection): toggle **Show Immich matches on Google** off in Settings → Features.
53+
3154
## Required browser permissions
3255

3356
| Permission | Reason |

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ A Chrome / Chromium extension that connects to your self-hosted [Immich](https:/
77
- **Omnibox search** — type `im` + space + a query in the address bar for live CLIP-powered smart search.
88
- **Toolbar popup** — search your library, view recent uploads, drag-and-drop files to upload.
99
- **New tab** — random photo from your library as the background, "On this day" memory strip, optional auto-rotate, optional album source.
10+
- **Inline matches on Google** — an Immich result card appears at the top of Google search results when your library has matching photos. Search runs entirely between your browser and your Immich; nothing is sent to Google. See [PRIVACY.md](PRIVACY.md) for details.
1011
- **Share-album toolbar** — Slideshow + Download-all controls on your own Immich `/share/...` URLs.
1112
- **Light & dark themes**, customisable per-feature.
1213

@@ -48,6 +49,36 @@ See [PRIVACY.md](PRIVACY.md). Short version: the only data the extension sees is
4849

4950
The icons and promo tile are generated from `icons/make_icons.py` and `webstore-assets/make_assets.py`. Both need Pillow (`pip install Pillow`).
5051

52+
## Building the extension zip
53+
54+
```
55+
./build.sh
56+
```
57+
58+
Writes `dist/immich-companion-<version>.zip` containing only the runtime files (manifest, JS, CSS, HTML, icon PNGs, LICENSE). Upload that zip to the Chrome Web Store.
59+
60+
## Releases
61+
62+
Push a tag matching the manifest version and the GitHub Action in `.github/workflows/release.yml` builds the zip and attaches it to a GitHub Release:
63+
64+
```
65+
git tag v$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
66+
git push --tags
67+
```
68+
69+
### Optional: auto-publish to the Chrome Web Store
70+
71+
Set these four repository secrets and the same workflow will also push the new build to the Web Store after tagging:
72+
73+
| Secret | What it is |
74+
| -------------------------- | ----------------------------------------------------------- |
75+
| `WEBSTORE_EXTENSION_ID` | The 32-char id shown in the Web Store dashboard after your first manual upload |
76+
| `WEBSTORE_CLIENT_ID` | Google Cloud OAuth 2.0 client ID |
77+
| `WEBSTORE_CLIENT_SECRET` | matching client secret |
78+
| `WEBSTORE_REFRESH_TOKEN` | long-lived refresh token from a one-time OAuth dance |
79+
80+
Step-by-step for the OAuth credentials: <https://developer.chrome.com/docs/webstore/using-api>. The first version still has to be uploaded manually so you can fill in the listing fields, screenshots, and privacy policy URL — automation only handles subsequent updates.
81+
5182
## License
5283

5384
[MIT](LICENSE).

STORE_LISTING.md

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Chrome Web Store — Listing Copy
2+
3+
Paste these into the relevant fields of the Web Store dashboard. Everything below is ready to copy verbatim.
4+
5+
---
6+
7+
## Item name
8+
9+
```
10+
Immich Companion
11+
```
12+
13+
## Short description (132 chars max — appears in search results)
14+
15+
```
16+
Save, share and search your self-hosted Immich library from any browser tab. Upload, smart-search, and a memory new tab.
17+
```
18+
(120 chars — leaves headroom)
19+
20+
## Category
21+
22+
`Productivity` (primary). Alternative: `Photos`.
23+
24+
## Language
25+
26+
English (en). Add German if you also localize the listing — the extension UI itself is currently English-only.
27+
28+
---
29+
30+
## Detailed description
31+
32+
```
33+
Immich Companion is a browser extension that connects to a self-hosted Immich server you already run. It does not have a backend, does not collect telemetry, and never sends your data to anyone other than your own Immich.
34+
35+
WHAT IT DOES
36+
37+
• Save to Immich — right-click any image or video on the web to upload it. Optionally adds every saved item to a default album.
38+
39+
• Save & share — uploads the asset, creates a public Immich share link, and copies it to your clipboard. One click, paste anywhere.
40+
41+
• Smart-search popup — click the toolbar icon to search your library with CLIP. The popup also has a Recent tab (your last 30 saves with one-click "Copy link" / "Open") and an Upload tab (drag and drop files from your computer).
42+
43+
• Omnibox search — type "im" + space + a query in the address bar for live results from your library. Press Enter to open the full search on your Immich.
44+
45+
• Inline matches on Google — when you Google something, photos in your library that match the query appear in a card at the top of the results. The search runs directly between your browser and your Immich; Google's servers are not involved.
46+
47+
• New tab page — a random photo from your library as the background, plus an "On this day" strip of memories from past years. Pick a specific album for the source, or set an auto-rotate interval.
48+
49+
• Share-album toolbar — on your Immich /share/... URLs, a small toolbar adds Slideshow and Download-all controls.
50+
51+
• Dark and light themes, keyboard shortcut (Ctrl+Shift+L / ⌘+Shift+L), connection-status badge on the toolbar icon.
52+
53+
REQUIREMENTS
54+
55+
You need a working Immich server you can sign into. The first-run welcome page walks you through creating an API key with the minimum required scopes: asset.upload, asset.read, asset.view, album.read, albumAsset.create, sharedLink.create. If your Immich version only has all-or-nothing keys, that works too.
56+
57+
PRIVACY
58+
59+
The extension stores your server URL and API key locally in chrome.storage.local. It transmits data only to the Immich server URL you configure. No analytics, no third-party services, no tracking. Full privacy policy at the URL below.
60+
61+
OPEN SOURCE
62+
63+
MIT-licensed. Source on GitHub.
64+
```
65+
66+
---
67+
68+
## Single purpose statement
69+
70+
```
71+
Companion for self-hosted Immich servers — save, search and share photos and videos.
72+
```
73+
74+
---
75+
76+
## Permission justifications
77+
78+
Paste each one into the corresponding field in the dashboard (the dashboard prompts per permission).
79+
80+
### `host_permissions: <all_urls>`
81+
82+
```
83+
Required for two purposes: (1) when the user right-clicks an image or video on any website and chooses "Save to Immich", the extension fetches that asset's URL and uploads it to the user's configured Immich server; (2) the optional "Show Immich matches on Google" feature injects a result card on google.<tld>/search pages with photos from the user's library that match their query. The host permission is the only way to either fetch arbitrary image URLs or run a content script on user-chosen pages.
84+
```
85+
86+
### `contextMenus`
87+
88+
```
89+
Adds "Save image to Immich", "Save image to Immich & copy share link", "Save video to Immich", and "Save video to Immich & copy share link" to the right-click menu when the user clicks on an image or video.
90+
```
91+
92+
### `storage`
93+
94+
```
95+
Stores the user's Immich server URL, API key, feature toggles, theme preference, and the most recent 30 uploads (filename, asset id, share link if any) locally in chrome.storage.local. No remote storage, no sync.
96+
```
97+
98+
### `notifications`
99+
100+
```
101+
Shows a desktop notification when an upload to Immich completes (or fails). Can be disabled in extension settings.
102+
```
103+
104+
### `activeTab`, `scripting`
105+
106+
```
107+
Used to render in-page upload progress toasts on the source tab when the user saves an asset, and to write the share link to the clipboard when "Save & share" is used (clipboard access requires running in the active tab's context).
108+
```
109+
110+
### `alarms`
111+
112+
```
113+
Schedules a periodic (every 5 minutes) connection check to the user's Immich server, so the toolbar icon can show whether the server is currently reachable.
114+
```
115+
116+
### `clipboardWrite`
117+
118+
```
119+
Writes the public Immich share link to the user's clipboard after a "Save & share" action so it can be pasted into other apps.
120+
```
121+
122+
---
123+
124+
## Privacy policy URL
125+
126+
The Web Store requires a publicly reachable URL. The repo is private, so a few options:
127+
128+
1. **Public gist (recommended)** — fastest. Run from the repo root:
129+
130+
```
131+
gh gist create --public --filename PRIVACY.md --desc "Privacy policy — Immich Companion (Chrome extension)" PRIVACY.md
132+
```
133+
134+
The command prints a URL like `https://gist.github.com/bjoernch/<id>`. Paste it into the Web Store dashboard's "Privacy policy URL" field.
135+
136+
2. **Make the repo public** — simplest if you don't mind the source being open. Use:
137+
138+
```
139+
gh repo edit --visibility public --accept-visibility-change-consequences
140+
```
141+
142+
Then the URL is `https://github.com/bjoernch/immich-companion/blob/main/PRIVACY.md`.
143+
144+
3. **GitHub Pages on a separate public repo** — host PRIVACY.md from a public repo of your choosing.
145+
146+
---
147+
148+
## Screenshots
149+
150+
Capture **at least one**, ideally three or four, at exactly 1280 × 800. The Web Store accepts up to five.
151+
152+
Suggestions:
153+
1. Toolbar popup with search results loaded.
154+
2. Options page on the Connection or New Tab section.
155+
3. The new tab itself with a real photo background and the "On this day" strip populated.
156+
4. A real Google search showing the injected Immich card.
157+
5. The right-click "Save to Immich" menu open over an image, with the success toast.
158+
159+
Save them as `.png` into `webstore-assets/`. The included `screenshot-template-1280x800.png` is just a placeholder.
160+
161+
---
162+
163+
## Promotional images
164+
165+
Already generated by `webstore-assets/make_assets.py`:
166+
167+
- `webstore-assets/promo-tile-440x280.png` — the small tile, required.
168+
- `webstore-assets/promo-marquee-1400x560.png` — the marquee tile, optional.
169+
170+
---
171+
172+
## Build the submission zip
173+
174+
From the repo root:
175+
176+
```
177+
./build.sh
178+
```
179+
180+
That writes `dist/immich-companion-<version>.zip` containing only the runtime files (manifest, JS, CSS, HTML, icon PNGs, LICENSE). Excluded: `.git`, dev files, README/PRIVACY/STORE_LISTING markdown, the `webstore-assets/` folder, Python source, and any `.DS_Store` / `__pycache__` cruft.
181+
182+
Upload that zip on the Web Store dashboard.

build.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
# Package the extension into dist/immich-companion-<version>.zip
3+
# ready for Chrome Web Store upload. Excludes dev files, screenshots,
4+
# Python tooling, and dotfiles.
5+
set -euo pipefail
6+
7+
cd "$(dirname "$0")"
8+
9+
VERSION=$(python3 -c "import json; print(json.load(open('manifest.json'))['version'])")
10+
OUT_DIR="dist"
11+
STAGE="$OUT_DIR/build"
12+
ZIP="$OUT_DIR/immich-companion-${VERSION}.zip"
13+
14+
rm -rf "$STAGE"
15+
mkdir -p "$STAGE"
16+
17+
cp manifest.json "$STAGE/"
18+
cp background.js "$STAGE/"
19+
cp content.js "$STAGE/"
20+
cp content.css "$STAGE/"
21+
cp LICENSE "$STAGE/"
22+
23+
cp -R lib "$STAGE/"
24+
cp -R pages "$STAGE/"
25+
26+
mkdir -p "$STAGE/icons"
27+
cp icons/*.png "$STAGE/icons/"
28+
29+
# Strip the usual cruft
30+
find "$STAGE" -name '.DS_Store' -delete
31+
find "$STAGE" -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
32+
find "$STAGE" -name '*.pyc' -delete
33+
34+
rm -f "$ZIP"
35+
( cd "$STAGE" && zip -qr "../../$ZIP" . )
36+
37+
SIZE=$(du -h "$ZIP" | cut -f1)
38+
echo "wrote $ZIP ($SIZE)"

0 commit comments

Comments
 (0)