mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
fix: order theme saves, reset hover slideshow per account, address review
This commit is contained in:
1 parent
4c301bb460
commit
6dac5555e7
9 files changed
+85
-45
No files matched your search
@@ -48,6 +48,7 @@ import { type AppSettings, type MakerWorldSettings, loadSettings, saveSettings }
|
||||
import { useResolvedTheme } from "./hooks/useResolvedTheme";
|
||||
import { setCachedAccentColor, setCachedRenderColor, useAccentColor } from "./hooks/useAccentColor";
|
||||
import { resetCardPreview } from "./hooks/useCardPreview";
|
||||
import { resetHoverSlideshowEnabled } from "./hooks/useHoverSlideshowEnabled";
|
||||
import { DEFAULT_ACCENT, type ResolvedTheme, type ThemeSelection } from "./constants/settingsOptions";
|
||||
import { buildTheme } from "./theme";
|
||||
|
||||
@@ -367,6 +368,7 @@ export default function App() {
|
||||
|
||||
const handleLogin = (tok: string, ttl: number, loggedInUser: AuthUser) => {
|
||||
resetCardPreview();
|
||||
resetHoverSlideshowEnabled();
|
||||
storeToken(tok);
|
||||
storeUser(loggedInUser);
|
||||
setToken(tok);
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "../../../hooks/useAccentColor";
|
||||
import { setCachedAuthorPreviewEnabled, useAuthorPreviewEnabled } from "../../../hooks/useAuthorPreviewEnabled";
|
||||
import { useCardPreview } from "../../../hooks/useCardPreview";
|
||||
import { setCachedHoverSlideshowEnabled, useHoverSlideshowEnabled } from "../../../hooks/useHoverSlideshowEnabled";
|
||||
import { useHoverSlideshowEnabled } from "../../../hooks/useHoverSlideshowEnabled";
|
||||
import { accentSwatch, renderSwatch } from "../../../theme";
|
||||
import ColorSwatches from "../../controls/ColorSwatches";
|
||||
import Segmented from "../../controls/Segmented";
|
||||
@@ -52,12 +52,19 @@ export default function AppearancePanel({ theme, onThemeChange, onUnauthorized }
|
||||
const [accentError, setAccentError] = React.useState<string | null>(null);
|
||||
const renderColor = useRenderColor();
|
||||
const [renderColorError, setRenderColorError] = React.useState<string | null>(null);
|
||||
// Like the accent's: only the latest pick's failure may put the colour back.
|
||||
const latestRenderColorSave = React.useRef(0);
|
||||
// Picks can overlap; only the latest one's failure may put the accent back.
|
||||
// Picks can overlap; only the latest one's failure may put the accent, or the render colour, back.
|
||||
const latestAccentSave = React.useRef(0);
|
||||
const latestRenderColorSave = React.useRef(0);
|
||||
// Accent and render colour saves go one at a time and in order, as a new accent clears the render colour on the
|
||||
// server: one sent first but answered later would otherwise undo a newer pick.
|
||||
const themeSaves = React.useRef<Promise<unknown>>(Promise.resolve());
|
||||
const queueThemeSave = (patch: Parameters<typeof settingsApi.updateTheme>[0]) => {
|
||||
const request = themeSaves.current.then(() => settingsApi.updateTheme(patch));
|
||||
themeSaves.current = request.catch(() => undefined);
|
||||
return request;
|
||||
};
|
||||
const [cardPreviewError, setCardPreviewError] = React.useState<string | null>(null);
|
||||
const hoverSlideshow = useHoverSlideshowEnabled();
|
||||
const [hoverSlideshow, setHoverSlideshow] = useHoverSlideshowEnabled();
|
||||
const [hoverSlideshowError, setHoverSlideshowError] = React.useState<string | null>(null);
|
||||
|
||||
const changeCardPreview = async (next: CardPreview) => {
|
||||
@@ -74,17 +81,19 @@ export default function AppearancePanel({ theme, onThemeChange, onUnauthorized }
|
||||
const previous = accent;
|
||||
const previousRender = getCachedRenderColor();
|
||||
const save = ++latestAccentSave.current;
|
||||
const renderSave = latestRenderColorSave.current;
|
||||
setAccentError(null);
|
||||
// The whole app recolours straight away, renders included, as the server puts the render colour back to
|
||||
// following the accent; put both back if the save fails.
|
||||
setCachedAccentColor(next);
|
||||
setCachedRenderColor(null);
|
||||
try {
|
||||
await settingsApi.updateTheme({ accent: next });
|
||||
await queueThemeSave({ accent: next });
|
||||
} catch (err) {
|
||||
if (save !== latestAccentSave.current) return;
|
||||
setCachedAccentColor(previous);
|
||||
setCachedRenderColor(previousRender);
|
||||
// A render colour picked since is newer than the one this save cleared.
|
||||
if (renderSave === latestRenderColorSave.current) setCachedRenderColor(previousRender);
|
||||
if (err instanceof UnauthorizedError) onUnauthorized?.();
|
||||
else setAccentError(t("appearance.accentFailed"));
|
||||
}
|
||||
@@ -96,7 +105,7 @@ export default function AppearancePanel({ theme, onThemeChange, onUnauthorized }
|
||||
setRenderColorError(null);
|
||||
setCachedRenderColor(next);
|
||||
try {
|
||||
await settingsApi.updateTheme({ renderColor: next });
|
||||
await queueThemeSave({ renderColor: next });
|
||||
} catch (err) {
|
||||
if (save !== latestRenderColorSave.current) return;
|
||||
setCachedRenderColor(previous);
|
||||
@@ -107,13 +116,9 @@ export default function AppearancePanel({ theme, onThemeChange, onUnauthorized }
|
||||
|
||||
const toggleHoverSlideshow = async (enabled: boolean) => {
|
||||
setHoverSlideshowError(null);
|
||||
// Shown straight away; put back if the save fails.
|
||||
setCachedHoverSlideshowEnabled(enabled);
|
||||
try {
|
||||
const res = await settingsApi.updateHoverSlideshow(enabled);
|
||||
setCachedHoverSlideshowEnabled(res.enabled);
|
||||
await setHoverSlideshow(enabled);
|
||||
} catch (err) {
|
||||
setCachedHoverSlideshowEnabled(!enabled);
|
||||
if (err instanceof UnauthorizedError) onUnauthorized?.();
|
||||
else setHoverSlideshowError(t("appearance.hoverSlideshow.failed"));
|
||||
}
|
||||
|
||||
@@ -1,41 +1,55 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { settingsApi } from "../api/settings";
|
||||
|
||||
// Shared by every model card so there's one request, and saves propagate to all of them.
|
||||
let cached: boolean | undefined; // undefined = not loaded yet
|
||||
// Shared by every model card so there's one request, and a switch shows everywhere at once.
|
||||
let cached: boolean | undefined;
|
||||
let inFlight: Promise<boolean> | null = null;
|
||||
// Bumped on sign-in, so an answer still on its way for the previous account is dropped.
|
||||
let account = 0;
|
||||
// Saves go one at a time, so the last pick is the one that sticks.
|
||||
let saving: Promise<unknown> = Promise.resolve();
|
||||
let latestSave = 0;
|
||||
const listeners = new Set<(value: boolean) => void>();
|
||||
|
||||
function load(): Promise<boolean> {
|
||||
if (cached !== undefined) return Promise.resolve(cached);
|
||||
if (!inFlight) {
|
||||
inFlight = settingsApi
|
||||
.getHoverSlideshow()
|
||||
.then((res) => {
|
||||
cached = res.enabled;
|
||||
return cached;
|
||||
})
|
||||
// A failed load shouldn't silently switch the feature off.
|
||||
.catch(() => true)
|
||||
.finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
}
|
||||
return inFlight;
|
||||
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.
|
||||
const request = settingsApi
|
||||
.getHoverSlideshow()
|
||||
.then((res) => (forAccount === account ? (cached ??= res.enabled) : res.enabled))
|
||||
// A failed load shouldn't silently switch the feature off.
|
||||
.catch(() => cached ?? true)
|
||||
.finally(() => {
|
||||
if (inFlight === request) inFlight = null;
|
||||
});
|
||||
inFlight = request;
|
||||
return request;
|
||||
}
|
||||
|
||||
export function setCachedHoverSlideshowEnabled(value: boolean) {
|
||||
function publish(value: boolean) {
|
||||
cached = value;
|
||||
listeners.forEach((listener) => listener(value));
|
||||
}
|
||||
|
||||
export function useHoverSlideshowEnabled(): boolean {
|
||||
/** Forgets the previous account's choice; call on sign-in. */
|
||||
export function resetHoverSlideshowEnabled() {
|
||||
account++;
|
||||
cached = undefined;
|
||||
inFlight = null;
|
||||
saving = Promise.resolve();
|
||||
}
|
||||
|
||||
/** On until the saved choice says otherwise. The setter shows the switch straight away, puts it back if the latest
|
||||
* save fails, and rejects with that save's error. */
|
||||
export function useHoverSlideshowEnabled(): [boolean, (enabled: boolean) => Promise<void>] {
|
||||
const [value, setValue] = useState<boolean>(cached ?? true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void load().then((v) => {
|
||||
if (!cancelled) setValue(v);
|
||||
if (!cancelled) setValue(cached ?? v);
|
||||
});
|
||||
listeners.add(setValue);
|
||||
return () => {
|
||||
@@ -44,5 +58,22 @@ export function useHoverSlideshowEnabled(): boolean {
|
||||
};
|
||||
}, []);
|
||||
|
||||
return value;
|
||||
const update = async (enabled: boolean) => {
|
||||
const previous = cached ?? true;
|
||||
const save = ++latestSave;
|
||||
const forAccount = account;
|
||||
publish(enabled);
|
||||
const request = saving.then(() => (forAccount === account ? settingsApi.updateHoverSlideshow(enabled) : undefined));
|
||||
saving = request.catch(() => undefined);
|
||||
try {
|
||||
await request;
|
||||
} catch (err) {
|
||||
// A newer pick, or another account, has taken over; this failure is no longer theirs to hear about.
|
||||
if (save !== latestSave || forAccount !== account) return;
|
||||
publish(previous);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
return [value, update];
|
||||
}
|
||||
@@ -1579,7 +1579,7 @@
|
||||
"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."
|
||||
"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 render color, or its pictures where the file can't be drawn."
|
||||
},
|
||||
"hoverSlideshow": {
|
||||
"heading": "Hover slideshow",
|
||||
|
||||
@@ -1579,7 +1579,7 @@
|
||||
"images": "Imágenes",
|
||||
"renders": "Renders 3D",
|
||||
"failed": "No se pudo guardar la opción de las tarjetas de modelos.",
|
||||
"help": "Lo que muestran primero las tarjetas de modelos. Imágenes: las imágenes que trae el modelo, o un render 3D si no tiene ninguna. Renders 3D: un render de cada modelo en tu color de acento, o sus imágenes si el archivo no se puede dibujar."
|
||||
"help": "Lo que muestran primero las tarjetas de modelos. Imágenes: las imágenes que trae el modelo, o un render 3D si no tiene ninguna. Renders 3D: un render de cada modelo en tu color del render, o sus imágenes si el archivo no se puede dibujar."
|
||||
},
|
||||
"hoverSlideshow": {
|
||||
"heading": "Presentación al pasar el cursor",
|
||||
|
||||
@@ -1592,7 +1592,7 @@
|
||||
"images": "Images",
|
||||
"renders": "Rendus 3D",
|
||||
"failed": "Impossible d’enregistrer le choix pour les cartes des modèles.",
|
||||
"help": "Ce que les cartes des modèles affichent en premier. Images : les images fournies avec le modèle, ou un rendu 3D s’il n’en a pas. Rendus 3D : un rendu de chaque modèle dans votre couleur d’accent, ou ses images si le fichier ne peut pas être dessiné."
|
||||
"help": "Ce que les cartes des modèles affichent en premier. Images : les images fournies avec le modèle, ou un rendu 3D s’il n’en a pas. Rendus 3D : un rendu de chaque modèle dans votre couleur de rendu, ou ses images si le fichier ne peut pas être dessiné."
|
||||
},
|
||||
"hoverSlideshow": {
|
||||
"heading": "Diaporama au survol",
|
||||
|
||||
@@ -1645,7 +1645,7 @@
|
||||
"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."
|
||||
"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ų atvaizdo spalva arba jo nuotraukos, jei failo nupiešti nepavyksta."
|
||||
},
|
||||
"hoverSlideshow": {
|
||||
"heading": "Skaidrės užvedus",
|
||||
|
||||
@@ -73,13 +73,15 @@ function ProviderRow({ provider, countLabel }: { provider: string | null; countL
|
||||
</>
|
||||
);
|
||||
return openAbout ? (
|
||||
<ListItemButton
|
||||
onClick={openAbout}
|
||||
aria-label={`${name}, ${countLabel}. ${t("providerAbout.aboutName", { name })}`}
|
||||
sx={{ borderRadius: 1, px: 1 }}
|
||||
>
|
||||
{content}
|
||||
</ListItemButton>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton
|
||||
onClick={openAbout}
|
||||
aria-label={`${name}, ${countLabel}. ${t("providerAbout.aboutName", { name })}`}
|
||||
sx={{ borderRadius: 1, px: 1 }}
|
||||
>
|
||||
{content}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem sx={{ px: 1 }}>{content}</ListItem>
|
||||
);
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function ModelCard({
|
||||
const authorAvatarUrl = author?.avatar_url || (showViewerAsAuthor ? viewerAvatarUrl : undefined);
|
||||
// Only mounted while hovered, so idle cards don't load images or tick.
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const slideshowEnabled = useHoverSlideshowEnabled();
|
||||
const [slideshowEnabled] = useHoverSlideshowEnabled();
|
||||
const contextMenu = useCardContextMenu();
|
||||
const toggleSelect = (event: React.MouseEvent | React.KeyboardEvent) => onToggleSelect?.(event.shiftKey);
|
||||
const slideshowImages =
|
||||
|
||||
Reference in new issue
Block a user