feat: enhance update dialog with changelog notes
This commit is contained in:
Generated
+1
-1
@@ -2847,7 +2847,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openchamber-desktop"
|
name = "openchamber-desktop"
|
||||||
version = "1.0.7"
|
version = "1.0.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
@@ -30,21 +30,26 @@ let cachedUpdate: Update | null = null;
|
|||||||
export async function checkForUpdates(): Promise<UpdateInfo> {
|
export async function checkForUpdates(): Promise<UpdateInfo> {
|
||||||
try {
|
try {
|
||||||
const { check } = await import('@tauri-apps/plugin-updater');
|
const { check } = await import('@tauri-apps/plugin-updater');
|
||||||
const update = await check();
|
const [update, currentVersion] = await Promise.all([
|
||||||
|
check(),
|
||||||
|
getCurrentVersion(),
|
||||||
|
]);
|
||||||
cachedUpdate = update;
|
cachedUpdate = update;
|
||||||
|
|
||||||
if (!update) {
|
if (!update) {
|
||||||
return {
|
return {
|
||||||
available: false,
|
available: false,
|
||||||
currentVersion: await getCurrentVersion(),
|
currentVersion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changelogNotes = await fetchChangelogNotes(currentVersion, update.version);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
version: update.version,
|
version: update.version,
|
||||||
currentVersion: await getCurrentVersion(),
|
currentVersion,
|
||||||
body: update.body ?? undefined,
|
body: changelogNotes ?? update.body ?? undefined,
|
||||||
date: update.date ?? undefined,
|
date: update.date ?? undefined,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -103,3 +108,38 @@ async function getCurrentVersion(): Promise<string> {
|
|||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchChangelogNotes(fromVersion: string, toVersion: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md'
|
||||||
|
);
|
||||||
|
if (!response.ok) return undefined;
|
||||||
|
|
||||||
|
const changelog = await response.text();
|
||||||
|
const sections = changelog.split(/^## /m).slice(1);
|
||||||
|
|
||||||
|
const fromNum = parseVersion(fromVersion);
|
||||||
|
const toNum = parseVersion(toVersion);
|
||||||
|
|
||||||
|
const relevantSections = sections.filter((section) => {
|
||||||
|
const match = section.match(/^\[(\d+\.\d+\.\d+)\]/);
|
||||||
|
if (!match) return false;
|
||||||
|
const ver = parseVersion(match[1]);
|
||||||
|
return ver > fromNum && ver <= toNum;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (relevantSections.length === 0) return undefined;
|
||||||
|
|
||||||
|
return relevantSections
|
||||||
|
.map((s) => '## ' + s.trim())
|
||||||
|
.join('\n\n');
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersion(version: string): number {
|
||||||
|
const parts = version.split('.').map(Number);
|
||||||
|
return (parts[0] || 0) * 10000 + (parts[1] || 0) * 100 + (parts[2] || 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { RiDownloadLine, RiExternalLinkLine, RiLoaderLine, RiRestartLine, RiSparklingLine } from '@remixicon/react';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
|
import { RiDownloadCloudLine, RiDownloadLine, RiExternalLinkLine, RiLoaderLine, RiRestartLine } from '@remixicon/react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { UpdateInfo, UpdateProgress } from '@/lib/desktop';
|
import type { UpdateInfo, UpdateProgress } from '@/lib/desktop';
|
||||||
|
|
||||||
@@ -47,24 +48,38 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
|||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<RiSparklingLine className="h-5 w-5 text-primary" />
|
<RiDownloadCloudLine className="h-5 w-5 text-primary" />
|
||||||
Update Available
|
Update Available
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 mt-2">
|
<div className="space-y-4 mt-2">
|
||||||
{info?.currentVersion && (
|
{(info?.currentVersion || info?.version) && (
|
||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<span className="text-muted-foreground">Current version</span>
|
{info?.currentVersion && (
|
||||||
<span className="font-mono">{info.currentVersion}</span>
|
<span className="font-mono">{info.currentVersion}</span>
|
||||||
|
)}
|
||||||
|
{info?.currentVersion && info?.version && (
|
||||||
|
<span className="text-muted-foreground">→</span>
|
||||||
|
)}
|
||||||
|
{info?.version && (
|
||||||
|
<span className="font-mono text-primary">{info.version}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{info?.version && (
|
{info?.body && (
|
||||||
<div className="flex items-center justify-between text-sm">
|
<ScrollableOverlay
|
||||||
<span className="text-muted-foreground">New version</span>
|
className="max-h-48 rounded-md border border-border bg-muted/30 p-3"
|
||||||
<span className="font-mono text-primary">{info.version}</span>
|
fillContainer={false}
|
||||||
</div>
|
>
|
||||||
|
<div className="text-sm text-muted-foreground whitespace-pre-wrap pr-3">
|
||||||
|
{info.body
|
||||||
|
.replace(/^## \[(\d+\.\d+\.\d+)\] - \d{4}-\d{2}-\d{2}\s*/gm, '— v$1 —\n')
|
||||||
|
.replace(/^- /gm, '• ')
|
||||||
|
.trim()}
|
||||||
|
</div>
|
||||||
|
</ScrollableOverlay>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{downloading && (
|
{downloading && (
|
||||||
@@ -94,15 +109,14 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-1 flex items-center justify-center gap-2 px-3 py-1.5 rounded-md',
|
'flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md',
|
||||||
'text-sm font-medium',
|
'text-sm text-muted-foreground',
|
||||||
'border border-border',
|
'hover:text-foreground hover:bg-accent',
|
||||||
'hover:bg-accent hover:text-accent-foreground',
|
|
||||||
'transition-colors'
|
'transition-colors'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<RiExternalLinkLine className="h-4 w-4" />
|
<RiExternalLinkLine className="h-4 w-4" />
|
||||||
View Release Notes
|
GitHub
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{!downloaded && !downloading && (
|
{!downloaded && !downloading && (
|
||||||
|
|||||||
@@ -25,31 +25,43 @@ export type UseUpdateCheckReturn = UpdateState & {
|
|||||||
dismiss: () => void;
|
dismiss: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MOCK_UPDATE: UpdateState = {
|
interface MockUpdateConfig {
|
||||||
|
currentVersion?: string;
|
||||||
|
newVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set window.__OPENCHAMBER_MOCK_UPDATE__ = { currentVersion: '1.0.3', newVersion: '1.0.8' } to test with real changelog
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__OPENCHAMBER_MOCK_UPDATE__?: boolean | MockUpdateConfig;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMockConfig = (): MockUpdateConfig | null => {
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
const mock = window.__OPENCHAMBER_MOCK_UPDATE__;
|
||||||
|
if (!mock) return null;
|
||||||
|
if (mock === true) return { currentVersion: '1.0.3', newVersion: '1.0.8' };
|
||||||
|
return mock;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createMockUpdate = (config: MockUpdateConfig): UpdateState => ({
|
||||||
checking: false,
|
checking: false,
|
||||||
available: true,
|
available: true,
|
||||||
downloading: false,
|
downloading: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
info: {
|
info: {
|
||||||
available: true,
|
available: true,
|
||||||
version: '99.0.0-test',
|
version: config.newVersion ?? '99.0.0-test',
|
||||||
currentVersion: '0.0.0',
|
currentVersion: config.currentVersion ?? '0.0.0',
|
||||||
body: 'Test update for UI development',
|
body: undefined,
|
||||||
},
|
},
|
||||||
progress: null,
|
progress: null,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
});
|
||||||
|
|
||||||
// Set window.__OPENCHAMBER_MOCK_UPDATE__ = true in console to test UI
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__OPENCHAMBER_MOCK_UPDATE__?: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldMockUpdate = (): boolean => {
|
const shouldMockUpdate = (): boolean => {
|
||||||
if (typeof window === 'undefined') return false;
|
return getMockConfig() !== null;
|
||||||
return window.__OPENCHAMBER_MOCK_UPDATE__ === true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||||
@@ -64,7 +76,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [mockMode, setMockMode] = useState(shouldMockUpdate);
|
const [mockMode, setMockMode] = useState(shouldMockUpdate);
|
||||||
const [mockState, setMockState] = useState<UpdateState>(MOCK_UPDATE);
|
const [mockState, setMockState] = useState<UpdateState | null>(null);
|
||||||
|
|
||||||
// Check for mock mode changes (for console toggling)
|
// Check for mock mode changes (for console toggling)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -73,7 +85,10 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
|||||||
if (shouldMock !== mockMode) {
|
if (shouldMock !== mockMode) {
|
||||||
setMockMode(shouldMock);
|
setMockMode(shouldMock);
|
||||||
if (shouldMock) {
|
if (shouldMock) {
|
||||||
setMockState(MOCK_UPDATE);
|
const config = getMockConfig();
|
||||||
|
if (config) {
|
||||||
|
setMockState(createMockUpdate(config));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -82,7 +97,15 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
|||||||
|
|
||||||
const checkForUpdates = useCallback(async () => {
|
const checkForUpdates = useCallback(async () => {
|
||||||
if (mockMode) {
|
if (mockMode) {
|
||||||
setMockState(MOCK_UPDATE);
|
const config = getMockConfig();
|
||||||
|
if (config) {
|
||||||
|
const mockUpdate = createMockUpdate(config);
|
||||||
|
mockUpdate.info = {
|
||||||
|
...mockUpdate.info!,
|
||||||
|
body: `## [${config.newVersion}] - 2025-01-01\n- Mock update for UI testing`,
|
||||||
|
};
|
||||||
|
setMockState(mockUpdate);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isDesktopRuntime()) {
|
if (!isDesktopRuntime()) {
|
||||||
@@ -173,7 +196,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
|||||||
|
|
||||||
const dismiss = useCallback(() => {
|
const dismiss = useCallback(() => {
|
||||||
if (mockMode) {
|
if (mockMode) {
|
||||||
setMockState(MOCK_UPDATE);
|
setMockState(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState((prev) => ({ ...prev, available: false, downloaded: false, info: null }));
|
setState((prev) => ({ ...prev, available: false, downloaded: false, info: null }));
|
||||||
@@ -189,7 +212,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
|||||||
}
|
}
|
||||||
}, [checkOnMount, checkForUpdates, mockMode]);
|
}, [checkOnMount, checkForUpdates, mockMode]);
|
||||||
|
|
||||||
const currentState = mockMode ? mockState : state;
|
const currentState = (mockMode && mockState) ? mockState : state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
|
|||||||
@@ -274,3 +274,4 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user