From caeb394fdb46ce99ba2632168cf5874f7213cde0 Mon Sep 17 00:00:00 2001 From: Tautvydas Derzinskas Date: Sun, 27 Sep 2026 13:35:25 +0200 Subject: [PATCH] ci(docs): edge store listing info and auto publishing --- .github/workflows/extension-release.yml | 74 +++++++++++++- README.md | 2 +- extension/CONTRIBUTING.md | 70 ++++++++++++-- extension/README.md | 46 ++++++--- extension/docs/store/store-icon.png | Bin 0 -> 3828 bytes extension/package.json | 2 +- extension/scripts/publish-edge.ts | 113 ++++++++++++++++++++++ extension/scripts/store-assets.ts | 4 +- extension/store-assets/store-icon.html | 9 ++ frontend/src/constants/extension.ts | 24 +++-- frontend/src/i18n/locales/en.json | 11 +-- frontend/src/i18n/locales/lt.json | 11 +-- frontend/src/pages/DownloadPage/index.tsx | 61 +++++++----- 13 files changed, 353 insertions(+), 74 deletions(-) create mode 100644 extension/docs/store/store-icon.png create mode 100644 extension/scripts/publish-edge.ts create mode 100644 extension/store-assets/store-icon.html diff --git a/.github/workflows/extension-release.yml b/.github/workflows/extension-release.yml index 3c27f7f..5fa653b 100644 --- a/.github/workflows/extension-release.yml +++ b/.github/workflows/extension-release.yml @@ -112,6 +112,71 @@ jobs: path: extension/dist/signed/thingport-grab-firefox.xpi if-no-files-found: error + # Publishes to Microsoft Edge Add-ons (https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol) + # once per version: Edge rejects a version it already has, and package.json's version is only + # bumped for real releases, not on every commit. Each version published here gets a + # `extension-edge-v` tag, which is what "already published" is checked against -- so + # bumping the version in extension/package.json and merging to main is the whole release step. + # A failed run (e.g. the previous version still in certification) leaves no tag, so the next + # push or a manual re-run tries again. Skips itself until the credentials are set up (see + # extension/CONTRIBUTING.md, "Publishing to Edge automatically"). + publish-edge: + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' + needs: [build-chrome] + runs-on: ubuntu-latest + permissions: + contents: write # pushes the extension-edge-v tag + defaults: + run: + working-directory: extension + env: + EDGE_PRODUCT_ID: ${{ vars.EDGE_PRODUCT_ID }} + EDGE_CLIENT_ID: ${{ secrets.EDGE_CLIENT_ID }} + EDGE_API_KEY: ${{ secrets.EDGE_API_KEY }} + steps: + - uses: actions/checkout@v4 + - name: Check whether this version still needs publishing + id: check + run: | + version=$(jq -r .version package.json) + tag="extension-edge-v$version" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + if [ -z "$EDGE_PRODUCT_ID" ] || [ -z "$EDGE_CLIENT_ID" ] || [ -z "$EDGE_API_KEY" ]; then + echo "::notice::Edge publishing isn't set up (EDGE_PRODUCT_ID variable, EDGE_CLIENT_ID and EDGE_API_KEY secrets) -- skipping." + echo "publish=false" >> "$GITHUB_OUTPUT" + elif git ls-remote --exit-code --tags origin "refs/tags/$tag" > /dev/null; then + echo "Version $version is already on Edge Add-ons ($tag exists) -- nothing to publish." + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "Version $version isn't on Edge Add-ons yet -- publishing." + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + - uses: actions/setup-node@v4 + if: steps.check.outputs.publish == 'true' + with: + node-version: 20 + cache: npm + cache-dependency-path: extension/package-lock.json + - if: steps.check.outputs.publish == 'true' + run: npm ci + # The exact zip build-chrome built and attached to the release, not a rebuild. + - uses: actions/download-artifact@v4 + if: steps.check.outputs.publish == 'true' + with: + name: thingport-grab-edge + path: extension/dist/zips + - name: Upload and submit for certification + if: steps.check.outputs.publish == 'true' + run: | + npx tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip --notes \ + "Automated update to version ${{ steps.check.outputs.version }} (commit ${{ github.sha }}). Testing instructions are unchanged from the previous submission; documentation: https://tautvydasderzinskas.github.io/Thingport/docs/grab/" + - name: Tag the published version + if: steps.check.outputs.publish == 'true' + run: | + git tag "${{ steps.check.outputs.tag }}" + git push origin "${{ steps.check.outputs.tag }}" + release: if: github.event_name == 'push' && github.ref == 'refs/heads/main' needs: [build-chrome, build-firefox] @@ -145,14 +210,15 @@ jobs: Automated build of the Thingport Grab browser extension (see `extension/README.md`) from ${{ github.sha }}. - Not published to any browser's extension store -- see `extension/README.md` for - install instructions: + See `extension/README.md` for install instructions: + - **Edge**: install it from + [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol), + which keeps it up to date. `thingport-grab-edge.zip` here is the same build, for + loading by hand (`edge://extensions` → **Developer mode** → **Load unpacked**). - **Chrome / other Chromium browsers**: unzip `thingport-grab-chrome.zip`, then `chrome://extensions` → enable **Developer mode** → **Load unpacked** → select the unzipped folder. - - **Edge**: the same with `thingport-grab-edge.zip`, from `edge://extensions` - (**Developer mode** is in the left sidebar). - **Firefox**: download `thingport-grab-firefox.xpi` and open it directly (or drag it onto a Firefox window). It's signed by Mozilla for self-distribution, so it installs and updates like any other extension -- no Developer mode needed. diff --git a/README.md b/README.md index aab8be0..519412c 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Instead of having your collection scattered across different websites and your f Thingport ships two small companion tools, each downloadable from the in-app Download page or GitHub Releases: - **[Thingport Bridge](bridge/README.md)** — a lightweight desktop helper that makes "Open in {Slicer}" work for slicers (Bambu Studio, PrusaSlicer, Cura) whose own URL-protocol handlers won't accept a link from a self-hosted domain. -- **[Thingport Grab](extension/README.md)** — a browser extension for Chrome, Edge and Firefox that imports MakerWorld, Thingiverse, and Printables models straight from their own pages, without leaving the site (see the screenshots above). +- **[Thingport Grab](extension/README.md)** — a browser extension for Chrome, Edge and Firefox that imports MakerWorld, Thingiverse, and Printables models straight from their own pages, without leaving the site (see the screenshots above). Get it for Edge from [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol). ## Provider Setup diff --git a/extension/CONTRIBUTING.md b/extension/CONTRIBUTING.md index a7ca2c6..70ba3e7 100644 --- a/extension/CONTRIBUTING.md +++ b/extension/CONTRIBUTING.md @@ -103,7 +103,8 @@ submitting an update -- every store rejects a version it has already seen. None of these IDs are secret. The ones a workflow needs are also repo **variables** (Settings > Secrets and variables > Actions > Variables), so they don't have to be hard-coded in a workflow. -**Microsoft Edge Add-ons** (submitted; the listing URL works once certification passes): +**Microsoft Edge Add-ons** (live; new versions are published by CI, see +[Publishing to Edge automatically](#publishing-to-edge-automatically)): | | Value | | --- | --- | @@ -112,15 +113,21 @@ Secrets and variables > Actions > Variables), so they don't have to be hard-code | Product ID | `8c5f106c-5a45-438f-8e0b-2d0c0584d253` -- repo variable `EDGE_PRODUCT_ID`, used by the publish API | | Store ID | `0RDCKH3TMN7G` | +**Chrome Web Store** (submitted; the listing URL works once review passes): + +| | Value | +| --- | --- | +| Listing | https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee | +| Extension ID | `nmblahmglpbplmfcggghdgohohlaeiee` -- repo variable `CHROME_EXTENSION_ID` | + +Publishing to Chrome from CI (not set up yet) needs a Google Cloud OAuth client for the +[Chrome Web Store API](https://developer.chrome.com/docs/webstore/using-api), as repo secrets: +`CHROME_CLIENT_ID`, `CHROME_CLIENT_SECRET` and `CHROME_REFRESH_TOKEN`. + Edge also issued a public key for the listing. Don't add it to the manifest (`key`) of the store builds -- the stores set that themselves. It's only useful for giving an unpacked development build the same extension ID as the store version. -Publishing to Edge from CI (not set up yet) additionally needs the Partner Center **Publish API** -credentials, as repo **secrets**: `EDGE_CLIENT_ID` and `EDGE_API_KEY`. The upload itself is a -`POST` of `dist/zips/thingport-grab-edge.zip` to the -[Edge Add-ons API](https://learn.microsoft.com/microsoft-edge/extensions/publish/api/using-addons-api) -for `EDGE_PRODUCT_ID`, followed by a publish request. ### Store listing images @@ -130,6 +137,7 @@ templates in `store-assets/` (styled like the website's social card): | File | Size | Use | | --- | --- | --- | | `logo.png` | 300x300 | Store logo | +| `store-icon.png` | 128x128 | Chrome Web Store icon -- transparent, 96x96 artwork with 16px padding | | `tile-small.png` | 440x280 | Small promotional tile | | `tile-large.png` | 1400x560 | Large promotional tile -- the extension's real panel (its compiled stylesheet) on a placeholder page | | `screenshots/*.png` | 1280x800 | The README's page screenshots as PNG, which is all the stores accept | @@ -158,7 +166,8 @@ pages are the live sites -- by default the first models linked from each site's typecheck, lint, the Chrome/Edge build, and `web-ext lint` on the Firefox build. On a push to `main` it also signs the Firefox build through AMO's unlisted channel and publishes `thingport-grab-chrome.zip`, `thingport-grab-edge.zip` and `thingport-grab-firefox.xpi` to the -`extension-latest` release (the in-app Download page links there). +`extension-latest` release (the in-app Download page links there for Chrome and Firefox), and +publishes new versions to Edge Add-ons (see below). AMO rejects re-uploading a version number it has already seen for this add-on ID (even on the unlisted channel), so CI signs a copy of the built manifest with the run number appended to the @@ -173,6 +182,53 @@ npx web-ext sign --source-dir dist/firefox --channel unlisted \ --api-key "$AMO_JWT_ISSUER" --api-secret "$AMO_JWT_SECRET" ``` +### Publishing to Edge automatically + +The workflow's `publish-edge` job publishes each new version to Microsoft Edge Add-ons by itself, +through the [Edge Add-ons Update API](https://learn.microsoft.com/microsoft-edge/extensions/update/api/using-addons-api) +(`scripts/publish-edge.ts`): it uploads the exact `thingport-grab-edge.zip` the build produced, +waits for Microsoft to process it, and submits it for certification. Microsoft's review then takes +anywhere from hours to a few days before the update reaches users. + +It runs on every push to `main`, but only publishes a version once: each published version gets +an `extension-edge-v` tag, and a version that already has one is skipped. So **releasing +to Edge means bumping `version` in `package.json`** -- Edge rejects a version it already has +anyway. If a run fails (most often because the previous version is still in certification, which +Edge doesn't allow two of), no tag is created, and the next push or a manual re-run of the +workflow tries again. + +To test the script against Edge by hand, with the credentials below in your environment: + +```bash +npm run zip:edge +EDGE_PRODUCT_ID=... EDGE_CLIENT_ID=... EDGE_API_KEY=... \ + npx tsx scripts/publish-edge.ts dist/zips/thingport-grab-edge.zip +``` + +### One-time setup: Edge publishing credentials + +The job skips itself (with a notice in the run) until these exist: + +1. Sign in to [Partner Center](https://partner.microsoft.com/dashboard/microsoftedge/overview) with + the account that owns the listing. +2. Under **Microsoft Edge**, open **Publish API**. If it offers to **enable the new experience**, + click **Enable** -- the workflow uses the API-key version (v1.1). +3. Click **Create API credentials** (it can take a minute). The page then shows a **Client ID** and + an **API key**, with the key's expiry date. Copy the key now; it isn't shown again. +4. Add them as repo secrets -- **Settings > Secrets and variables > Actions > Secrets**, or from + this repo's folder: + + ```bash + gh secret set EDGE_CLIENT_ID # paste the Client ID when prompted + gh secret set EDGE_API_KEY # paste the API key when prompted + ``` + +5. `EDGE_PRODUCT_ID` is already a repo **variable** (see [Store listings](#store-listings)). + +**API keys expire.** Partner Center shows each key's expiry date; before then, create a new key on +the same page and replace `EDGE_API_KEY` (the Client ID stays the same). An expired key makes the +job fail with HTTP 401 and a message saying so. + ### One-time setup: AMO signing credentials Firefox requires every extension -- even self-distributed, unlisted ones -- to be signed by diff --git a/extension/README.md b/extension/README.md index 04079b6..ed491a7 100644 --- a/extension/README.md +++ b/extension/README.md @@ -6,6 +6,10 @@ Visiting a model, collection, or Thingiverse Likes page shows a floating Thingpo opens a small panel to pick what to import (and, for a single model, an optional destination collection), then imports it the same way Thingport's own "+ Add > Import" does. +**Get it for Edge from [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol).** +Chrome and Firefox builds are on the [releases page](#install) until their store listings are +live. + It talks directly to your Thingport instance's API from the extension's background script -- no separate server, no data sent anywhere else. See the [privacy policy](PRIVACY.md) for exactly what it stores and sends. @@ -35,23 +39,39 @@ it stores and sends. -## Install (no extension store listing) +## Install -This isn't published to any browser's extension store yet, so it installs the same way the -[Thingport releases](https://github.com/TautvydasDerzinskas/Thingport/releases) page's other -downloads do. +### Microsoft Edge -### Chrome / Edge / other Chromium browsers +Install **[Thingport Grab from Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol)** +-- click **Get**, then click the new Thingport icon in your toolbar (it may be under the +puzzle-piece Extensions button) and enter your instance's URL and your Thingport login. Edge keeps +it up to date from then on. -As a developer-mode "unpacked" extension: +
+Alternative: install the Edge build by hand -1. Download `thingport-grab-chrome.zip` (Edge: `thingport-grab-edge.zip`) from the in-app Download - page (or the +Only needed if you can't use the store, e.g. to try a build before it's been published there: + +1. Download `thingport-grab-edge.zip` from the + [`extension-latest` release](https://github.com/TautvydasDerzinskas/Thingport/releases/tag/extension-latest) + and unzip it somewhere permanent (Edge loads the extension from that folder every time it + starts, so don't delete it). +2. Open `edge://extensions` and turn on **Developer mode** (in the left sidebar). +3. Click **Load unpacked** and select the unzipped folder. + +A hand-installed copy doesn't update itself -- repeat these steps for a newer version. +
+ +### Chrome / other Chromium browsers + +As a developer-mode "unpacked" extension, until the Chrome Web Store listing is live: + +1. Download `thingport-grab-chrome.zip` from the in-app Download page (or the [`extension-latest` release](https://github.com/TautvydasDerzinskas/Thingport/releases/tag/extension-latest)) and unzip it somewhere permanent (don't delete the folder afterwards -- the browser loads the extension from it every time it starts). -2. Open `chrome://extensions` (Edge: `edge://extensions`), turn on **Developer mode** (top right in - Chrome, in the left sidebar in Edge). +2. Open `chrome://extensions` and turn on **Developer mode** (top right). 3. Click **Load unpacked** and select the unzipped folder. 4. Click the new Thingport icon in your toolbar, enter your instance's URL and your Thingport login, and save. @@ -70,9 +90,9 @@ development guide): 3. Click the new Thingport icon in your toolbar, enter your instance's URL and your Thingport login, and save. -It's signed for **self-distribution** (the "unlisted" channel), not listed on -addons.mozilla.org -- signing is still required for Firefox to allow the install at all, and it's -what lets Firefox treat later versions as updates to the same install instead of a fresh add-on. +It's signed for **self-distribution** (the "unlisted" channel) -- signing is still required for +Firefox to allow the install at all, and it's what lets Firefox treat later versions as updates to +the same install instead of a fresh add-on. A listing on addons.mozilla.org is in review. ## Setup diff --git a/extension/docs/store/store-icon.png b/extension/docs/store/store-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..db70f4bd237d419339e52871c0fefb4d7a3de8ec GIT binary patch literal 3828 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 = { + InProgressSubmission: + "A previous submission is still in certification. Wait for Microsoft to finish reviewing it, then re-run this workflow.", + NoModulesUpdated: "The uploaded package is identical to the published one -- bump the version in package.json.", + UnpublishInProgress: "The listing is being unpublished in Partner Center.", + CreateNotAllowed: "The product ID doesn't belong to an existing listing -- check the EDGE_PRODUCT_ID variable.", +}; + +function requireEnv(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} isn't set`); + return value; +} + +function argValue(flag: string): string | undefined { + const index = process.argv.indexOf(flag); + return index >= 0 ? process.argv[index + 1] : undefined; +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** A 202 whose Location header carries the operation to poll (documented as the bare ID; a + * full URL is tolerated too). */ +async function startOperation(url: string, init: RequestInit, what: string): Promise { + const res = await fetch(url, init); + if (res.status !== 202) { + const body = await res.text().catch(() => ""); + const hint = res.status === 401 ? " -- the API key or client ID is wrong, or the key has expired (Partner Center > Publish API)." : ""; + throw new Error(`${what} failed: HTTP ${res.status} ${body}${hint}`); + } + const location = res.headers.get("location"); + if (!location) throw new Error(`${what}: no operation ID in the response`); + return location.replace(/\/+$/, "").split("/").pop()!; +} + +async function waitFor(url: string, auth: Record, what: string): Promise { + const deadline = Date.now() + POLL_TIMEOUT_MS; + for (;;) { + const res = await fetch(url, { headers: auth }); + if (!res.ok) throw new Error(`${what}: status check failed with HTTP ${res.status} ${await res.text().catch(() => "")}`); + const operation = (await res.json()) as Operation; + if (operation.status !== "InProgress") { + if (operation.status === "Succeeded") return operation; + const hint = operation.errorCode ? ERROR_HINTS[operation.errorCode] : undefined; + const details = operation.errors?.length ? `\n ${JSON.stringify(operation.errors)}` : ""; + throw new Error(`${what} failed (${operation.errorCode || "no error code"}): ${operation.message}${details}${hint ? `\n ${hint}` : ""}`); + } + if (Date.now() > deadline) throw new Error(`${what} still in progress after ${POLL_TIMEOUT_MS / 60000} minutes`); + await sleep(POLL_INTERVAL_MS); + } +} + +async function main(): Promise { + const zipPath = process.argv.slice(2).find((arg, i, all) => !arg.startsWith("--") && all[i - 1] !== "--notes"); + if (!zipPath) throw new Error("Usage: tsx scripts/publish-edge.ts [--notes ]"); + const productId = requireEnv("EDGE_PRODUCT_ID"); + const auth = { Authorization: `ApiKey ${requireEnv("EDGE_API_KEY")}`, "X-ClientID": requireEnv("EDGE_CLIENT_ID") }; + const productUrl = `${API_BASE}/v1/products/${productId}`; + + const zip = await readFile(zipPath); + console.log(`Uploading ${zipPath} (${(zip.length / 1024).toFixed(1)} KB) to Edge Add-ons product ${productId}…`); + const uploadId = await startOperation( + `${productUrl}/submissions/draft/package`, + { method: "POST", headers: { ...auth, "Content-Type": "application/zip" }, body: zip }, + "Upload", + ); + const uploaded = await waitFor(`${productUrl}/submissions/draft/package/operations/${uploadId}`, auth, "Upload"); + console.log(`Upload processed: ${uploaded.message ?? "ok"}`); + + const notes = argValue("--notes") ?? "Automated update. Testing instructions are unchanged from the previous submission."; + const publishId = await startOperation( + `${productUrl}/submissions`, + { method: "POST", headers: { ...auth, "Content-Type": "application/json" }, body: JSON.stringify({ notes }) }, + "Submission", + ); + const submitted = await waitFor(`${productUrl}/submissions/operations/${publishId}`, auth, "Submission"); + console.log(`Submitted for certification: ${submitted.message ?? "ok"}`); +} + +try { + await main(); +} catch (err) { + console.error(err instanceof Error ? err.message : err); + process.exit(1); +} diff --git a/extension/scripts/store-assets.ts b/extension/scripts/store-assets.ts index 3026bc1..ceb35c6 100644 --- a/extension/scripts/store-assets.ts +++ b/extension/scripts/store-assets.ts @@ -3,6 +3,7 @@ // npm run store-assets // // logo.png 300x300 store logo (Edge Add-ons "Extension logo") +// store-icon.png 128x128 Chrome Web Store icon (transparent, 96px artwork + 16px padding) // tile-small.png 440x280 small promotional tile // tile-large.png 1400x560 large promotional tile (the extension's real panel on a placeholder page) // screenshots/*.png 1280x800 the README's page screenshots (docs/screenshots/*.jpg) as PNG, at the @@ -32,6 +33,7 @@ const CONTENT_CSS = const ASSETS = [ { template: "logo.html", out: "logo.png", width: 300, height: 300 }, + { template: "store-icon.html", out: "store-icon.png", width: 128, height: 128, transparent: true }, { template: "tile-small.html", out: "tile-small.png", width: 440, height: 280 }, { template: "tile-large.html", out: "tile-large.png", width: 1400, height: 560 }, ]; @@ -45,7 +47,7 @@ try { await page.goto(pathToFileURL(path.join(TEMPLATES, asset.template)).href, { waitUntil: "load" }); await page.addStyleTag({ content: CONTENT_CSS }); await page.evaluate(() => document.fonts.ready); - await page.screenshot({ path: path.join(OUT_DIR, asset.out) }); + await page.screenshot({ path: path.join(OUT_DIR, asset.out), omitBackground: "transparent" in asset }); await page.close(); console.log(`docs/store/${asset.out} ${asset.width}x${asset.height}`); } diff --git a/extension/store-assets/store-icon.html b/extension/store-assets/store-icon.html new file mode 100644 index 0000000..4078b76 --- /dev/null +++ b/extension/store-assets/store-icon.html @@ -0,0 +1,9 @@ + + + + + diff --git a/frontend/src/constants/extension.ts b/frontend/src/constants/extension.ts index c6a7eca..21da5e0 100644 --- a/frontend/src/constants/extension.ts +++ b/frontend/src/constants/extension.ts @@ -8,16 +8,26 @@ // build on every release rather than ever making a new one. const EXTENSION_RELEASE_BASE = "https://github.com/TautvydasDerzinskas/Thingport/releases/download/extension-latest"; -export type ExtensionDownload = { browser: "chrome" | "edge" | "firefox"; label: string; asset: string }; +/** A browser whose card links to a file from the release (followed by install steps), or to the + * extension's store listing, which installs and updates it like any other extension. */ +export type ExtensionDownload = + | { kind: "file"; browser: "chrome" | "firefox"; label: string; asset: string } + | { kind: "store"; browser: "edge"; label: string; storeUrl: string }; + +/** Browsers installed from a downloaded file -- the ones the install-steps dialog covers. */ +export type ExtensionFileBrowser = Extract["browser"]; + +// Published there by the release workflow's publish-edge job. The Edge zip is still attached to +// every release, but only the docs mention it now (extension/README.md) -- the store version is +// the one to recommend, since it updates itself. +export const EDGE_ADDONS_URL = "https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol"; // Firefox's asset is a Mozilla-signed .xpi rather than a zip -- see extension/README.md's -// "Firefox" install section for why an unpacked zip won't do there. Edge gets its own zip (built -// alongside Chrome's, see extension/scripts/manifest.ts) and its own install steps, since -// edge://extensions is laid out differently from chrome://extensions. +// "Firefox" install section for why an unpacked zip won't do there. export const EXTENSION_DOWNLOADS: ExtensionDownload[] = [ - { browser: "chrome", label: "Chrome", asset: "thingport-grab-chrome.zip" }, - { browser: "edge", label: "Edge", asset: "thingport-grab-edge.zip" }, - { browser: "firefox", label: "Firefox", asset: "thingport-grab-firefox.xpi" }, + { kind: "file", browser: "chrome", label: "Chrome", asset: "thingport-grab-chrome.zip" }, + { kind: "store", browser: "edge", label: "Edge", storeUrl: EDGE_ADDONS_URL }, + { kind: "file", browser: "firefox", label: "Firefox", asset: "thingport-grab-firefox.xpi" }, ]; export function extensionDownloadUrl(asset: string): string { diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 0258de1..19dc7dc 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -466,6 +466,9 @@ "heading": "Thingport Grab", "intro": "A browser extension that imports MakerWorld, Thingiverse, and Printables models straight from their own pages.", "name": "Thingport Grab", + "store": { + "edge": "Get from Edge Add-ons" + }, "modal": { "chrome": { "heading": "Install Thingport Grab", @@ -475,14 +478,6 @@ "step4": "Click \"Load unpacked\" and select the unzipped folder.", "step5": "Click the new Thingport icon in your toolbar and enter your instance URL and login." }, - "edge": { - "heading": "Install Thingport Grab", - "step1": "Unzip the downloaded file somewhere permanent -- Edge loads the extension from this folder every time it starts.", - "step2": "Open edge://extensions in a new tab.", - "step3": "Turn on Developer mode (in the left sidebar).", - "step4": "Click \"Load unpacked\" and select the unzipped folder.", - "step5": "Click the new Thingport icon in your toolbar (it may be under the puzzle-piece Extensions button) and enter your instance URL and login." - }, "firefox": { "heading": "Install Thingport Grab", "step1": "Open the downloaded .xpi file directly, or drag it onto a Firefox window.", diff --git a/frontend/src/i18n/locales/lt.json b/frontend/src/i18n/locales/lt.json index 2a1da1b..b042ab5 100644 --- a/frontend/src/i18n/locales/lt.json +++ b/frontend/src/i18n/locales/lt.json @@ -467,6 +467,9 @@ "heading": "Thingport Grab", "intro": "Naršyklės plėtinys, importuojantis MakerWorld, Thingiverse ir Printables modelius tiesiai iš jų puslapių.", "name": "Thingport Grab", + "store": { + "edge": "Gauti iš Edge Add-ons" + }, "modal": { "chrome": { "heading": "Įdiegti Thingport Grab", @@ -476,14 +479,6 @@ "step4": "Spustelėkite „Load unpacked“ ir pasirinkite išarchyvuotą aplanką.", "step5": "Spustelėkite naują Thingport piktogramą įrankių juostoje ir įveskite savo aplinkos URL bei prisijungimo duomenis." }, - "edge": { - "heading": "Įdiegti Thingport Grab", - "step1": "Išarchyvuokite atsisiųstą failą kur nors nuolatinėje vietoje -- Edge įkelia plėtinį iš šio aplanko kaskart paleidus naršyklę.", - "step2": "Naujame skirtuke atidarykite edge://extensions.", - "step3": "Įjunkite Developer mode (kairėje šoninėje juostoje).", - "step4": "Spustelėkite „Load unpacked“ ir pasirinkite išarchyvuotą aplanką.", - "step5": "Spustelėkite naują Thingport piktogramą įrankių juostoje (ji gali būti po dėlionės formos Extensions mygtuku) ir įveskite savo aplinkos URL bei prisijungimo duomenis." - }, "firefox": { "heading": "Įdiegti Thingport Grab", "step1": "Atidarykite atsisiųstą .xpi failą tiesiogiai arba nutempkite jį į Firefox langą.", diff --git a/frontend/src/pages/DownloadPage/index.tsx b/frontend/src/pages/DownloadPage/index.tsx index 7384a97..45eefbe 100644 --- a/frontend/src/pages/DownloadPage/index.tsx +++ b/frontend/src/pages/DownloadPage/index.tsx @@ -11,13 +11,19 @@ import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import DialogActions from "@mui/material/DialogActions"; import DownloadIcon from "@mui/icons-material/Download"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; import LaptopWindowsIcon from "@mui/icons-material/LaptopWindows"; import AppleIcon from "@mui/icons-material/Apple"; import TerminalIcon from "@mui/icons-material/Terminal"; import CableIcon from "@mui/icons-material/Cable"; import { usePageHeader } from "../../components/Layout/PageHeaderContext"; import { BRIDGE_DOWNLOADS, bridgeDownloadUrl, type BridgeDownload } from "../../constants/bridge"; -import { EXTENSION_DOWNLOADS, extensionDownloadUrl, type ExtensionDownload } from "../../constants/extension"; +import { + EXTENSION_DOWNLOADS, + extensionDownloadUrl, + type ExtensionDownload, + type ExtensionFileBrowser, +} from "../../constants/extension"; import extensionIcon from "../../assets/logos/thingport-icon-color.svg"; // Official browser logos (github.com/alrra/browser-logos), shown only to mark which browser each // extension download is for. @@ -87,9 +93,9 @@ export default function DownloadPage() { usePageHeader({ title: t("sidebar.downloads") }); const [installOs, setInstallOs] = useState(null); - const [extensionInstallBrowser, setExtensionInstallBrowser] = useState(null); + const [extensionInstallBrowser, setExtensionInstallBrowser] = useState(null); - const extensionInstallSteps: Record = { + const extensionInstallSteps: Record = { chrome: [ { text: t("download.extension.modal.chrome.step1") }, { text: t("download.extension.modal.chrome.step2") }, @@ -97,13 +103,6 @@ export default function DownloadPage() { { text: t("download.extension.modal.chrome.step4") }, { text: t("download.extension.modal.chrome.step5") }, ], - edge: [ - { text: t("download.extension.modal.edge.step1") }, - { text: t("download.extension.modal.edge.step2") }, - { text: t("download.extension.modal.edge.step3") }, - { text: t("download.extension.modal.edge.step4") }, - { text: t("download.extension.modal.edge.step5") }, - ], firefox: [ { text: t("download.extension.modal.firefox.step1") }, { text: t("download.extension.modal.firefox.step2") }, @@ -145,26 +144,40 @@ export default function DownloadPage() { - {EXTENSION_DOWNLOADS.map(({ browser, label, asset }) => ( - + {EXTENSION_DOWNLOADS.map((item) => ( + - + - {t("download.extension.name")} ({label}) + {t("download.extension.name")} ({item.label}) - + {item.kind === "store" ? ( + + ) : ( + + )} ))}