mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix: address card preview review comments
Retry thumbnail adoption for a cached render, reset card and model-page render state per print, scope the card-preview choice to the signed-in account and put it back when saving fails.
This commit is contained in:
1 parent
908ae80b5b
commit
678c4802d7
9 files changed
+73
-15
No files matched your search
@@ -316,11 +316,21 @@ export async function generateModelImage(
|
||||
): Promise<void> {
|
||||
const errorPath = modelImageErrorPath(plateId, kind);
|
||||
const key = `${kind}:${plateId}`;
|
||||
if (!canRenderModelImage(filename) || fsSync.existsSync(modelImagePath(plateId, kind))) return;
|
||||
if (!canRenderModelImage(filename)) return;
|
||||
const cachedPath = modelImagePath(plateId, kind);
|
||||
if (fsSync.existsSync(cachedPath)) {
|
||||
// Still run onReady, so a step that failed after an earlier render gets another go.
|
||||
try {
|
||||
await onReady?.(cachedPath);
|
||||
} catch (err) {
|
||||
console.error(`Model picture post-processing failed for plate ${plateId}:`, err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (imagesInFlight.has(key) || recentlyFailed(plateId, errorPath)) return;
|
||||
imagesInFlight.add(key);
|
||||
await scheduleRender("image", async () => {
|
||||
const dest = modelImagePath(plateId, kind);
|
||||
const dest = cachedPath;
|
||||
const tmp = `${dest}.${process.pid}.${Date.now()}.tmp`;
|
||||
try {
|
||||
const outcome = await renderPlateFile(plateId, srcPath, (from) =>
|
||||
|
||||
@@ -47,6 +47,7 @@ import { clearToken, clearUser, readToken, readUser, storeToken, storeUser } fro
|
||||
import { type AppSettings, type MakerWorldSettings, loadSettings, saveSettings } from "./utils/settings";
|
||||
import { useResolvedTheme } from "./hooks/useResolvedTheme";
|
||||
import { setCachedAccentColor, useAccentColor } from "./hooks/useAccentColor";
|
||||
import { resetCardPreview } from "./hooks/useCardPreview";
|
||||
import type { ResolvedTheme, ThemeSelection } from "./constants/settingsOptions";
|
||||
import { buildTheme } from "./theme";
|
||||
|
||||
@@ -353,6 +354,7 @@ export default function App() {
|
||||
const authRequired = health?.auth_required ?? true;
|
||||
|
||||
const handleLogin = (tok: string, ttl: number, loggedInUser: AuthUser) => {
|
||||
resetCardPreview();
|
||||
storeToken(tok);
|
||||
storeUser(loggedInUser);
|
||||
setToken(tok);
|
||||
|
||||
@@ -5,7 +5,7 @@ import Select from "@mui/material/Select";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { UnauthorizedError } from "../../../api/client";
|
||||
import { settingsApi } from "../../../api/settings";
|
||||
import { settingsApi, type CardPreview } from "../../../api/settings";
|
||||
import { matchLanguage, SUPPORTED_LANGUAGES } from "../../../constants/languages";
|
||||
import { ACCENT_COLORS, type AccentColor, type ThemeSelection } from "../../../constants/settingsOptions";
|
||||
import { setCachedAccentColor, useAccentColor } from "../../../hooks/useAccentColor";
|
||||
@@ -36,6 +36,17 @@ export default function AppearancePanel({ theme, onThemeChange, onUnauthorized }
|
||||
const accent = useAccentColor();
|
||||
const [cardPreview, setCardPreview] = useCardPreview();
|
||||
const [accentError, setAccentError] = React.useState<string | null>(null);
|
||||
const [cardPreviewError, setCardPreviewError] = React.useState<string | null>(null);
|
||||
|
||||
const changeCardPreview = async (next: CardPreview) => {
|
||||
setCardPreviewError(null);
|
||||
try {
|
||||
await setCardPreview(next);
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedError) onUnauthorized?.();
|
||||
else setCardPreviewError(t("appearance.cardPreview.failed"));
|
||||
}
|
||||
};
|
||||
|
||||
const changeAccent = async (next: AccentColor) => {
|
||||
const previous = accent;
|
||||
@@ -104,12 +115,17 @@ export default function AppearancePanel({ theme, onThemeChange, onUnauthorized }
|
||||
label={t("appearance.cardPreview.heading")}
|
||||
value={cardPreview ?? "images"}
|
||||
disabled={cardPreview === null}
|
||||
onChange={setCardPreview}
|
||||
onChange={(next) => void changeCardPreview(next)}
|
||||
options={[
|
||||
{ value: "images", label: t("appearance.cardPreview.images") },
|
||||
{ value: "renders", label: t("appearance.cardPreview.renders") },
|
||||
]}
|
||||
/>
|
||||
{cardPreviewError && (
|
||||
<Typography variant="caption" color="error" sx={{ display: "block", mt: 1 }}>
|
||||
{cardPreviewError}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<SectionLabel>{t("profile.language.heading")}</SectionLabel>
|
||||
|
||||
@@ -16,7 +16,7 @@ type Props<T extends string> = {
|
||||
bare?: boolean;
|
||||
};
|
||||
|
||||
/** Round colour swatches on Segmented's track; the chosen one is ringed and ticked. */
|
||||
/** Round colour swatches on Segmented's track, or bare; the chosen one is ringed and ticked. */
|
||||
export default function ColorSwatches<T extends string>({ value, options, onChange, label, bare }: Props<T>) {
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -118,7 +118,14 @@ export function renderPreviewContent(
|
||||
} else {
|
||||
fallback = <PreviewPlaceholder text={t("library:modelViewer.noPreview")} />;
|
||||
}
|
||||
return <CardPicture print={print} previewMode={previewMode} fallback={fallback} />;
|
||||
return (
|
||||
<CardPicture
|
||||
key={`${print.id}:${print.render_url ?? ""}`}
|
||||
print={print}
|
||||
previewMode={previewMode}
|
||||
fallback={fallback}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (is3d) {
|
||||
|
||||
@@ -5,19 +5,24 @@ import { settingsApi, type CardPreview } from "../api/settings";
|
||||
// switch shows everywhere at once.
|
||||
let cached: CardPreview | undefined;
|
||||
let inFlight: Promise<CardPreview> | null = null;
|
||||
// Bumped on sign-in, so an answer still on its way for the previous account is dropped.
|
||||
let account = 0;
|
||||
const listeners = new Set<(value: CardPreview) => void>();
|
||||
|
||||
function load(): Promise<CardPreview> {
|
||||
if (cached !== undefined) return Promise.resolve(cached);
|
||||
if (inFlight) return inFlight;
|
||||
const forAccount = account;
|
||||
// A switch made while this is in flight is newer than the server's answer, so it wins.
|
||||
inFlight ??= settingsApi
|
||||
const request = settingsApi
|
||||
.getCardPreview()
|
||||
.then((res) => (cached ??= res.preview))
|
||||
.then((res) => (forAccount === account ? (cached ??= res.preview) : res.preview))
|
||||
.catch((): CardPreview => cached ?? "images")
|
||||
.finally(() => {
|
||||
inFlight = null;
|
||||
if (inFlight === request) inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
inFlight = request;
|
||||
return request;
|
||||
}
|
||||
|
||||
function publish(value: CardPreview) {
|
||||
@@ -25,8 +30,16 @@ function publish(value: CardPreview) {
|
||||
listeners.forEach((listener) => listener(value));
|
||||
}
|
||||
|
||||
/** Null until the saved choice is known, so cards don't flash the wrong picture first. */
|
||||
export function useCardPreview(): [CardPreview | null, (preview: CardPreview) => void] {
|
||||
/** Forgets the previous account's choice; call on sign-in. */
|
||||
export function resetCardPreview() {
|
||||
account++;
|
||||
cached = undefined;
|
||||
inFlight = null;
|
||||
}
|
||||
|
||||
/** Null until the saved choice is known, so cards don't flash the wrong picture first. The setter shows the switch
|
||||
* straight away, puts it back if the save fails, and rejects with the save's error. */
|
||||
export function useCardPreview(): [CardPreview | null, (preview: CardPreview) => Promise<void>] {
|
||||
const [value, setValue] = useState<CardPreview | null>(cached ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,10 +54,16 @@ export function useCardPreview(): [CardPreview | null, (preview: CardPreview) =>
|
||||
};
|
||||
}, []);
|
||||
|
||||
const update = (preview: CardPreview) => {
|
||||
const update = async (preview: CardPreview) => {
|
||||
const previous = cached ?? "images";
|
||||
publish(preview);
|
||||
// The switch already happened here; a failed save only means other devices won't follow.
|
||||
settingsApi.updateCardPreview(preview).catch(() => {});
|
||||
try {
|
||||
await settingsApi.updateCardPreview(preview);
|
||||
} catch (err) {
|
||||
// Only if nothing newer was picked meanwhile.
|
||||
if (cached === preview) publish(previous);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return [value, update];
|
||||
|
||||
@@ -1589,6 +1589,7 @@
|
||||
"heading": "Model cards",
|
||||
"images": "Images",
|
||||
"renders": "3D renders",
|
||||
"failed": "Couldn't save the model card choice.",
|
||||
"help": "What model cards show first. Images: the pictures a model came with, or a 3D render where it has none. 3D renders: a render of every model in your accent color, or its pictures where the file can't be drawn."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1655,6 +1655,7 @@
|
||||
"heading": "Modelių kortelės",
|
||||
"images": "Nuotraukos",
|
||||
"renders": "3D vaizdai",
|
||||
"failed": "Nepavyko išsaugoti modelių kortelių pasirinkimo.",
|
||||
"help": "Ką modelių kortelės rodo pirmiausia. Nuotraukos: paveikslėliai, su kuriais modelis atkeliavo, arba 3D vaizdas, jei jų nėra. 3D vaizdai: kiekvieno modelio vaizdas jūsų akcento spalva arba jo nuotraukos, jei failo nupiešti nepavyksta."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -83,6 +83,7 @@ export default function ModelDetailPage({ theme, onSelectCategory, onUnauthorize
|
||||
if (cancelled) return;
|
||||
setPrint(data);
|
||||
setActiveImageIndex(0);
|
||||
setRenderUnavailable(false);
|
||||
} catch (err) {
|
||||
if (cancelled) return;
|
||||
if (err instanceof UnauthorizedError) {
|
||||
@@ -230,6 +231,7 @@ export default function ModelDetailPage({ theme, onSelectCategory, onUnauthorize
|
||||
</>
|
||||
) : showRender ? (
|
||||
<ModelRender
|
||||
key={print.id}
|
||||
url={print.render_url!}
|
||||
alt={print.title || print.name}
|
||||
onUnavailable={onRenderUnavailable}
|
||||
|
||||
Reference in new issue
Block a user