docs: update docs, screenshots & site

This commit is contained in:
Tautvydas Derzinskas committed 2026-10-08 19:20:54 +02:00
1 parent 1a88f8ffea
commit 9df01d0ed5
39 files changed
+216 -25

No files matched your search

+9 -1
View File
@@ -46,6 +46,7 @@ Instead of having your collection scattered across different websites and your f
- 🧊 **3D previews** — STL, 3MF, OBJ and STEP in the browser, with a thumbnail for every plate of a multi-plate 3MF. Very complex models can be simplified for the preview only.
- 🗂️ **Organize and search** — nested categories, collections, tags, favourites and browsing history, with full-text search across names, tags, notes and authors. Pin the collections and tags you use most to the sidebar.
- 📊 **Dashboard** — library stats, recently added models, your most viewed and most used models, and top authors.
- 📈 **Profile and activity** — a year-long activity heatmap and a month-by-month timeline of the models you imported, uploaded, downloaded, opened in a slicer or deleted, plus a read-only [API token](docs/guides/api-token.md) for connecting other apps to your library.
- 👤 **Author pages** — every model you've saved from an author, with a preview card when you hover their name.
- 🖨️ **Open in your slicer** — Bambu Studio, OrcaSlicer, PrusaSlicer, Cura, Creality Print, Anycubic Slicer Next, Elegoo Slicer and Snapmaker Orca, choosing the file when a model has several.
- 💡 **Normalized MakerWorld 3MFs** — slicers that mis-read Bambu Studio projects get an "Open normalized" option that keeps painted colours, plates and the designer's print settings.
@@ -74,6 +75,11 @@ Instead of having your collection scattered across different websites and your f
<td width="33%"><a href="frontend/src/assets/screenshots/08_my_models.png" target="_blank"><img src="frontend/src/assets/screenshots/08_my_models.png" width="100%" alt="My models"></a><br><sub><b>My Models</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/09_dark_theme.png" target="_blank"><img src="frontend/src/assets/screenshots/09_dark_theme.png" width="100%" alt="Dark theme"></a><br><sub><b>Dark Theme</b></sub></td>
</tr>
<tr>
<td width="33%"><a href="frontend/src/assets/screenshots/10_profile.png" target="_blank"><img src="frontend/src/assets/screenshots/10_profile.png" width="100%" alt="Profile with activity heatmap"></a><br><sub><b>Profile &amp; Activity</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/11_configuration_providers.png" target="_blank"><img src="frontend/src/assets/screenshots/11_configuration_providers.png" width="100%" alt="Configuration, Providers section"></a><br><sub><b>Configuration — Providers</b></sub></td>
<td width="33%"><a href="extension/docs/screenshots/collection-printables.jpg" target="_blank"><img src="extension/docs/screenshots/collection-printables.jpg" width="100%" alt="Thingport Grab importing a Printables collection"></a><br><sub><b>Thingport Grab — Collections</b></sub></td>
</tr>
<tr>
<td width="33%"><a href="extension/docs/screenshots/panel-printables.jpg" target="_blank"><img src="extension/docs/screenshots/panel-printables.jpg" width="100%" alt="Thingport Grab on Printables"></a><br><sub><b>Thingport Grab — Printables</b></sub></td>
<td width="33%"><a href="extension/docs/screenshots/panel-thingiverse.jpg" target="_blank"><img src="extension/docs/screenshots/panel-thingiverse.jpg" width="100%" alt="Thingport Grab on Thingiverse"></a><br><sub><b>Thingport Grab — Thingiverse</b></sub></td>
@@ -90,7 +96,9 @@ Thingport ships two small companion tools, each downloadable from the in-app Dow
## Provider Setup
Printables imports work with no setup. MakerWorld and Thingiverse each need a credential from your own account first -- see **[docs/PROVIDER_SETUP.md](docs/PROVIDER_SETUP.md)** for how to create a Thingiverse Access Token and how to grab a MakerWorld session cookie.
Printables imports work with no setup. MakerWorld and Thingiverse each need a credential from your own account first. Connect them under **Configuration → Providers** (from the user menu): right-click a provider to connect or disconnect it. See **[docs/PROVIDER_SETUP.md](docs/PROVIDER_SETUP.md)** for how to create a Thingiverse Access Token and how to grab a MakerWorld session cookie.
<a href="frontend/src/assets/screenshots/11_configuration_providers.png" target="_blank"><img src="frontend/src/assets/screenshots/11_configuration_providers.png" width="600" alt="Configuration, Providers section with MakerWorld, Thingiverse and Printables connected"></a>
## Bring Your Existing Library
+8 -4
View File
@@ -2,20 +2,24 @@
Importing from **Printables** works out of the box (its public API needs no credential). **MakerWorld** and **Thingiverse** each need a credential from your own account before Thingport can import from them.
Connect them under **Configuration → Providers**: open the user menu (your avatar, top right) and choose **Configuration**, or click one of the provider chips at the bottom of that menu. Right-click a provider to connect or disconnect it, or double-click one that isn't connected yet. A green tick means it's connected.
![Configuration, Providers section with MakerWorld, Thingiverse and Printables connected](../frontend/src/assets/screenshots/11_configuration_providers.png)
## Thingiverse Access Token
Instance-wide, admin-configured (Admin > Settings > Thingiverse Access Token) -- one token is shared by every user's Thingiverse imports.
Instance-wide, admin-configured (Configuration > Providers > Thingiverse, or Administration > Settings > Thingiverse Access Token) -- one token is shared by every user's Thingiverse imports. Other users see Thingiverse as connected once an admin has set it up.
1. Sign in to your Thingiverse account, then go to [thingiverse.com/apps/create](https://www.thingiverse.com/apps/create).
2. Register a new app. The name/description don't matter for this; pick whichever Application Type doesn't require a redirect URL (e.g. "Desktop" or "Mobile") -- a "Web App" asks for OAuth details you don't need here.
3. Once created, Thingiverse shows an **Access Token** for it. This is different from your Thingiverse login password -- it's a permanent credential tied to that registered app.
4. Copy it and paste it into Thingport's **Admin Settings > Thingiverse Access Token**, then Save. Thingport verifies the token against Thingiverse before storing it, so a mistyped or revoked token is rejected immediately with a clear error instead of only failing on the next import.
4. Copy it, right-click **Thingiverse** in Thingport's **Configuration > Providers**, choose **Connect** and paste it. Thingport verifies the token against Thingiverse before storing it, so a mistyped or revoked token is rejected immediately with a clear error instead of only failing on the next import.
Treat this token like a password: anyone who has it can use it to act as your Thingiverse app. If it's ever exposed, revoke it from your Thingiverse account's Apps page and generate a new one.
## MakerWorld cookie
Per-user (Profile > MakerWorld) -- MakerWorld has no developer API, so importing from it runs as _your own_ logged-in MakerWorld session, captured as a cookie value.
Per-user (Configuration > Providers > MakerWorld) -- MakerWorld has no developer API, so importing from it runs as _your own_ logged-in MakerWorld session, captured as a cookie value.
### Easiest: Thingport Grab
@@ -27,7 +31,7 @@ The [Thingport Grab](../extension/README.md) browser extension captures this aut
2. Open DevTools (`F12`, or `Cmd+Option+I` on macOS).
3. Chrome/Edge: go to the **Application** tab > **Cookies** > `https://makerworld.com`. Firefox: **Storage** tab > **Cookies** > `https://makerworld.com`.
4. Find the cookie named `token` and copy its **Value** column -- just that raw value, nothing else needs adding.
5. Paste it into Thingport's **Profile > MakerWorld** field and Save. Thingport verifies it against MakerWorld before storing it, so a stale or mistyped paste is rejected immediately instead of only failing on the next import.
5. Right-click **MakerWorld** in Thingport's **Configuration > Providers**, choose **Connect** and paste it. Thingport verifies it against MakerWorld before storing it, so a stale or mistyped paste is rejected immediately instead of only failing on the next import.
If there's no cookie named `token` (MakerWorld occasionally renames things), use the **Network** tab instead: reload the page, click any request to `makerworld.com`, open its Request Headers, and copy the whole `cookie:` value. Thingport only pulls the `token=...` part out of it and ignores the rest, so pasting the entire header works too.
+60
View File
@@ -0,0 +1,60 @@
# Connect other apps with an API token
An API token lets another app, script or dashboard read your Thingport library as you, without your password. It
can't change anything: it only reads.
Each user has their own token, and it only sees that user's library.
## Generate a token
1. Open the user menu (your avatar, top right) and choose **Profile**.
2. Next to **API token**, click **Generate**.
3. Copy the token straight away. It's shown only once, and Thingport keeps only a fingerprint of it, so it can't show
it to you again.
![The profile page, with the API token row under the email and password](../../frontend/src/assets/screenshots/10_profile.png)
The token starts with `tp_`. Afterwards your profile shows its last four characters, when it was created and when it
was last used, so you can tell which token is in use and whether anything is still using it.
You have one token at a time. To use Thingport from several apps, give them all the same token.
## Use the token
Send the token in the `Authorization` header of each request, as a bearer token. The API is at `/api` on the same
address you open Thingport at:
```bash
curl -H "Authorization: Bearer tp_your_token_here" https://thingport.example.com/api/dashboard/summary
```
That returns your library stats as JSON: how many models, collections, authors and categories you have, plus your most
viewed, most printed and most recently added models. Searching works the same way:
```bash
curl -H "Authorization: Bearer tp_your_token_here" "https://thingport.example.com/api/search?q=benchy"
```
The token works only in the header. Putting it in the address as `?token=...` is refused, because addresses end up in
browser history and proxy logs.
## What a token can and can't do
- **Read:** anything you can see in Thingport yourself, such as models, collections, tags, search, the dashboard and
your activity.
- **Not write:** any request that would add, change or delete something is refused with `403 API tokens are read-only`.
- **Not manage itself:** a token can't be used to generate a new token, revoke one or sign in, so a leaked token can't
be turned into anything more.
- **Not reach administration:** even an admin's token can't open the admin pages or settings, which hold instance
secrets such as email and Thingiverse credentials.
## Replace or revoke a token
On your **Profile**:
- **Regenerate** creates a new token. The old one stops working at once, so update anything that used it.
- **Revoke** removes the token. Anything using it loses access straight away.
Do either if a token might have leaked. Generating and revoking tokens are recorded in the admin logs.
Treat the token like a password: anyone who has it can read your whole library.
+1 -1
View File
@@ -42,7 +42,7 @@ the **provider site you're on**.
knows whether to show its button. On a MakerWorld collection page, the same check runs for each
model in the collection when you open the extension's panel there.
- The address of whatever you import, and your choice of collection, when you import it.
- A request for the slicer picked in your Thingport profile, when a MakerWorld model page opens, so
- A request for the slicer picked in your Thingport Configuration, when a MakerWorld model page opens, so
the extension knows whether to show its **Download normalized** button.
- If you're logged into MakerWorld in this browser: your MakerWorld session cookie, when you import
from MakerWorld. MakerWorld requires it to download files. The extension also saves it to your
+10 -6
View File
@@ -27,7 +27,11 @@ it stores and sends.
</tr>
<tr>
<td width="50%"><a href="docs/screenshots/panel-makerworld.jpg"><img src="docs/screenshots/panel-makerworld.jpg" alt="Import panel on a MakerWorld model page"></a><br><sub><b>MakerWorld</b> -- import panel on a model page</sub></td>
<td width="50%"><a href="docs/screenshots/panel-thingiverse.jpg"><img src="docs/screenshots/panel-thingiverse.jpg" alt="Import panel on a Thingiverse thing page"></a><br><sub><b>Thingiverse</b> -- import panel on a thing page</sub></td>
<td width="50%"><a href="docs/screenshots/panel-thingiverse.jpg"><img src="docs/screenshots/panel-thingiverse.jpg" alt="Import panel on a Thingiverse thing page, filing into a new collection"></a><br><sub><b>Thingiverse</b> -- import panel, filing into a new collection</sub></td>
</tr>
<tr>
<td width="50%"><a href="docs/screenshots/collection-printables.jpg"><img src="docs/screenshots/collection-printables.jpg" alt="Import panel on a Printables collection, listing its models"></a><br><sub><b>Printables collection</b> -- pick which models to import</sub></td>
<td width="50%"><a href="docs/screenshots/collection-makerworld.jpg"><img src="docs/screenshots/collection-makerworld.jpg" alt="Guided import of a MakerWorld collection, waiting 30 seconds between models"></a><br><sub><b>MakerWorld collection</b> -- guided import, paced between models</sub></td>
</tr>
<tr>
<td width="50%"><a href="docs/screenshots/setup-dialog-makerworld.jpg"><img src="docs/screenshots/setup-dialog-makerworld.jpg" alt="Setup dialog opened from the grayed-out icon"></a><br><sub><b>Not set up yet</b> -- the grayed-out icon's setup dialog</sub></td>
@@ -125,7 +129,7 @@ per-object overrides and layer color changes are dropped, and you pick your own
Hover the bulb on the button for the same summary.
- **Who sees it:** everyone who hasn't connected the extension to a Thingport instance, and people
who have, when their Thingport profile's slicer is one of those above (with Bambu Studio or
who have, when the slicer chosen in Thingport's Configuration > Slicer is one of those above (with Bambu Studio or
OrcaSlicer picked, the original file is the right one, so the button stays hidden).
- **Where it runs:** entirely in your browser, the same conversion Thingport's "Open normalized"
does on the server. The file goes nowhere but your downloads folder.
@@ -151,15 +155,15 @@ those no longer work.
### MakerWorld: no separate cookie setup needed
Importing from MakerWorld normally requires pasting a session cookie into Thingport's Profile
settings by hand (MakerWorld's own site sets it `HttpOnly`, which blocks a normal web page from
Importing from MakerWorld normally requires pasting a session cookie into Thingport's
Configuration > Providers by hand (MakerWorld's own site sets it `HttpOnly`, which blocks a normal web page from
reading it -- that's the whole reason for the manual copy/paste). This extension reads that same
cookie directly from your browser instead, using the `cookies` API -- a privileged, extension-only
capability explicitly allowed to read `HttpOnly` cookies, unlike a regular page's own JavaScript.
It's sent only to your own Thingport instance, as part of the same import request that needs it,
exactly like the cookie you'd otherwise paste in by hand -- never anywhere else. If your Thingport
account doesn't already have a MakerWorld cookie saved, the extension also pushes this one to
Profile > MakerWorld for you, so the plain web app's own imports benefit too, not just ones started
account doesn't already have a MakerWorld cookie saved, the extension also connects MakerWorld in
Configuration > Providers for you, so the plain web app's own imports benefit too, not just ones started
from the extension.
## What counts as "importable"
Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 576 KiB

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 525 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 305 KiB

+64 -3
View File
@@ -8,6 +8,9 @@
//
// npm run screenshots -- --makerworld=https://makerworld.com/en/models/123 --printables=...
//
// The collection shots use fixed collections, changeable with --printables-collection=<url> and
// --makerworld-collection=<url>.
//
// Needs Playwright's Chromium once: `npx playwright install chromium`.
import { mkdir, rm } from "node:fs/promises";
@@ -17,6 +20,7 @@ import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { chromium, type BrowserContext, type Page, type Worker } from "playwright";
import type { BatchEntriesResult } from "../src/shared/api";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const EXTENSION_DIR = path.join(ROOT, "dist", "chrome");
@@ -24,6 +28,9 @@ const OUT_DIR = path.join(ROOT, "docs", "screenshots");
const PAGE_VIEWPORT = { width: 1280, height: 800 };
const DEMO_HOST = "thingport.home.arpa";
const POPUP_WIDTH = 320;
const NEW_COLLECTION_NAME = "Desk organizers";
const PRINTABLES_COLLECTION = "https://www.printables.com/@Thinkable/collections/342542";
const MAKERWORLD_COLLECTION = "https://makerworld.com/en/collections/15756490-little-coin";
type ProviderName = "makerworld" | "thingiverse" | "printables";
const PROVIDERS: { name: ProviderName; label: string; home: string; modelLink: RegExp }[] = [
@@ -48,7 +55,7 @@ const PROVIDERS: { name: ProviderName; label: string; home: string; modelLink: R
];
/** CORS is open, so the extension reaches it without a host permission. */
type MockState = { inspectTitle: string | null };
type MockState = { inspectTitle: string | null; batch: BatchEntriesResult | null };
function startMockInstance(state: MockState): Promise<{ url: string; close: () => void }> {
const collections = [
@@ -58,7 +65,7 @@ function startMockInstance(state: MockState): Promise<{ url: string; close: () =
];
const server = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Thingport-Client");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PATCH, OPTIONS");
if (req.method === "OPTIONS") {
res.writeHead(204).end();
@@ -74,13 +81,19 @@ function startMockInstance(state: MockState): Promise<{ url: string; close: () =
return json({ already_imported: false, state: "not_imported", print_id: null });
case "POST /api/import/inspect":
return json({ title: state.inspectTitle, is_zip: false });
case "POST /api/import/printables-collection/entries":
return json(state.batch ?? { title: null, entries: [] });
case "GET /api/collections":
return json(collections);
case "POST /api/import":
return json({ id: "demo-imported", title: state.inspectTitle, import_outcome: "created", thumb_url: null });
// No slicer, so MakerWorld pages don't add their "Download normalized" button.
case "GET /api/settings/slicer":
return json({ slicer: null });
case "PATCH /api/settings/makerworld":
return json({});
default:
console.warn(` mock instance: ${route} isn't mocked`);
return json({ detail: `Not mocked: ${route}` }, 404);
}
});
@@ -162,6 +175,31 @@ async function readModelInfo(page: Page): Promise<ModelInfo> {
return { url: page.url(), title, image: meta.image };
}
/** What the instance would list for a Printables collection, read from the page itself. */
async function readPrintablesCollection(page: Page): Promise<BatchEntriesResult> {
await page.locator('a[href*="/model/"]').first().waitFor({ timeout: 30000 });
const { title, links } = await page.evaluate(() => ({
title: document.querySelector("h1")?.textContent?.trim() || null,
links: [...document.querySelectorAll<HTMLAnchorElement>('a[href*="/model/"]')].map((a) => ({
href: a.href,
text: a.textContent?.trim() || "",
})),
}));
const titles = new Map<string, string>();
for (const { href, text } of links) {
const id = href.match(/\/model\/(\d+)/)?.[1];
// A card links its model twice: the image (no text), then the name.
if (id && !titles.get(id)) titles.set(id, text);
}
const entries = [...titles].map(([id, name], i) => ({
design_id: id,
title: name || null,
// A couple already in the library, to show how those are marked.
already_imported: i === 1 || i === 4,
}));
return { title, entries };
}
async function toDataUrl(imageUrl: string | null): Promise<string | null> {
if (!imageUrl) return null;
try {
@@ -197,7 +235,7 @@ async function screenshotViewport(page: Page, file: string): Promise<void> {
async function main(): Promise<void> {
await rm(OUT_DIR, { recursive: true, force: true });
await mkdir(OUT_DIR, { recursive: true });
const mockState: MockState = { inspectTitle: null };
const mockState: MockState = { inspectTitle: null, batch: null };
const instance = await startMockInstance(mockState);
const mockPort = Number(new URL(instance.url).port);
const instanceUrl = `http://${DEMO_HOST}`;
@@ -249,6 +287,10 @@ async function main(): Promise<void> {
await openPage(page, model.url);
await page.locator(".tg-fab:not(.tg-fab--inactive)").click();
await page.locator('.tg-panel [data-action="import"]').waitFor({ timeout: 20000 });
if (provider.name === "thingiverse") {
await page.locator("#tg-collection").selectOption("__new__");
await page.locator("#tg-new-collection").fill(NEW_COLLECTION_NAME);
}
await screenshotViewport(page, `panel-${provider.name}.jpg`);
// Not MakerWorld: its import clicks the page's real Download button.
@@ -258,6 +300,25 @@ async function main(): Promise<void> {
await screenshotViewport(page, `imported-${provider.name}.jpg`);
}
}
const printablesCollection = argValue("printables-collection") ?? PRINTABLES_COLLECTION;
await openPage(page, printablesCollection);
mockState.batch = await readPrintablesCollection(page);
console.log(`Printables collection: ${printablesCollection} -- ${mockState.batch.entries.length} models`);
await page.locator(".tg-fab:not(.tg-fab--inactive)").click();
await page.locator('.tg-panel [data-action="import"]').waitFor({ timeout: 20000 });
await screenshotViewport(page, "collection-printables.jpg");
// The panel comes up once the whole collection has been scrolled through.
const makerworldCollection = argValue("makerworld-collection") ?? MAKERWORLD_COLLECTION;
await openPage(page, makerworldCollection);
console.log(`MakerWorld collection: ${makerworldCollection}`);
await page.locator(".tg-fab:not(.tg-fab--inactive)").click();
await page.locator('.tg-panel [data-action="start"]').waitFor({ timeout: 180000 });
await page.locator("#tg-step-delay").selectOption("30000");
await page.evaluate(() => window.scrollTo(0, 0));
await screenshotViewport(page, "collection-makerworld.jpg");
console.log(`Screenshots written to ${path.relative(process.cwd(), OUT_DIR)}/`);
} finally {
await context.close();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 378 KiB

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 705 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+32 -7
View File
@@ -72,7 +72,15 @@ async function scrollToTop(page) {
async function openUserMenu(page, displayName) {
await page.getByRole("button", { name: displayName }).click();
await page.getByRole("menuitem", { name: "Theme" }).waitFor();
await page.getByRole("menuitem", { name: "Configuration" }).waitFor();
}
async function openConfiguration(page, displayName, tab) {
await openUserMenu(page, displayName);
await page.getByRole("menuitem", { name: "Configuration" }).click();
const dialog = page.getByRole("dialog");
await dialog.getByRole("button", { name: tab, exact: true }).click();
return dialog;
}
const SHOTS = [
@@ -143,18 +151,33 @@ const SHOTS = [
run: async (page, ctx) => {
await page.goto("/models");
await settle(page, 0);
await openUserMenu(page, ctx.displayName);
await page.getByRole("menuitem", { name: "Theme" }).click();
await page.getByRole("menuitem", { name: "Dark" }).click();
const dialog = await openConfiguration(page, ctx.displayName, "Appearance");
await dialog.getByRole("button", { name: "Dark", exact: true }).click();
await page.keyboard.press("Escape");
await dialog.waitFor({ state: "hidden" });
await settle(page, 0);
// Shown with the choice that made it: the user menu and its theme list.
await openUserMenu(page, ctx.displayName);
await page.getByRole("menuitem", { name: "Theme" }).click();
await page.getByRole("menuitem", { name: "Dark" }).waitFor();
await scrollToTop(page);
},
},
{
name: "10_profile",
run: async (page) => {
await page.goto("/profile");
await page
.getByText(/activit(y|ies) in /)
.first()
.waitFor();
},
},
{
name: "11_configuration_providers",
run: async (page, ctx) => {
await page.goto("/models");
await settle(page, 0);
await openConfiguration(page, ctx.displayName, "Providers");
},
},
];
function wanted(shot) {
@@ -220,6 +243,8 @@ try {
const ctx = { displayName, modelId };
for (const shot of SHOTS.filter(wanted)) {
process.stdout.write(`${shot.name}... `);
// 09 switches to dark through the UI, which saves it to the account.
await api(token, "PATCH", "/settings/theme", { theme: "light" });
await shot.run(page, ctx);
await settle(page);
await page.screenshot({ path: path.join(outDir, `${shot.name}.png`) });
+1
View File
@@ -27,6 +27,7 @@ const PATHS = {
heart: "M12 21s-8-5.2-8-11a4.5 4.5 0 0 1 8-2.8A4.5 4.5 0 0 1 20 10c0 5.8-8 11-8 11Z",
rss: "M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z",
check: "m5 12 5 5L20 7",
activity: "M3 12h4l3-8 4 16 3-8h4",
copy: "M9 9h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1Zm-4 6H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1",
};
+8
View File
@@ -123,6 +123,14 @@ export const DOCS = [
description:
"A small desktop helper that makes “Open in slicer” work for Bambu Studio, PrusaSlicer, Cura and Anycubic Slicer Next.",
},
{
slug: "guides/api-token",
source: "docs/guides/api-token.md",
group: "Integrations",
title: "Connect other apps with an API token",
nav: "API token",
description: "Generate a read-only token that lets other apps, scripts and dashboards read your Thingport library.",
},
{
slug: "contributing",
source: "CONTRIBUTING.md",
+19 -3
View File
@@ -8,8 +8,9 @@ import preview3d from "../../../frontend/src/assets/screenshots/04_model_details
import tags from "../../../frontend/src/assets/screenshots/06_tags.png";
import downloads from "../../../frontend/src/assets/screenshots/07_downloads.png";
import myModels from "../../../frontend/src/assets/screenshots/08_my_models.png";
import profile from "../../../frontend/src/assets/screenshots/10_profile.png";
import grabMakerworld from "../../../extension/docs/screenshots/panel-makerworld.jpg";
import grabImported from "../../../extension/docs/screenshots/imported-printables.jpg";
import grabCollection from "../../../extension/docs/screenshots/collection-printables.jpg";
import folderImport from "../../../docs/guides/images/folder-import-mode.png";
const SECTIONS = [
@@ -65,8 +66,8 @@ const SECTIONS = [
"Talks only to your own Thingport instance. Nothing goes anywhere else.",
],
link: { label: "Thingport Grab", href: url("docs/grab/") },
image: grabImported,
alt: "Thingport Grab after importing a Printables model, with an Open in Thingport button",
image: grabCollection,
alt: "Thingport Grab on a Printables collection, picking which models to import",
},
{
id: "preview",
@@ -101,6 +102,21 @@ const SECTIONS = [
image: tags,
alt: "Browsing models by tag in Thingport",
},
{
id: "activity",
short: "Activity",
icon: "activity",
title: "See what you've been up to",
intro: "Your profile keeps a record of how you use your library.",
points: [
"A year-long activity heatmap, like the one on your GitHub profile, with earlier years one click away.",
"A month-by-month timeline of the models you imported, uploaded, downloaded, opened in a slicer or deleted.",
"Generate a read-only API token to connect other apps and services to your library, and revoke it whenever you like.",
],
link: { label: "Connect other apps", href: url("docs/guides/api-token/") },
image: profile,
alt: "A Thingport profile with its activity heatmap and timeline",
},
{
id: "print",
short: "Print",
+4
View File
@@ -11,6 +11,8 @@ import models from "../../../frontend/src/assets/screenshots/02_models.png";
import modelDetails from "../../../frontend/src/assets/screenshots/03_model_details.png";
import preview3d from "../../../frontend/src/assets/screenshots/04_model_details_3d_preview.png";
import collections from "../../../frontend/src/assets/screenshots/05_collections.png";
import profile from "../../../frontend/src/assets/screenshots/10_profile.png";
import configProviders from "../../../frontend/src/assets/screenshots/11_configuration_providers.png";
import grabPrintables from "../../../extension/docs/screenshots/panel-printables.jpg";
const FEATURES = [
@@ -163,6 +165,8 @@ const jsonLd = [
{ img: modelDetails, caption: "Every model's files, photos and source" },
{ img: preview3d, caption: "Inspect models in 3D" },
{ img: collections, caption: "Group models into collections" },
{ img: profile, caption: "Look back on your activity" },
{ img: configProviders, caption: "Connect the sites you import from" },
].map(({ img, caption }) => (
<figure class="shot">
<Image src={img} alt={caption} widths={[480, 800, 1100]} sizes="(min-width: 48rem) 34rem, 100vw" />