mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-19 05:10:01 -05:00
feat(extension): add loading state phrases for different operations
This commit is contained in:
1 parent
67a31cc940
commit
0325f16c85
4 files changed
+110
-34
No files matched your search
@@ -6,6 +6,7 @@ import { createIcon } from "../../shared/icon";
|
||||
import { NORMALIZE_3MF_SLICERS } from "../../shared/slicers";
|
||||
import { api } from "../runtime";
|
||||
import css from "../styles/normalized.scss?inline";
|
||||
import { gettingFilePhrases, normalizingPhrases, rotatePhrases } from "../panels/funStatus";
|
||||
import { resolveMakerworldDownloadUrl } from "./downloadResolver";
|
||||
import { DOWNLOAD_3MF, DOWNLOAD_OTHER, DOWNLOAD_STL, OPEN_IN_APP } from "./labels";
|
||||
import { normalizeBambu3mf } from "./normalize";
|
||||
@@ -110,9 +111,9 @@ function saveFile(bytes: Uint8Array, filename: string): void {
|
||||
}
|
||||
|
||||
async function downloadNormalized(pageUrl: string, onProgress: (text: string) => void): Promise<void> {
|
||||
onProgress("Getting file…");
|
||||
let stopRotation = rotatePhrases(gettingFilePhrases, onProgress);
|
||||
// The normalizer reads the 3MF project, never the raw-files zip.
|
||||
const resolved = await resolveMakerworldDownloadUrl(pageUrl, { profileOnly: true });
|
||||
const resolved = await resolveMakerworldDownloadUrl(pageUrl, { profileOnly: true }).finally(stopRotation);
|
||||
if (!resolved) {
|
||||
throw new Error(
|
||||
"Couldn't get this model's file from MakerWorld. Make sure you're signed in to MakerWorld, then try again.",
|
||||
@@ -126,10 +127,10 @@ async function downloadNormalized(pageUrl: string, onProgress: (text: string) =>
|
||||
throw tooBig(declared);
|
||||
}
|
||||
const input = await readBody(res, declared, onProgress);
|
||||
onProgress("Normalizing…");
|
||||
stopRotation = rotatePhrases(normalizingPhrases, onProgress);
|
||||
let output: Uint8Array;
|
||||
try {
|
||||
output = await normalizeBambu3mf(input);
|
||||
output = await normalizeBambu3mf(input).finally(stopRotation);
|
||||
} catch (err) {
|
||||
throw new Error("Couldn't normalize this file: it doesn't look like a 3MF project MakerWorld's Download gives.", {
|
||||
cause: err,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ctx } from "../context";
|
||||
import { api, escapeHtml, sleep } from "../runtime";
|
||||
import { isPanelMounted, onPanelAction, panelQueryAll, renderPanel } from "../shell";
|
||||
import { errorHtml, statusHtml, successHtml } from "./results";
|
||||
import { loadingModelsPhrases, renderFunStatus } from "./funStatus";
|
||||
|
||||
type BatchKey = "thingiverse:likes" | "thingiverse:collection" | "printables:collection";
|
||||
|
||||
@@ -35,7 +36,7 @@ function endpoints() {
|
||||
}
|
||||
|
||||
export async function loadBatchEntries(): Promise<void> {
|
||||
renderPanel(statusHtml("Loading models…"));
|
||||
renderFunStatus(loadingModelsPhrases(ctx().classification.provider));
|
||||
let result: BatchEntriesResult;
|
||||
try {
|
||||
result = await api<BatchEntriesResult>("POST", endpoints().entries, { url: ctx().url });
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Loading states that cycle through short, playful phrases about the work in progress, so a wait of
|
||||
// a few seconds reads as something happening rather than a stuck panel.
|
||||
// a few seconds reads as something happening rather than a stuck panel. Each set describes its own
|
||||
// step; the first phrase is the plain description.
|
||||
|
||||
import type { Provider } from "../../shared/urls";
|
||||
import { escapeHtml } from "../runtime";
|
||||
import { panelQuery, renderPanel } from "../shell";
|
||||
|
||||
const ROTATE_MS = 2500;
|
||||
@@ -13,47 +13,121 @@ const SITE_NAMES: Record<Provider, string> = {
|
||||
thingiverse: "Thingiverse",
|
||||
};
|
||||
|
||||
/** While the instance inspects the link: what file it is, its title, whether it's a zip. */
|
||||
/** The instance inspects the link: finds the file, reads its title, checks whether it's a zip. */
|
||||
export function checkingLinkPhrases(provider: Provider): string[] {
|
||||
return [
|
||||
"Checking the link…",
|
||||
`Asking ${SITE_NAMES[provider]} nicely…`,
|
||||
"Sniffing out the file…",
|
||||
"Peeking behind the download button…",
|
||||
"Crunching the model…",
|
||||
"Taking the photos…",
|
||||
"Reading the name tag…",
|
||||
"Measuring twice…",
|
||||
"Counting the layers…",
|
||||
"Warming up the nozzle…",
|
||||
"Leveling the bed…",
|
||||
"Unspooling the filament…",
|
||||
"Checking it's really a model…",
|
||||
"Shaking the box to hear what's inside…",
|
||||
"Reading the fine print…",
|
||||
"Untangling the supports…",
|
||||
];
|
||||
}
|
||||
|
||||
let rotation: ReturnType<typeof setInterval> | undefined;
|
||||
/** The instance downloads the file, builds the model, its plates and gallery, and files it. */
|
||||
export function importingPhrases(provider: Provider): string[] {
|
||||
return [
|
||||
"Importing…",
|
||||
`Carrying it over from ${SITE_NAMES[provider]}…`,
|
||||
"Downloading the file…",
|
||||
"Crunching the model…",
|
||||
"Unpacking the plates…",
|
||||
"Taking the photos…",
|
||||
"Framing the thumbnail…",
|
||||
"Writing the label…",
|
||||
"Crediting the designer…",
|
||||
"Finding it a shelf…",
|
||||
];
|
||||
}
|
||||
|
||||
/** Shows the first phrase, then a random other one every few seconds (never the same twice in a
|
||||
* row) until the panel renders something else. */
|
||||
export function renderFunStatus(phrases: readonly string[]): void {
|
||||
clearInterval(rotation);
|
||||
renderPanel(`<div class="tg-status tg-status--fun">${escapeHtml(phrases[0])}</div>`);
|
||||
const el = panelQuery<HTMLElement>(".tg-status--fun");
|
||||
if (!el || phrases.length < 2) return;
|
||||
/** "Update model in Thingport": fills empty details and images, adds files it doesn't hold yet. */
|
||||
export function updatingPhrases(provider: Provider): string[] {
|
||||
return [
|
||||
"Updating the model…",
|
||||
`Checking ${SITE_NAMES[provider]} for anything new…`,
|
||||
"Filling in the blanks…",
|
||||
"Looking for missing photos…",
|
||||
"Hunting for new files…",
|
||||
"Leaving your edits alone…",
|
||||
"Comparing notes…",
|
||||
"Dusting off the shelf…",
|
||||
];
|
||||
}
|
||||
|
||||
/** The instance downloads a zip to list the files inside it. */
|
||||
export const zipFilesPhrases = [
|
||||
"Loading files…",
|
||||
"Downloading the archive…",
|
||||
"Unzipping carefully…",
|
||||
"Counting what's inside…",
|
||||
"Peeking into every folder…",
|
||||
"Sorting the models from the extras…",
|
||||
"Untangling the file names…",
|
||||
];
|
||||
|
||||
/** The instance pages through a collection or likes list and checks which are in the library. */
|
||||
export function loadingModelsPhrases(provider: Provider): string[] {
|
||||
return [
|
||||
"Loading models…",
|
||||
`Flipping through ${SITE_NAMES[provider]}…`,
|
||||
"Paging through the list…",
|
||||
"Counting the models…",
|
||||
"Gathering the thumbnails…",
|
||||
"Checking what's already on your shelf…",
|
||||
"Lining them up…",
|
||||
];
|
||||
}
|
||||
|
||||
/** Short, for the "Download normalized" button: MakerWorld hands over the 3MF. */
|
||||
export const gettingFilePhrases = ["Getting file…", "Finding the 3MF…", "Asking MakerWorld…"];
|
||||
|
||||
/** Short, for the "Download normalized" button: the Bambu project becomes a plain 3MF. */
|
||||
export const normalizingPhrases = [
|
||||
"Normalizing…",
|
||||
"Keeping the colors…",
|
||||
"Translating settings…",
|
||||
"Repacking plates…",
|
||||
"Smoothing it out…",
|
||||
];
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
/** Shows the first phrase, then a random other one every few seconds, never the same twice in a
|
||||
* row. Returns the function that stops it. */
|
||||
export function rotatePhrases(phrases: readonly string[], show: (phrase: string) => void): () => void {
|
||||
show(phrases[0]);
|
||||
if (phrases.length < 2) return noop;
|
||||
let current = 0;
|
||||
rotation = setInterval(() => {
|
||||
if (!el.isConnected) {
|
||||
clearInterval(rotation);
|
||||
return;
|
||||
}
|
||||
const timer = setInterval(() => {
|
||||
let next = Math.floor(Math.random() * (phrases.length - 1));
|
||||
if (next >= current) next += 1;
|
||||
current = next;
|
||||
el.textContent = phrases[current];
|
||||
show(phrases[current]);
|
||||
}, ROTATE_MS);
|
||||
return () => clearInterval(timer);
|
||||
}
|
||||
|
||||
let stopPanelRotation = noop;
|
||||
|
||||
/** The panel's status line, rotating until the panel renders something else. */
|
||||
export function renderFunStatus(phrases: readonly string[]): void {
|
||||
stopPanelRotation();
|
||||
renderPanel(`<div class="tg-status tg-status--fun"></div>`);
|
||||
const el = panelQuery<HTMLElement>(".tg-status--fun");
|
||||
if (!el) return;
|
||||
stopPanelRotation = rotatePhrases(phrases, (phrase) => {
|
||||
if (!el.isConnected) {
|
||||
stopPanelRotation();
|
||||
return;
|
||||
}
|
||||
el.textContent = phrase;
|
||||
// Restarts the fade-in on every change.
|
||||
el.classList.remove("tg-status--fun");
|
||||
void el.offsetWidth;
|
||||
el.classList.add("tg-status--fun");
|
||||
}, ROTATE_MS);
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import { api, escapeHtml } from "../runtime";
|
||||
import { onPanelAction, panelQuery, panelQueryAll, renderPanel } from "../shell";
|
||||
import { collectionPickerHtml, selectedCollectionId } from "./collectionPicker";
|
||||
import { errorHtml, statusHtml, successHtml } from "./results";
|
||||
import { checkingLinkPhrases, renderFunStatus } from "./funStatus";
|
||||
import { checkingLinkPhrases, importingPhrases, renderFunStatus, updatingPhrases, zipFilesPhrases } from "./funStatus";
|
||||
|
||||
/** Model name for pages that skip /import/inspect. The <h1> first: Thingiverse's og:title goes
|
||||
* stale on SPA navigation and Printables' has a suffix. */
|
||||
@@ -154,7 +154,7 @@ function addProfileHtml(profilesPicker: string): string {
|
||||
async function runReimport(): Promise<void> {
|
||||
const { library, instanceUrl } = ctx();
|
||||
if (!library?.printId) return;
|
||||
renderPanel(statusHtml("Updating model in Thingport…"));
|
||||
renderFunStatus(updatingPhrases(ctx().classification.provider));
|
||||
try {
|
||||
await api("POST", `/print/${library.printId}/reimport`, { metadata: true, files: true, images: true });
|
||||
const link = `${instanceUrl}/models/${library.printId}`;
|
||||
@@ -165,7 +165,7 @@ async function runReimport(): Promise<void> {
|
||||
}
|
||||
|
||||
async function loadZipEntries(): Promise<void> {
|
||||
renderPanel(statusHtml("Loading files…"));
|
||||
renderFunStatus(zipFilesPhrases);
|
||||
let result: ZipEntriesResult;
|
||||
try {
|
||||
result = await api<ZipEntriesResult>("POST", "/import/zip/entries", { url: ctx().url });
|
||||
@@ -204,7 +204,7 @@ async function runDirectImport(opts?: { entries?: string[] }): Promise<void> {
|
||||
return;
|
||||
}
|
||||
const { url, instanceUrl, classification, title } = ctx();
|
||||
renderPanel(statusHtml("Importing…"));
|
||||
renderFunStatus(importingPhrases(classification.provider));
|
||||
const resolved =
|
||||
classification.provider === "makerworld" && classification.type === "model"
|
||||
? await resolveMakerworldDownloadUrl(url).catch(() => null)
|
||||
|
||||
Reference in new issue
Block a user