mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf77d67c00 | ||
|
|
930dc5c018 | ||
|
|
c2cc7ede10 | ||
|
|
a95db1aa8b | ||
|
|
45da220060 |
@@ -27,12 +27,14 @@ import DownloadIcon from "@mui/icons-material/Download";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||
|
||||
interface UpdateModalProps {
|
||||
export interface UpdateModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
currentVersion: string;
|
||||
updateSummary: UpdateSummary;
|
||||
machineInfo: MachineInfo;
|
||||
isDesktop?: boolean;
|
||||
desktopVersion?: string | null;
|
||||
}
|
||||
|
||||
const UpdateModal: React.FC<UpdateModalProps> = ({
|
||||
@@ -41,6 +43,8 @@ const UpdateModal: React.FC<UpdateModalProps> = ({
|
||||
currentVersion,
|
||||
updateSummary,
|
||||
machineInfo,
|
||||
isDesktop = false,
|
||||
desktopVersion,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [fullUpdateInfo, setFullUpdateInfo] = useState<FullUpdateInfo | null>(
|
||||
@@ -96,7 +100,19 @@ const UpdateModal: React.FC<UpdateModalProps> = ({
|
||||
return t(`update.priority.${key}`, priority || "Normal");
|
||||
};
|
||||
|
||||
const downloadUrl = updateService.getDownloadUrl(machineInfo);
|
||||
const desktopNeedsUpdate =
|
||||
isDesktop && desktopVersion
|
||||
? updateService.compareVersions(
|
||||
updateSummary.latest_version || "0",
|
||||
desktopVersion,
|
||||
) > 0
|
||||
: false;
|
||||
|
||||
const downloadUrl = isDesktop
|
||||
? desktopNeedsUpdate
|
||||
? updateService.getDownloadUrl(machineInfo, true)
|
||||
: null
|
||||
: updateService.getDownloadUrl(machineInfo);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -504,7 +520,7 @@ const UpdateModal: React.FC<UpdateModalProps> = ({
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
color="green"
|
||||
variant="filled"
|
||||
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
|
||||
>
|
||||
{t("update.downloadLatest", "Download Latest")}
|
||||
|
||||
+112
-21
@@ -39,12 +39,16 @@ interface GeneralSectionProps {
|
||||
hideTitle?: boolean;
|
||||
hideUpdateSection?: boolean;
|
||||
hideAdminBanner?: boolean;
|
||||
desktopVersion?: string | null;
|
||||
isDesktop?: boolean;
|
||||
}
|
||||
|
||||
const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
hideTitle = false,
|
||||
hideUpdateSection = false,
|
||||
hideAdminBanner = false,
|
||||
desktopVersion,
|
||||
isDesktop = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { preferences, updatePreference } = usePreferences();
|
||||
@@ -272,11 +276,25 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
)}
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"settings.general.updates.currentBackendVersion",
|
||||
"Current Backend Version",
|
||||
)}
|
||||
{desktopVersion && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{t(
|
||||
"settings.general.versionInfo.desktop",
|
||||
"Desktop Version",
|
||||
)}
|
||||
:{" "}
|
||||
<Text component="span" fw={500}>
|
||||
{desktopVersion}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
<Text size="sm" c="dimmed" mt={desktopVersion ? 4 : 0}>
|
||||
{desktopVersion
|
||||
? t("settings.general.versionInfo.server", "Server Version")
|
||||
: t(
|
||||
"settings.general.updates.currentBackendVersion",
|
||||
"Current Backend Version",
|
||||
)}
|
||||
:{" "}
|
||||
<Text component="span" fw={500}>
|
||||
{config.appVersion}
|
||||
@@ -294,6 +312,33 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
{isDesktop &&
|
||||
desktopVersion &&
|
||||
updateSummary &&
|
||||
(() => {
|
||||
const desktopNeedsUpdate =
|
||||
updateService.compareVersions(
|
||||
updateSummary.latest_version || "0",
|
||||
desktopVersion,
|
||||
) > 0;
|
||||
const serverNeedsUpdate =
|
||||
updateService.compareVersions(
|
||||
updateSummary.latest_version || "0",
|
||||
config.appVersion || "0",
|
||||
) > 0;
|
||||
|
||||
if (!desktopNeedsUpdate && serverNeedsUpdate) {
|
||||
return (
|
||||
<Text size="sm" c="orange" mt={4} fw={500}>
|
||||
{t(
|
||||
"settings.general.updates.serverNeedsUpdate",
|
||||
"Server needs to be updated by administrator",
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
@@ -315,22 +360,68 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
)}
|
||||
</Button>
|
||||
{updateSummary && (
|
||||
<Button
|
||||
size="sm"
|
||||
color={
|
||||
updateSummary.max_priority === "urgent" ? "red" : "blue"
|
||||
}
|
||||
onClick={() => setUpdateModalOpened(true)}
|
||||
leftSection={
|
||||
<LocalIcon
|
||||
icon="system-update-alt-rounded"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t("settings.general.updates.viewDetails", "View Details")}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
color={
|
||||
updateSummary.max_priority === "urgent" ? "red" : "blue"
|
||||
}
|
||||
onClick={() => setUpdateModalOpened(true)}
|
||||
leftSection={
|
||||
<LocalIcon
|
||||
icon="system-update-alt-rounded"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t(
|
||||
"settings.general.updates.viewDetails",
|
||||
"View Details",
|
||||
)}
|
||||
</Button>
|
||||
{isDesktop &&
|
||||
desktopVersion &&
|
||||
(() => {
|
||||
const desktopNeedsUpdate =
|
||||
updateService.compareVersions(
|
||||
updateSummary.latest_version || "0",
|
||||
desktopVersion,
|
||||
) > 0;
|
||||
|
||||
if (!desktopNeedsUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadUrl = updateService.getDownloadUrl(
|
||||
{
|
||||
machineType: config.machineType || "Unknown",
|
||||
activeSecurity: config.activeSecurity ?? false,
|
||||
licenseType: config.license ?? "NORMAL",
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
return downloadUrl ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="filled"
|
||||
component="a"
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
leftSection={
|
||||
<LocalIcon
|
||||
icon="download-rounded"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t("update.downloadLatest", "Download Latest")}
|
||||
</Button>
|
||||
) : null;
|
||||
})()}
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// Centralized download URLs for Stirling PDF desktop installers
|
||||
export const DOWNLOAD_URLS = {
|
||||
WINDOWS: "https://files.stirlingpdf.com/win-installer.exe",
|
||||
MAC: "https://files.stirlingpdf.com/mac-installer.dmg",
|
||||
LINUX_DOCS: "https://docs.stirlingpdf.com/Installation/Unix%20Installation/",
|
||||
// Centralized download URLs for Stirling PDF desktop installers.
|
||||
// File names match the GitHub release artifacts so files.stirlingpdf.com can
|
||||
// alias them 1:1.
|
||||
export const DOWNLOAD_BASE_URL = "https://files.stirlingpdf.com/";
|
||||
|
||||
export const DESKTOP_INSTALLER_FILES = {
|
||||
// Universal Mac binary - works on both Intel and Apple silicon, so no arch
|
||||
// detection is needed on the client.
|
||||
MAC: "Stirling-PDF-macos-universal.dmg",
|
||||
WINDOWS: "Stirling-PDF-windows-x86_64.msi",
|
||||
LINUX_DEB: "Stirling-PDF-linux-x86_64.deb",
|
||||
} as const;
|
||||
|
||||
export const DOWNLOAD_BASE_URL = "https://files.stirlingpdf.com/";
|
||||
export const DOWNLOAD_URLS = {
|
||||
WINDOWS: DOWNLOAD_BASE_URL + DESKTOP_INSTALLER_FILES.WINDOWS,
|
||||
MAC: DOWNLOAD_BASE_URL + DESKTOP_INSTALLER_FILES.MAC,
|
||||
LINUX_DOCS: "https://docs.stirlingpdf.com/Installation/Unix%20Installation/",
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { updateService } from "@app/services/updateService";
|
||||
import { DOWNLOAD_URLS } from "@app/constants/downloads";
|
||||
|
||||
type MachineInfo = Parameters<typeof updateService.getDownloadUrl>[0];
|
||||
|
||||
const stubMachineInfo: MachineInfo = {
|
||||
machineType: "Server-jar",
|
||||
activeSecurity: false,
|
||||
licenseType: "FREE",
|
||||
};
|
||||
|
||||
const setUserAgent = (value: string) => {
|
||||
Object.defineProperty(window.navigator, "userAgent", {
|
||||
value,
|
||||
configurable: true,
|
||||
});
|
||||
};
|
||||
|
||||
describe("updateService.getDownloadUrl (desktop)", () => {
|
||||
const original = window.navigator.userAgent;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setUserAgent(original);
|
||||
});
|
||||
|
||||
it("returns the universal Mac installer for Apple-silicon Macs", () => {
|
||||
setUserAgent(
|
||||
"Mozilla/5.0 (Macintosh; ARM Mac OS X 14_0) AppleWebKit/605.1.15",
|
||||
);
|
||||
expect(updateService.getDownloadUrl(stubMachineInfo, true)).toBe(
|
||||
DOWNLOAD_URLS.MAC,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the universal Mac installer for Intel Macs", () => {
|
||||
setUserAgent(
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15",
|
||||
);
|
||||
expect(updateService.getDownloadUrl(stubMachineInfo, true)).toBe(
|
||||
DOWNLOAD_URLS.MAC,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the universal Mac URL even when the UA misreports Apple-silicon as Intel", () => {
|
||||
setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)");
|
||||
const url = updateService.getDownloadUrl(stubMachineInfo, true);
|
||||
expect(url).toBe(DOWNLOAD_URLS.MAC);
|
||||
expect(url).toContain("Stirling-PDF-macos-universal.dmg");
|
||||
});
|
||||
|
||||
it("returns the Windows installer for Windows UAs", () => {
|
||||
setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
|
||||
expect(updateService.getDownloadUrl(stubMachineInfo, true)).toBe(
|
||||
DOWNLOAD_URLS.WINDOWS,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns a .deb URL for Linux UAs", () => {
|
||||
setUserAgent("Mozilla/5.0 (X11; Linux x86_64)");
|
||||
expect(updateService.getDownloadUrl(stubMachineInfo, true)).toContain(
|
||||
"Stirling-PDF-linux-x86_64.deb",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the GitHub releases page for unknown platforms", () => {
|
||||
setUserAgent("Mozilla/5.0 (Unknown OS)");
|
||||
expect(updateService.getDownloadUrl(stubMachineInfo, true)).toBe(
|
||||
"https://github.com/Stirling-Tools/Stirling-PDF/releases/latest",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,8 @@
|
||||
import { DOWNLOAD_BASE_URL } from "@app/constants/downloads";
|
||||
import {
|
||||
DESKTOP_INSTALLER_FILES,
|
||||
DOWNLOAD_BASE_URL,
|
||||
DOWNLOAD_URLS,
|
||||
} from "@app/constants/downloads";
|
||||
|
||||
export interface UpdateSummary {
|
||||
latest_version: string | null;
|
||||
@@ -67,8 +71,27 @@ export class UpdateService {
|
||||
|
||||
/**
|
||||
* Get download URL based on machine type and security settings
|
||||
* If isDesktop is true, returns desktop installer URLs instead
|
||||
*/
|
||||
getDownloadUrl(machineInfo: MachineInfo): string | null {
|
||||
getDownloadUrl(
|
||||
machineInfo: MachineInfo,
|
||||
isDesktop: boolean = false,
|
||||
): string | null {
|
||||
if (isDesktop) {
|
||||
const userAgent = navigator.userAgent.toLowerCase();
|
||||
if (userAgent.includes("win")) {
|
||||
return DOWNLOAD_URLS.WINDOWS;
|
||||
} else if (userAgent.includes("mac")) {
|
||||
// Universal Mac binary covers both Intel and Apple silicon, so we
|
||||
// don't need to detect arch (which is unreliable on the web anyway -
|
||||
// Apple-silicon Macs often report Intel in the UA).
|
||||
return DOWNLOAD_URLS.MAC;
|
||||
} else if (userAgent.includes("linux")) {
|
||||
return DOWNLOAD_BASE_URL + DESKTOP_INSTALLER_FILES.LINUX_DEB;
|
||||
}
|
||||
return "https://github.com/Stirling-Tools/Stirling-PDF/releases/latest";
|
||||
}
|
||||
|
||||
// Only show download for non-Docker installations
|
||||
if (
|
||||
machineInfo.machineType === "Docker" ||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useMemo, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, Tooltip, useMantineTheme, rem } from "@mantine/core";
|
||||
import { Box, Tooltip, useMantineTheme, rem, Stack, Text } from "@mantine/core";
|
||||
import { useBackendHealth } from "@app/hooks/useBackendHealth";
|
||||
import { useVersionInfo } from "@app/hooks/useVersionInfo";
|
||||
|
||||
interface BackendHealthIndicatorProps {
|
||||
className?: string;
|
||||
@@ -13,8 +14,9 @@ export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const theme = useMantineTheme();
|
||||
const { status, isOnline, checkHealth } = useBackendHealth();
|
||||
const { desktopVersion, serverVersion } = useVersionInfo();
|
||||
|
||||
const label = useMemo(() => {
|
||||
const statusText = useMemo(() => {
|
||||
if (status === "starting") {
|
||||
return t("backendHealth.checking", "Checking backend status...");
|
||||
}
|
||||
@@ -26,6 +28,38 @@ export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
||||
return t("backendHealth.offline", "Backend Offline");
|
||||
}, [status, isOnline, t]);
|
||||
|
||||
const versionLines = useMemo(() => {
|
||||
const lines: string[] = [];
|
||||
if (desktopVersion) {
|
||||
lines.push(`Desktop: ${desktopVersion}`);
|
||||
}
|
||||
if (serverVersion) {
|
||||
lines.push(`Server: ${serverVersion}`);
|
||||
}
|
||||
return lines;
|
||||
}, [desktopVersion, serverVersion]);
|
||||
|
||||
const ariaLabel = useMemo(() => {
|
||||
if (versionLines.length === 0) {
|
||||
return statusText;
|
||||
}
|
||||
return `${statusText} (${versionLines.join(", ")})`;
|
||||
}, [statusText, versionLines]);
|
||||
|
||||
const label =
|
||||
versionLines.length > 0 ? (
|
||||
<Stack gap={4}>
|
||||
<Text size="sm">{statusText}</Text>
|
||||
{versionLines.map((line, idx) => (
|
||||
<Text key={idx} size="xs" c="dimmed">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
statusText
|
||||
);
|
||||
|
||||
const dotColor = useMemo(() => {
|
||||
if (status === "starting") {
|
||||
return theme.colors.yellow?.[5] ?? "#fcc419";
|
||||
@@ -59,7 +93,7 @@ export const BackendHealthIndicator: React.FC<BackendHealthIndicatorProps> = ({
|
||||
className={className ? `${className}` : undefined}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={label}
|
||||
aria-label={ariaLabel}
|
||||
tabIndex={0}
|
||||
onClick={checkHealth}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import CoreUpdateModal from "@core/components/shared/UpdateModal";
|
||||
import type { UpdateModalProps } from "@core/components/shared/UpdateModal";
|
||||
import { useVersionInfo } from "@app/hooks/useVersionInfo";
|
||||
|
||||
const UpdateModal: React.FC<UpdateModalProps> = (props) => {
|
||||
const { desktopVersion } = useVersionInfo();
|
||||
return (
|
||||
<CoreUpdateModal {...props} isDesktop desktopVersion={desktopVersion} />
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateModal;
|
||||
+4
-4
@@ -2,15 +2,15 @@ import React from "react";
|
||||
import { Stack } from "@mantine/core";
|
||||
import CoreGeneralSection from "@core/components/shared/config/configSections/GeneralSection";
|
||||
import { DefaultAppSettings } from "@app/components/shared/config/configSections/DefaultAppSettings";
|
||||
import { useVersionInfo } from "@app/hooks/useVersionInfo";
|
||||
|
||||
/**
|
||||
* Desktop extension of GeneralSection that adds default PDF editor settings
|
||||
*/
|
||||
const GeneralSection: React.FC = () => {
|
||||
const { desktopVersion } = useVersionInfo();
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<DefaultAppSettings />
|
||||
<CoreGeneralSection />
|
||||
<CoreGeneralSection hideTitle desktopVersion={desktopVersion} isDesktop />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { getVersion } from "@tauri-apps/api/app";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
|
||||
export function useVersionInfo() {
|
||||
const { config } = useAppConfig();
|
||||
const [desktopVersion, setDesktopVersion] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const fetchDesktopVersion = async () => {
|
||||
try {
|
||||
const version = await getVersion();
|
||||
if (!cancelled) {
|
||||
setDesktopVersion(version);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[useVersionInfo] Failed to fetch desktop version:",
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void fetchDesktopVersion();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
desktopVersion,
|
||||
serverVersion: config?.appVersion ?? null,
|
||||
loading: loading || !config,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user