fix: adjust downloads page

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-09 19:27:32 +02:00
1 parent e78c7c20ac
commit 58c773b955
10 files changed
+135 -116

No files matched your search

@@ -3,7 +3,7 @@ import { prisma } from "../db";
// Slicers that register their own URL protocol for opening a remote model directly (e.g.
// bambustudio://) -- the only ones worth offering here, since the whole point of this preference
// is a future "open in {slicer}" launch via that protocol. Anything else falls under "other".
export const SLICER_IDS = ["bambustudio", "orcaslicer", "prusaslicer", "other"] as const;
export const SLICER_IDS = ["bambustudio", "orcaslicer", "prusaslicer", "crealityprintlink", "other"] as const;
export type SlicerId = (typeof SLICER_IDS)[number];
function isSlicerId(value: string): value is SlicerId {
+1 -1
View File
@@ -139,7 +139,7 @@ function AppShell({
path="/profile/password"
element={<ChangePasswordPage onUnauthorized={onUnauthorized} />}
/>
<Route path="/download" element={<DownloadPage />} />
<Route path="/downloads" element={<DownloadPage />} />
<Route
path="/admin-settings"
element={
+2 -2
View File
@@ -74,9 +74,9 @@ function useRouteChrome() {
} else if (path === "/profile") {
title = t("profile.title");
onBack = () => navigate(-1);
} else if (path === "/download") {
} else if (path === "/downloads") {
// Overridden by DownloadPage's usePageHeader once translations resolve.
title = t("sidebar.downloadBridge");
title = t("sidebar.downloads");
onBack = () => navigate("/");
} else if (path.startsWith("/admin-settings")) {
title = t("adminSettings.pageTitle");
@@ -58,7 +58,7 @@ type Props = {
isAdmin: boolean;
};
/** The persistent app-wide navigation rail: Dashboard, Models, Collections, Download Bridge, and
/** The persistent app-wide navigation rail: Dashboard, Models, Collections, Downloads, and
* (for admins) Administration. Folder browsing lives inside the Models page itself, not here. */
export default function Sidebar({ isAdmin }: Props) {
const { t } = useTranslation(["app", "common"]);
@@ -73,7 +73,7 @@ export default function Sidebar({ isAdmin }: Props) {
const onDashboard = location.pathname === "/";
const onCollections = location.pathname.startsWith("/models/collections");
const onModels = (location.pathname.startsWith("/models") && !onCollections) || location.pathname.startsWith("/authors");
const onDownload = location.pathname.startsWith("/download");
const onDownload = location.pathname.startsWith("/downloads");
const onAdminSettings = location.pathname.startsWith("/admin-settings");
const onAdminUsers = location.pathname.startsWith("/admin-users");
const onAdminLogs = location.pathname.startsWith("/admin-logs");
@@ -189,20 +189,20 @@ export default function Sidebar({ isAdmin }: Props) {
{collapsed ? (
<CollapsedNavIcon
icon={<DownloadIcon fontSize="small" />}
label={t("sidebar.downloadBridge")}
label={t("sidebar.downloads")}
selected={onDownload}
onClick={() => navigate("/download")}
onClick={() => navigate("/downloads")}
/>
) : (
<ListItemButton
selected={onDownload}
onClick={() => navigate("/download")}
onClick={() => navigate("/downloads")}
sx={{ borderRadius: 1, mb: 0.5 }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<DownloadIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("sidebar.downloadBridge")} primaryTypographyProps={{ variant: "body2" }} />
<ListItemText primary={t("sidebar.downloads")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
)}
-2
View File
@@ -5,8 +5,6 @@
// release, so they don't need updating when a new Bridge build ships.
const BRIDGE_RELEASES_BASE = "https://github.com/TautvydasDerzinskas/print-stash/releases/latest/download";
export const BRIDGE_RELEASES_PAGE = "https://github.com/TautvydasDerzinskas/print-stash/releases";
export type BridgeDownload = { os: "windows" | "macos" | "linux"; label: string; asset: string };
export const BRIDGE_DOWNLOADS: BridgeDownload[] = [
+4 -1
View File
@@ -2,11 +2,14 @@ export type SlicerOption = { id: string; label: string };
// Limited to slicers that register their own URL protocol for opening a remote model directly
// (e.g. bambustudio://) -- the only ones a future "open in {slicer}" launch could actually use.
// IDs must match backend's services/slicerPreferenceService.ts SLICER_IDS.
// IDs must match backend's services/slicerPreferenceService.ts SLICER_IDS, and (id !== "other")
// must equal the exact scheme the slicer registers -- e.g. Creality Print's is "crealityprintlink"
// (crealityprintlink://open?file=...), not "creality", per its libslic3r/Utils.hpp.
export const SLICER_OPTIONS: SlicerOption[] = [
{ id: "bambustudio", label: "Bambu Studio" },
{ id: "orcaslicer", label: "OrcaSlicer" },
{ id: "prusaslicer", label: "PrusaSlicer" },
{ id: "crealityprintlink", label: "Creality Print" },
{ id: "other", label: "Other / Manual" },
];
+19 -17
View File
@@ -91,7 +91,7 @@
"foldersHeadingWithCount": "Folders ({{count}})",
"models": "Models",
"collections": "Collections",
"downloadBridge": "Download Bridge"
"downloads": "Downloads"
},
"uploadBar": {
"uploading": "Uploading…",
@@ -419,23 +419,25 @@
"download": {
"pageTitle": "PrintStash Bridge",
"intro": "A small helper app that runs on your computer so \"Open in Slicer\" can hand a model straight to your slicer.",
"why": "Bambu Studio's own bambustudio:// link only accepts files from a short allowlist of domains Bambu Lab controls, and a self-hosted PrintStash never qualifies -- so \"Open in Bambu Studio\" would otherwise do nothing. The Bridge listens on its own print-stash:// link instead, downloads the file itself, and hands it to Bambu Studio locally. OrcaSlicer and PrusaSlicer already accept links directly and don't need it.",
"downloadHeading": "Download",
"install": {
"windows": "Run the downloaded .exe once (double-click is fine) -- it installs itself and registers the protocol.",
"macos": "chmod +x it, remove the Gatekeeper quarantine flag (xattr -d com.apple.quarantine), then run it once with --install.",
"linux": "Mark it executable (chmod +x) and run it once from a terminal."
},
"footerLinks": "Need the exact commands, or building from source? See the <releases>Bridge releases</releases> page.",
"autostart": {
"heading": "Run at startup (optional)",
"windowsLinuxNote": "Not needed on Windows or Linux -- the OS launches the Bridge fresh, with the link's URL already attached, every time you click \"Open in Bambu Studio\". There's no background process to keep running.",
"macosIntro": "Not required on macOS either, for the same reason -- it launches on demand even after a reboot. Adding it as a login item just means the very first click after starting up responds instantly instead of waiting about a second for the app to launch.",
"macosGuiHeading": "Easiest: System Settings",
"macosGuiSteps": "System Settings -> General -> Login Items & Extensions -> click + and add \"PrintStash Bridge\" from ~/Applications.",
"macosLaunchAgentHeading": "Or, a LaunchAgent (for Terminal users)",
"macosLaunchAgentIntro": "Save this as ~/Library/LaunchAgents/com.printstash.bridge.plist (replace YOUR_USERNAME with your actual username):",
"macosLaunchAgentLoad": "Then load it:"
"modal": {
"startedNote": "Your download should start automatically. Follow these steps to finish installing:",
"windows": {
"heading": "Install on Windows",
"step1": "Double-click the downloaded .exe (or run it with no arguments from a terminal). It installs itself and registers the protocol automatically."
},
"linux": {
"heading": "Install on Linux",
"step1": "If your file manager blocks execution, mark the file as executable first.",
"step2": "Then run it once -- double-click, or from a terminal."
},
"macos": {
"heading": "Install on macOS",
"step1": "Make it executable.",
"step2": "Remove the Gatekeeper quarantine flag -- downloaded files are quarantined automatically.",
"step3": "Install it. This copies the app into ~/Applications and registers the print-stash:// link.",
"note": "If macOS still refuses to run it, open System Settings -> Privacy & Security and allow it there instead."
}
}
}
},
+19 -17
View File
@@ -91,7 +91,7 @@
"foldersHeadingWithCount": "Aplankai ({{count}})",
"models": "Modeliai",
"collections": "Kolekcijos",
"downloadBridge": "Atsisiųsti Bridge"
"downloads": "Atsisiuntimai"
},
"uploadBar": {
"uploading": "Įkeliama…",
@@ -419,23 +419,25 @@
"download": {
"pageTitle": "PrintStash Bridge",
"intro": "Nedidelė pagalbinė programa, veikianti jūsų kompiuteryje, kad „Atverti pjaustyklėje“ galėtų perduoti modelį tiesiai jūsų pjaustyklei.",
"why": "Bambu Studio nuosava bambustudio:// nuoroda priima failus tik iš trumpo, Bambu Lab kontroliuojamo domenų sąrašo, o savarankiškai talpinamas PrintStash niekada jame nepasirodys -- todėl „Atverti Bambu Studio“ paprasčiausiai nieko nedarytų. Vietoje to Bridge klausosi savo print-stash:// nuorodos, pats atsisiunčia failą ir perduoda jį Bambu Studio vietoje. OrcaSlicer ir PrusaSlicer nuorodas priima tiesiogiai, todėl jiems Bridge nereikalingas.",
"downloadHeading": "Atsisiųsti",
"install": {
"windows": "Vieną kartą paleiskite atsisiųstą .exe failą (užtenka dvigubo paspaudimo) -- jis pats įsidiegs ir užregistruos protokolą.",
"macos": "Suteikite leidimą vykdyti (chmod +x), pašalinkite Gatekeeper karantino žymą (xattr -d com.apple.quarantine), tada vieną kartą paleiskite su --install.",
"linux": "Pažymėkite kaip vykdomą (chmod +x) ir vieną kartą paleiskite iš terminalo."
},
"footerLinks": "Reikia tikslių komandų arba norite susikompiliuoti iš išeitinio kodo? Žr. <releases>Bridge leidimų</releases> puslapį.",
"autostart": {
"heading": "Automatinis paleidimas paleidžiant sistemą (nebūtina)",
"windowsLinuxNote": "„Windows“ ar „Linux“ tai nereikalinga -- kiekvieną kartą paspaudus „Atverti Bambu Studio“, operacinė sistema paleidžia Bridge iš naujo, jau su nuorodos URL. Nėra jokio fone veikiančio proceso, kurį reikėtų palaikyti.",
"macosIntro": "Tas pats galioja ir „macOS“ -- ji paleidžia programą pagal poreikį net ir po perkrovimo. Pridėjus ją kaip prisijungimo elementą, tiesiog pirmas paspaudimas po sistemos paleidimo įvyks akimirksniu, o ne po maždaug sekundės, kol programa pasileis.",
"macosGuiHeading": "Paprasčiausia: sistemos nustatymai",
"macosGuiSteps": "System Settings -> General -> Login Items & Extensions -> spauskite + ir pridėkite „PrintStash Bridge“ iš ~/Applications.",
"macosLaunchAgentHeading": "Arba LaunchAgent (naudojantiems terminalą)",
"macosLaunchAgentIntro": "Išsaugokite tai kaip ~/Library/LaunchAgents/com.printstash.bridge.plist (pakeiskite YOUR_USERNAME savo naudotojo vardu):",
"macosLaunchAgentLoad": "Tada įkelkite jį:"
"modal": {
"startedNote": "Atsisiuntimas turėtų prasidėti automatiškai. Norėdami baigti diegimą, atlikite šiuos veiksmus:",
"windows": {
"heading": "Diegimas „Windows“",
"step1": "Dukart spustelėkite atsisiųstą .exe failą (arba paleiskite jį be argumentų iš terminalo). Jis pats įsidiegs ir automatiškai užregistruos protokolą."
},
"linux": {
"heading": "Diegimas „Linux“",
"step1": "Jei failų tvarkyklė neleidžia vykdyti failo, pirmiausia pažymėkite jį kaip vykdomą.",
"step2": "Tada vieną kartą jį paleiskite -- dukart spustelėję arba iš terminalo."
},
"macos": {
"heading": "Diegimas „macOS“",
"step1": "Suteikite leidimą vykdyti.",
"step2": "Pašalinkite Gatekeeper karantino žymą -- atsisiųsti failai automatiškai pažymimi karantinu.",
"step3": "Įdiekite jį. Tai nukopijuos programą į ~/Applications ir užregistruos print-stash:// nuorodą.",
"note": "Jei „macOS“ vis tiek atsisako jį paleisti, atidarykite System Settings -> Privacy & Security ir leiskite jį ten."
}
}
}
},
+82 -68
View File
@@ -1,41 +1,25 @@
import { useTranslation, Trans } from "react-i18next";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Alert from "@mui/material/Alert";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import Dialog from "@mui/material/Dialog";
import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import DownloadIcon from "@mui/icons-material/Download";
import LaptopWindowsIcon from "@mui/icons-material/LaptopWindows";
import AppleIcon from "@mui/icons-material/Apple";
import TerminalIcon from "@mui/icons-material/Terminal";
import CableIcon from "@mui/icons-material/Cable";
import { usePageHeader } from "../../components/Layout/PageHeaderContext";
import { BRIDGE_DOWNLOADS, BRIDGE_RELEASES_PAGE, bridgeDownloadUrl } from "../../constants/bridge";
import { BRIDGE_DOWNLOADS, bridgeDownloadUrl, type BridgeDownload } from "../../constants/bridge";
const OS_ICON = { windows: LaptopWindowsIcon, macos: AppleIcon, linux: TerminalIcon };
// launchd doesn't expand `~`, so the plist needs an absolute path -- the user has to swap in
// their own username either way, since we can't know it here.
const MACOS_LAUNCH_AGENT_PLIST = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.printstash.bridge</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOUR_USERNAME/Applications/PrintStash Bridge.app/Contents/MacOS/print-stash-bridge</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>`;
const MACOS_LAUNCH_AGENT_LOAD_CMD = `mkdir -p ~/Library/LaunchAgents
# save the file above as ~/Library/LaunchAgents/com.printstash.bridge.plist
launchctl load ~/Library/LaunchAgents/com.printstash.bridge.plist`;
type InstallStep = { text: string; code?: string };
function CodeBlock({ children }: { children: string }) {
return (
@@ -56,9 +40,60 @@ function CodeBlock({ children }: { children: string }) {
);
}
function InstallSteps({ steps }: { steps: InstallStep[] }) {
return (
<Stack spacing={2}>
{steps.map((step, i) => (
<Stack key={i} direction="row" spacing={1.5}>
<Box
sx={{
width: 22,
height: 22,
borderRadius: "50%",
bgcolor: "primary.main",
color: "primary.contrastText",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 12,
fontWeight: 600,
flexShrink: 0,
mt: "1px",
}}
>
{i + 1}
</Box>
<Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="body2">{step.text}</Typography>
{step.code && <Box sx={{ mt: 1 }}><CodeBlock>{step.code}</CodeBlock></Box>}
</Box>
</Stack>
))}
</Stack>
);
}
export default function DownloadPage() {
const { t } = useTranslation(["app", "common"]);
usePageHeader({ title: t("sidebar.downloadBridge") });
usePageHeader({ title: t("sidebar.downloads") });
const [installOs, setInstallOs] = useState<BridgeDownload["os"] | null>(null);
const installSteps: Record<BridgeDownload["os"], InstallStep[]> = {
windows: [{ text: t("download.modal.windows.step1") }],
linux: [
{ text: t("download.modal.linux.step1"), code: "chmod +x print-stash-bridge-linux-amd64" },
{ text: t("download.modal.linux.step2"), code: "./print-stash-bridge-linux-amd64" },
],
macos: [
{ text: t("download.modal.macos.step1"), code: "chmod +x ~/Downloads/print-stash-bridge-macos" },
{
text: t("download.modal.macos.step2"),
code: "xattr -d com.apple.quarantine ~/Downloads/print-stash-bridge-macos",
},
{ text: t("download.modal.macos.step3"), code: "~/Downloads/print-stash-bridge-macos --install" },
],
};
return (
<Stack spacing={3} sx={{ maxWidth: 720 }}>
@@ -70,10 +105,6 @@ export default function DownloadPage() {
</Box>
</Stack>
<Alert severity="info" variant="outlined">
<Typography variant="body2">{t("download.why")}</Typography>
</Alert>
<Stack spacing={2}>
<Typography variant="subtitle1" fontWeight={600}>{t("download.downloadHeading")}</Typography>
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
@@ -84,15 +115,13 @@ export default function DownloadPage() {
<Stack spacing={1.5} alignItems="flex-start">
<Icon fontSize="large" />
<Typography variant="subtitle2" fontWeight={600}>{label}</Typography>
<Typography variant="caption" color="text.secondary">
{t(`download.install.${os}`)}
</Typography>
<Button
component="a"
href={bridgeDownloadUrl(asset)}
variant="outlined"
size="small"
startIcon={<DownloadIcon fontSize="small" />}
onClick={() => setInstallOs(os)}
>
{t("common:download")}
</Button>
@@ -103,42 +132,27 @@ export default function DownloadPage() {
</Stack>
</Stack>
<Stack spacing={1.5}>
<Typography variant="subtitle1" fontWeight={600}>{t("download.autostart.heading")}</Typography>
<Typography variant="body2" color="text.secondary">{t("download.autostart.windowsLinuxNote")}</Typography>
<Typography variant="body2" color="text.secondary">{t("download.autostart.macosIntro")}</Typography>
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="body2" fontWeight={600} sx={{ mb: 0.5 }}>
{t("download.autostart.macosGuiHeading")}
</Typography>
<Typography variant="body2" color="text.secondary">{t("download.autostart.macosGuiSteps")}</Typography>
</Paper>
<Paper variant="outlined" sx={{ p: 2 }}>
<Typography variant="body2" fontWeight={600} sx={{ mb: 0.5 }}>
{t("download.autostart.macosLaunchAgentHeading")}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{t("download.autostart.macosLaunchAgentIntro")}
</Typography>
<CodeBlock>{MACOS_LAUNCH_AGENT_PLIST}</CodeBlock>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1.5, mb: 1 }}>
{t("download.autostart.macosLaunchAgentLoad")}
</Typography>
<CodeBlock>{MACOS_LAUNCH_AGENT_LOAD_CMD}</CodeBlock>
</Paper>
</Stack>
<Typography variant="caption" color="text.secondary">
<Trans
t={t}
i18nKey="download.footerLinks"
components={{
releases: <Link href={BRIDGE_RELEASES_PAGE} target="_blank" rel="noopener noreferrer" />,
}}
/>
</Typography>
<Dialog open={installOs !== null} onClose={() => setInstallOs(null)} maxWidth="xs" fullWidth>
{installOs && (
<>
<DialogTitle>{t(`download.modal.${installOs}.heading`)}</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2.5 }}>
{t("download.modal.startedNote")}
</Typography>
<InstallSteps steps={installSteps[installOs]} />
{installOs === "macos" && (
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 2.5 }}>
{t("download.modal.macos.note")}
</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setInstallOs(null)}>{t("common:close")}</Button>
</DialogActions>
</>
)}
</Dialog>
</Stack>
);
}
@@ -91,7 +91,7 @@ export default function SlicerPicker({ onUnauthorized }: Props) {
{value === "bambustudio" && (
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mt: 1 }}>
{t("profile.slicer.bridgeRequiredPrefix")}{" "}
<Link component={RouterLink} to="/download">{t("profile.slicer.bridgeRequiredLink")}</Link>
<Link component={RouterLink} to="/downloads">{t("profile.slicer.bridgeRequiredLink")}</Link>
</Typography>
)}
{status && <Typography variant="caption" color="error" sx={{ display: "block", mt: 1 }}>{status}</Typography>}