mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
feat(extension): automate publishing to Chrome Web Store
This commit is contained in:
1 parent
955513c5c5
commit
e37073cf53
10 files changed
+269
-128
No files matched your search
@@ -135,6 +135,10 @@ jobs:
|
||||
|
||||
See `extension/README.md` for install instructions:
|
||||
|
||||
- **Chrome / other Chromium browsers**: install it from the
|
||||
[Chrome Web Store](https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee),
|
||||
which keeps it up to date. `thingport-grab-chrome.zip` here is a build of this commit,
|
||||
for loading by hand (`chrome://extensions` → **Developer mode** → **Load unpacked**).
|
||||
- **Firefox**: install it from
|
||||
[Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/), which keeps
|
||||
it up to date. `thingport-grab-firefox.xpi` here is a Mozilla-signed build of this
|
||||
@@ -143,9 +147,6 @@ jobs:
|
||||
[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.
|
||||
files: |
|
||||
dist/thingport-grab-chrome.zip
|
||||
dist/thingport-grab-edge.zip
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Extension store release
|
||||
# Versioned releases of Thingport Grab via semantic-release (extension/.releaserc.json): every
|
||||
# Friday at 13:00 Polish time, or on demand. Only commits touching extension/ count; with a
|
||||
# releasable one it bumps the version, writes the changelog, tags, creates a GitHub release with
|
||||
# the store zips, and publishes to Edge Add-ons and addons.mozilla.org. Per-merge builds are in
|
||||
# the store zips, and publishes to the Chrome Web Store, Edge Add-ons and addons.mozilla.org. Per-merge builds are in
|
||||
# extension-release.yml.
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
@@ -80,6 +80,47 @@ jobs:
|
||||
path: extension/dist/zips/*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
# Its own job so it can be re-run alone. Signs in as the publisher's service account; see
|
||||
# extension/CONTRIBUTING.md.
|
||||
publish-chrome:
|
||||
needs: release
|
||||
if: needs.release.outputs.version != ''
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: extension
|
||||
env:
|
||||
CHROME_EXTENSION_ID: ${{ vars.CHROME_EXTENSION_ID }}
|
||||
CWS_PUBLISHER_ID: ${{ secrets.CWS_PUBLISHER_ID }}
|
||||
CWS_SERVICE_ACCOUNT_KEY: ${{ secrets.CWS_SERVICE_ACCOUNT_KEY }}
|
||||
steps:
|
||||
- name: Credentials set up?
|
||||
id: creds
|
||||
run: |
|
||||
if [ -z "$CWS_SERVICE_ACCOUNT_KEY" ] || [ -z "$CWS_PUBLISHER_ID" ]; then
|
||||
echo "::notice::CWS_SERVICE_ACCOUNT_KEY / CWS_PUBLISHER_ID aren't set -- skipping the Chrome Web Store (see extension/CONTRIBUTING.md)."
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
working-directory: .
|
||||
- uses: actions/checkout@v4
|
||||
if: steps.creds.outputs.ok != 'false'
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.creds.outputs.ok != 'false'
|
||||
with:
|
||||
node-version: 20
|
||||
cache: npm
|
||||
cache-dependency-path: extension/package-lock.json
|
||||
- if: steps.creds.outputs.ok != 'false'
|
||||
run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
if: steps.creds.outputs.ok != 'false'
|
||||
with:
|
||||
name: thingport-grab-store-zips
|
||||
path: extension/dist/zips
|
||||
- name: Upload and submit for review
|
||||
if: steps.creds.outputs.ok != 'false'
|
||||
run: npx tsx scripts/publish-chrome.ts dist/zips/thingport-grab-chrome.zip
|
||||
|
||||
# Its own job so it can be re-run alone: Edge takes one submission at a time and fails with
|
||||
# InProgressSubmission while the previous one is in certification. Credentials: see
|
||||
# extension/CONTRIBUTING.md.
|
||||
|
||||
@@ -84,7 +84,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, Anycubic Slicer Next) 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). Even without a Thingport instance, it adds a "Download normalized" button to MakerWorld model pages that converts Bambu Studio projects into 3MFs other slicers open with their colours and settings. Get it from [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/) or [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol).
|
||||
- **[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). Even without a Thingport instance, it adds a "Download normalized" button to MakerWorld model pages that converts Bambu Studio projects into 3MFs other slicers open with their colours and settings. Get it from the [Chrome Web Store](https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee), [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/) or [Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol).
|
||||
|
||||
## Provider Setup
|
||||
|
||||
|
||||
@@ -133,16 +133,14 @@ Secrets and variables > Actions > Variables), so they don't have to be hard-code
|
||||
| Listing | https://addons.mozilla.org/firefox/addon/thingport-grab/ |
|
||||
| Add-on ID | `grab@thingport.app` -- `gecko.id` in `scripts/manifest.ts` |
|
||||
|
||||
**Chrome Web Store** (submitted; the listing URL works once review passes):
|
||||
**Chrome Web Store** (live; new versions are published by CI, see
|
||||
[Publishing to the Chrome Web Store automatically](#publishing-to-the-chrome-web-store-automatically)):
|
||||
|
||||
| | 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`.
|
||||
| Publisher ID | repo secret `CWS_PUBLISHER_ID`, used by the publish API |
|
||||
|
||||
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
|
||||
@@ -187,7 +185,8 @@ Two workflows, for two different jobs.
|
||||
touching this folder: 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 for Chrome).
|
||||
to the `extension-latest` release, for installing by hand (the in-app Download page links the
|
||||
store listings instead).
|
||||
It never changes the version: AMO rejects a version number it has already signed, so CI signs a
|
||||
copy of the built manifest with the run number appended (e.g. `1.1.2.456`).
|
||||
|
||||
@@ -196,7 +195,8 @@ Polish time**, or whenever you start it (**Actions > Extension store release > R
|
||||
`gh workflow run extension-store-release.yml`), it releases whatever changed in the extension since
|
||||
the last release, with [semantic-release](https://semantic-release.gitbook.io/) (see below): bumps
|
||||
the version, writes `CHANGELOG.md`, commits both to `main`, tags it, creates a GitHub release with
|
||||
the store zips, and publishes to Edge Add-ons and Firefox Add-ons. If nothing releasable changed,
|
||||
the store zips, and publishes to the Chrome Web Store, Edge Add-ons and Firefox Add-ons. If nothing
|
||||
releasable changed,
|
||||
the run just ends.
|
||||
|
||||
### Versioning: how the next version is picked
|
||||
@@ -231,6 +231,24 @@ To see what the next release would be, without releasing anything (needs Node 22
|
||||
npm run release:dry-run
|
||||
```
|
||||
|
||||
### Publishing to the Chrome Web Store automatically
|
||||
|
||||
The store release's `publish-chrome` job publishes each new version to the Chrome Web Store,
|
||||
through the [Chrome Web Store API v2](https://developer.chrome.com/docs/webstore/using-api)
|
||||
(`scripts/publish-chrome.ts`): it signs in as the publisher's service account, uploads the exact
|
||||
`thingport-grab-chrome.zip` the release built, waits for Google to process it, and submits it for
|
||||
review. The update reaches users once Google's review passes, usually within a few days.
|
||||
|
||||
Like the other publish jobs it can be retried on its own with **Re-run failed jobs**, and it skips
|
||||
itself (with a notice in the run) until the credentials below exist. To test the script by hand:
|
||||
|
||||
```bash
|
||||
npm run zip:chrome
|
||||
CHROME_EXTENSION_ID=nmblahmglpbplmfcggghdgohohlaeiee CWS_PUBLISHER_ID=... \
|
||||
CWS_SERVICE_ACCOUNT_KEY="$(cat path/to/key.json)" \
|
||||
npx tsx scripts/publish-chrome.ts dist/zips/thingport-grab-chrome.zip
|
||||
```
|
||||
|
||||
### Publishing to Edge automatically
|
||||
|
||||
The store release's `publish-edge` job publishes each new version to Microsoft Edge Add-ons,
|
||||
@@ -277,6 +295,32 @@ npx web-ext sign --source-dir dist/firefox --channel unlisted \
|
||||
--api-key "$AMO_JWT_ISSUER" --api-secret "$AMO_JWT_SECRET"
|
||||
```
|
||||
|
||||
### One-time setup: Chrome Web Store publishing credentials
|
||||
|
||||
The job signs in as a Google Cloud service account that's linked to the store's publisher account:
|
||||
|
||||
1. In the [Google Cloud Console](https://console.cloud.google.com/), pick (or create) a project and
|
||||
enable the **Chrome Web Store API** (search for it in the top bar).
|
||||
2. **IAM & Admin > Service Accounts > Create service account.** It needs no roles or permissions.
|
||||
3. Open the service account, go to **Keys > Add key > Create new key > JSON**. The browser
|
||||
downloads the key file; Google keeps no copy, so if it's lost, create a new key and delete the
|
||||
old one on the same page.
|
||||
4. In the [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/devconsole),
|
||||
open **Account** and add the service account's email (`...@<project>.iam.gserviceaccount.com`,
|
||||
also the key file's `client_email`). A publisher can only have one service account.
|
||||
5. Add the key file and the publisher ID (shown in the dashboard's publisher settings) as repo
|
||||
secrets, then delete the local key file:
|
||||
|
||||
```bash
|
||||
gh secret set CWS_SERVICE_ACCOUNT_KEY < path/to/key.json
|
||||
gh secret set CWS_PUBLISHER_ID # paste the publisher ID when prompted
|
||||
```
|
||||
|
||||
6. `CHROME_EXTENSION_ID` is already a repo **variable** (see [Store listings](#store-listings)).
|
||||
|
||||
Service account keys don't expire, but an organization policy can block creating them
|
||||
(`iam.disableServiceAccountKeyCreation`) -- a personal Google account isn't affected.
|
||||
|
||||
### One-time setup: Edge publishing credentials
|
||||
|
||||
The job skips itself (with a notice in the run) until these exist:
|
||||
|
||||
+25
-15
@@ -10,9 +10,9 @@ It's useful without Thingport too: on MakerWorld model pages it adds a **Downloa
|
||||
button that turns the Bambu Studio project into a 3MF that PrusaSlicer, Cura and other slicers open
|
||||
with its colors and print settings intact. See [Download normalized](#download-normalized-no-thingport-needed).
|
||||
|
||||
**Get it from [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/) or
|
||||
**Get it from the [Chrome Web Store](https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee),
|
||||
[Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/) or
|
||||
[Microsoft Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol).**
|
||||
The Chrome build is on the [releases page](#install) until its store listing is live.
|
||||
|
||||
It talks directly to your Thingport instance's API from the extension's background script -- no
|
||||
separate server, no data sent anywhere else -- and converts files in your browser. See the [privacy policy](PRIVACY.md) for exactly what
|
||||
@@ -45,6 +45,29 @@ it stores and sends.
|
||||
|
||||
## Install
|
||||
|
||||
### Chrome / other Chromium browsers
|
||||
|
||||
Install **[Thingport Grab from the Chrome Web Store](https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee)**
|
||||
-- click **Add to Chrome**, 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. Chrome
|
||||
keeps it up to date from then on. Other Chromium browsers that install from the Chrome Web Store
|
||||
(Brave, Vivaldi, Opera with its Chrome extensions add-on) work the same way.
|
||||
|
||||
<details>
|
||||
<summary>Alternative: install the Chrome build by hand</summary>
|
||||
|
||||
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-chrome.zip` from the
|
||||
[`extension-latest` release](https://github.com/TautvydasDerzinskas/Thingport/releases/tag/extension-latest)
|
||||
and unzip it somewhere permanent (the browser loads the extension from that folder every time it
|
||||
starts, so don't delete it).
|
||||
2. Open `chrome://extensions` and turn on **Developer mode** (top right).
|
||||
3. Click **Load unpacked** and select the unzipped folder.
|
||||
|
||||
A hand-installed copy doesn't update itself -- repeat these steps for a newer version.
|
||||
</details>
|
||||
|
||||
### Firefox
|
||||
|
||||
Install **[Thingport Grab from Firefox Add-ons](https://addons.mozilla.org/firefox/addon/thingport-grab/)**
|
||||
@@ -90,19 +113,6 @@ Only needed if you can't use the store, e.g. to try a build before it's been pub
|
||||
A hand-installed copy doesn't update itself -- repeat these steps for a newer version.
|
||||
</details>
|
||||
|
||||
### 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` 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.
|
||||
|
||||
## Download normalized (no Thingport needed)
|
||||
|
||||
MakerWorld's files are Bambu Studio projects, which PrusaSlicer, Cura, Anycubic Slicer Next,
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Publishes a built Chrome zip to the existing Chrome Web Store listing, through the Chrome Web
|
||||
// Store API v2 (https://developer.chrome.com/docs/webstore/using-api): sign in as the publisher's
|
||||
// service account, upload the package, wait for it to be processed, and submit it for review.
|
||||
// Google's review then takes anywhere from hours to days -- this only gets the update into the queue.
|
||||
//
|
||||
// CHROME_EXTENSION_ID=... CWS_PUBLISHER_ID=... CWS_SERVICE_ACCOUNT_KEY="$(cat key.json)" \
|
||||
// tsx scripts/publish-chrome.ts dist/zips/thingport-grab-chrome.zip
|
||||
//
|
||||
// Run by .github/workflows/extension-store-release.yml (see extension/CONTRIBUTING.md for setting
|
||||
// up the credentials). CWS_API_BASE / CWS_TOKEN_URL override the endpoints, for testing against a mock.
|
||||
|
||||
import { createSign } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const API_BASE = process.env.CWS_API_BASE || "https://chromewebstore.googleapis.com";
|
||||
const TOKEN_URL = process.env.CWS_TOKEN_URL || "https://oauth2.googleapis.com/token";
|
||||
const SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
||||
const POLL_INTERVAL_MS = 5000;
|
||||
const POLL_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
|
||||
type UploadState = "SUCCEEDED" | "IN_PROGRESS" | "FAILED" | "NOT_FOUND" | "UPLOAD_STATE_UNSPECIFIED";
|
||||
|
||||
function requireEnv(name: string): string {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) throw new Error(`${name} isn't set`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const base64url = (data: string | Buffer) => Buffer.from(data).toString("base64url");
|
||||
|
||||
/** Exchanges a JWT signed with the service account's private key for an access token
|
||||
* (https://developers.google.com/identity/protocols/oauth2/service-account#httprest). */
|
||||
async function accessToken(keyJson: string): Promise<string> {
|
||||
let key: { client_email?: string; private_key?: string };
|
||||
try {
|
||||
key = JSON.parse(keyJson);
|
||||
} catch {
|
||||
throw new Error(
|
||||
"CWS_SERVICE_ACCOUNT_KEY isn't valid JSON -- paste the whole key file the Cloud Console downloaded",
|
||||
);
|
||||
}
|
||||
if (!key.client_email || !key.private_key)
|
||||
throw new Error("CWS_SERVICE_ACCOUNT_KEY has no client_email/private_key -- is it a service account key file?");
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const header = base64url(JSON.stringify({ alg: "RS256", typ: "JWT" }));
|
||||
const claims = base64url(
|
||||
JSON.stringify({ iss: key.client_email, scope: SCOPE, aud: TOKEN_URL, iat: now, exp: now + 3600 }),
|
||||
);
|
||||
const signature = createSign("RSA-SHA256").update(`${header}.${claims}`).sign(key.private_key);
|
||||
const res = await fetch(TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
assertion: `${header}.${claims}.${base64url(signature)}`,
|
||||
}),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(
|
||||
`Signing in as ${key.client_email} failed: HTTP ${res.status} ${await res.text().catch(() => "")}\n The key may have been deleted in the Cloud Console -- create a new one.`,
|
||||
);
|
||||
return ((await res.json()) as { access_token: string }).access_token;
|
||||
}
|
||||
|
||||
async function call<T>(url: string, init: RequestInit, what: string): Promise<T> {
|
||||
const res = await fetch(url, init);
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => "");
|
||||
const hint =
|
||||
res.status === 401 || res.status === 403
|
||||
? "\n Check that the service account is added under Account in the Chrome Web Store dashboard, the Chrome Web Store API is enabled in its Cloud project, and CWS_PUBLISHER_ID / CHROME_EXTENSION_ID are right."
|
||||
: "";
|
||||
throw new Error(`${what} failed: HTTP ${res.status} ${body}${hint}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const zipPath = process.argv[2];
|
||||
if (!zipPath) throw new Error("Usage: tsx scripts/publish-chrome.ts <package.zip>");
|
||||
const itemId = requireEnv("CHROME_EXTENSION_ID");
|
||||
const item = `publishers/${requireEnv("CWS_PUBLISHER_ID")}/items/${itemId}`;
|
||||
const auth = { Authorization: `Bearer ${await accessToken(requireEnv("CWS_SERVICE_ACCOUNT_KEY"))}` };
|
||||
|
||||
const zip = await readFile(zipPath);
|
||||
console.log(`Uploading ${zipPath} (${(zip.length / 1024).toFixed(1)} KB) to Chrome Web Store item ${itemId}…`);
|
||||
const upload = await call<{ uploadState?: UploadState; crxVersion?: string }>(
|
||||
`${API_BASE}/upload/v2/${item}:upload`,
|
||||
{ method: "POST", headers: { ...auth, "Content-Type": "application/zip" }, body: zip },
|
||||
"Upload",
|
||||
);
|
||||
|
||||
let state = upload.uploadState;
|
||||
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
||||
while (state === "IN_PROGRESS") {
|
||||
if (Date.now() > deadline) throw new Error(`Upload still in progress after ${POLL_TIMEOUT_MS / 60000} minutes`);
|
||||
await sleep(POLL_INTERVAL_MS);
|
||||
const status = await call<{ lastAsyncUploadState?: UploadState }>(
|
||||
`${API_BASE}/v2/${item}:fetchStatus`,
|
||||
{ headers: auth },
|
||||
"Upload status check",
|
||||
);
|
||||
state = status.lastAsyncUploadState;
|
||||
}
|
||||
if (state !== "SUCCEEDED")
|
||||
throw new Error(`Upload failed (${state ?? "no upload state"}): ${JSON.stringify(upload)}`);
|
||||
console.log(`Upload processed${upload.crxVersion ? `: version ${upload.crxVersion}` : ""}`);
|
||||
|
||||
const published = await call<{ state?: string; warningInfo?: unknown }>(
|
||||
`${API_BASE}/v2/${item}:publish`,
|
||||
{ method: "POST", headers: { ...auth, "Content-Type": "application/json" }, body: JSON.stringify({}) },
|
||||
"Submission",
|
||||
);
|
||||
if (published.warningInfo) console.log(`Warnings: ${JSON.stringify(published.warningInfo)}`);
|
||||
console.log(`Submitted for review: ${published.state ?? "ok"}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await main();
|
||||
} catch (err) {
|
||||
console.error(err instanceof Error ? err.message : err);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,24 +1,13 @@
|
||||
// Published to the fixed "extension-latest" tag, which the release workflow moves on every build,
|
||||
// rather than the repo-wide "latest" release that Bridge uses.
|
||||
const EXTENSION_RELEASE_BASE = "https://github.com/TautvydasDerzinskas/Thingport/releases/download/extension-latest";
|
||||
// The self-updating store versions. The zips and Firefox .xpi on the extension-latest release are
|
||||
// for installing by hand and are only linked from the docs.
|
||||
export type ExtensionDownload = { browser: "chrome" | "edge" | "firefox"; label: string; storeUrl: string };
|
||||
|
||||
export type ExtensionDownload =
|
||||
| { kind: "file"; browser: "chrome"; label: string; asset: string }
|
||||
| { kind: "store"; browser: "edge" | "firefox"; label: string; storeUrl: string };
|
||||
|
||||
export type ExtensionFileBrowser = Extract<ExtensionDownload, { kind: "file" }>["browser"];
|
||||
|
||||
// The Edge zip and Firefox .xpi are still on every release, but the self-updating store versions
|
||||
// are the ones to recommend.
|
||||
export const CHROME_WEB_STORE_URL = "https://chromewebstore.google.com/detail/nmblahmglpbplmfcggghdgohohlaeiee";
|
||||
export const EDGE_ADDONS_URL = "https://microsoftedge.microsoft.com/addons/detail/kahfidpmojfocohinlmglnfoaimocbol";
|
||||
export const FIREFOX_ADDONS_URL = "https://addons.mozilla.org/firefox/addon/thingport-grab/";
|
||||
|
||||
export const EXTENSION_DOWNLOADS: ExtensionDownload[] = [
|
||||
{ kind: "store", browser: "firefox", label: "Firefox", storeUrl: FIREFOX_ADDONS_URL },
|
||||
{ kind: "store", browser: "edge", label: "Edge", storeUrl: EDGE_ADDONS_URL },
|
||||
{ kind: "file", browser: "chrome", label: "Chrome", asset: "thingport-grab-chrome.zip" },
|
||||
{ browser: "chrome", label: "Chrome", storeUrl: CHROME_WEB_STORE_URL },
|
||||
{ browser: "firefox", label: "Firefox", storeUrl: FIREFOX_ADDONS_URL },
|
||||
{ browser: "edge", label: "Edge", storeUrl: EDGE_ADDONS_URL },
|
||||
];
|
||||
|
||||
export function extensionDownloadUrl(asset: string): string {
|
||||
return `${EXTENSION_RELEASE_BASE}/${asset}`;
|
||||
}
|
||||
@@ -564,18 +564,9 @@
|
||||
"intro": "A browser extension that imports MakerWorld, Thingiverse, and Printables models straight from their own pages.",
|
||||
"name": "Thingport Grab",
|
||||
"store": {
|
||||
"chrome": "Get from Chrome Web Store",
|
||||
"edge": "Get from Edge Add-ons",
|
||||
"firefox": "Get from Firefox Add-ons"
|
||||
},
|
||||
"modal": {
|
||||
"chrome": {
|
||||
"heading": "Install Thingport Grab",
|
||||
"step1": "Unzip the downloaded file somewhere permanent -- Chrome loads the extension from this folder every time it starts.",
|
||||
"step2": "Open chrome://extensions in a new tab.",
|
||||
"step3": "Turn on Developer mode (top right).",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
|
||||
@@ -566,18 +566,9 @@
|
||||
"intro": "Naršyklės plėtinys, importuojantis MakerWorld, Thingiverse ir Printables modelius tiesiai iš jų puslapių.",
|
||||
"name": "Thingport Grab",
|
||||
"store": {
|
||||
"chrome": "Gauti iš Chrome Web Store",
|
||||
"edge": "Gauti iš Edge Add-ons",
|
||||
"firefox": "Gauti iš Firefox Add-ons"
|
||||
},
|
||||
"modal": {
|
||||
"chrome": {
|
||||
"heading": "Įdiegti Thingport Grab",
|
||||
"step1": "Išarchyvuokite atsisiųstą failą kur nors nuolatinėje vietoje -- Chrome įkelia plėtinį iš šio aplanko kaskart paleidus naršyklę.",
|
||||
"step2": "Naujame skirtuke atidarykite chrome://extensions.",
|
||||
"step3": "Įjunkite Developer mode (viršuje dešinėje).",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
},
|
||||
"bridge": {
|
||||
|
||||
@@ -18,12 +18,7 @@ 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,
|
||||
type ExtensionFileBrowser,
|
||||
} from "../../constants/extension";
|
||||
import { EXTENSION_DOWNLOADS, type ExtensionDownload } 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.
|
||||
@@ -101,17 +96,6 @@ export default function DownloadPage() {
|
||||
usePageHeader({ title: t("sidebar.downloads") });
|
||||
|
||||
const [installOs, setInstallOs] = useState<BridgeDownload["os"] | null>(null);
|
||||
const [extensionInstallBrowser, setExtensionInstallBrowser] = useState<ExtensionFileBrowser | null>(null);
|
||||
|
||||
const extensionInstallSteps: Record<ExtensionFileBrowser, InstallStep[]> = {
|
||||
chrome: [
|
||||
{ text: t("download.extension.modal.chrome.step1") },
|
||||
{ text: t("download.extension.modal.chrome.step2") },
|
||||
{ text: t("download.extension.modal.chrome.step3") },
|
||||
{ text: t("download.extension.modal.chrome.step4") },
|
||||
{ text: t("download.extension.modal.chrome.step5") },
|
||||
],
|
||||
};
|
||||
|
||||
const installSteps: Record<BridgeDownload["os"], InstallStep[]> = {
|
||||
windows: [{ text: t("download.modal.windows.step1") }],
|
||||
@@ -168,30 +152,17 @@ export default function DownloadPage() {
|
||||
<Typography variant="subtitle2" fontWeight={600}>
|
||||
{t("download.extension.name")} ({item.label})
|
||||
</Typography>
|
||||
{item.kind === "store" ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={item.storeUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<OpenInNewIcon fontSize="small" />}
|
||||
>
|
||||
{t(`download.extension.store.${item.browser}`)}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
component="a"
|
||||
href={extensionDownloadUrl(item.asset)}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<DownloadIcon fontSize="small" />}
|
||||
onClick={() => setExtensionInstallBrowser(item.browser)}
|
||||
>
|
||||
{t("common:download")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
component="a"
|
||||
href={item.storeUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
startIcon={<OpenInNewIcon fontSize="small" />}
|
||||
>
|
||||
{t(`download.extension.store.${item.browser}`)}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
))}
|
||||
@@ -260,28 +231,6 @@ export default function DownloadPage() {
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={extensionInstallBrowser !== null}
|
||||
onClose={() => setExtensionInstallBrowser(null)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
>
|
||||
{extensionInstallBrowser && (
|
||||
<>
|
||||
<DialogTitle>{t(`download.extension.modal.${extensionInstallBrowser}.heading`)}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2.5 }}>
|
||||
{t("download.modal.startedNote")}
|
||||
</Typography>
|
||||
<InstallSteps steps={extensionInstallSteps[extensionInstallBrowser]} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setExtensionInstallBrowser(null)}>{t("common:close")}</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user