feat: enhance update dialog with changelog notes
This commit is contained in:
Generated
+1
-1
@@ -2847,7 +2847,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openchamber-desktop"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
@@ -30,21 +30,26 @@ let cachedUpdate: Update | null = null;
|
||||
export async function checkForUpdates(): Promise<UpdateInfo> {
|
||||
try {
|
||||
const { check } = await import('@tauri-apps/plugin-updater');
|
||||
const update = await check();
|
||||
const [update, currentVersion] = await Promise.all([
|
||||
check(),
|
||||
getCurrentVersion(),
|
||||
]);
|
||||
cachedUpdate = update;
|
||||
|
||||
if (!update) {
|
||||
return {
|
||||
available: false,
|
||||
currentVersion: await getCurrentVersion(),
|
||||
currentVersion,
|
||||
};
|
||||
}
|
||||
|
||||
const changelogNotes = await fetchChangelogNotes(currentVersion, update.version);
|
||||
|
||||
return {
|
||||
available: true,
|
||||
version: update.version,
|
||||
currentVersion: await getCurrentVersion(),
|
||||
body: update.body ?? undefined,
|
||||
currentVersion,
|
||||
body: changelogNotes ?? update.body ?? undefined,
|
||||
date: update.date ?? undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -103,3 +108,38 @@ async function getCurrentVersion(): Promise<string> {
|
||||
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,
|
||||
DialogTitle,
|
||||
} 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 type { UpdateInfo, UpdateProgress } from '@/lib/desktop';
|
||||
|
||||
@@ -47,24 +48,38 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<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
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-2">
|
||||
{info?.currentVersion && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">Current version</span>
|
||||
<span className="font-mono">{info.currentVersion}</span>
|
||||
{(info?.currentVersion || info?.version) && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
{info?.currentVersion && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{info?.version && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">New version</span>
|
||||
<span className="font-mono text-primary">{info.version}</span>
|
||||
</div>
|
||||
{info?.body && (
|
||||
<ScrollableOverlay
|
||||
className="max-h-48 rounded-md border border-border bg-muted/30 p-3"
|
||||
fillContainer={false}
|
||||
>
|
||||
<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 && (
|
||||
@@ -94,15 +109,14 @@ export const UpdateDialog: React.FC<UpdateDialogProps> = ({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 px-3 py-1.5 rounded-md',
|
||||
'text-sm font-medium',
|
||||
'border border-border',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
'flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-md',
|
||||
'text-sm text-muted-foreground',
|
||||
'hover:text-foreground hover:bg-accent',
|
||||
'transition-colors'
|
||||
)}
|
||||
>
|
||||
<RiExternalLinkLine className="h-4 w-4" />
|
||||
View Release Notes
|
||||
GitHub
|
||||
</a>
|
||||
|
||||
{!downloaded && !downloading && (
|
||||
|
||||
@@ -25,31 +25,43 @@ export type UseUpdateCheckReturn = UpdateState & {
|
||||
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,
|
||||
available: true,
|
||||
downloading: false,
|
||||
downloaded: false,
|
||||
info: {
|
||||
available: true,
|
||||
version: '99.0.0-test',
|
||||
currentVersion: '0.0.0',
|
||||
body: 'Test update for UI development',
|
||||
version: config.newVersion ?? '99.0.0-test',
|
||||
currentVersion: config.currentVersion ?? '0.0.0',
|
||||
body: undefined,
|
||||
},
|
||||
progress: 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 => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return window.__OPENCHAMBER_MOCK_UPDATE__ === true;
|
||||
return getMockConfig() !== null;
|
||||
};
|
||||
|
||||
export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||
@@ -64,7 +76,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||
});
|
||||
|
||||
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)
|
||||
useEffect(() => {
|
||||
@@ -73,7 +85,10 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||
if (shouldMock !== mockMode) {
|
||||
setMockMode(shouldMock);
|
||||
if (shouldMock) {
|
||||
setMockState(MOCK_UPDATE);
|
||||
const config = getMockConfig();
|
||||
if (config) {
|
||||
setMockState(createMockUpdate(config));
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
@@ -82,7 +97,15 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
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;
|
||||
}
|
||||
if (!isDesktopRuntime()) {
|
||||
@@ -173,7 +196,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
if (mockMode) {
|
||||
setMockState(MOCK_UPDATE);
|
||||
setMockState(null);
|
||||
return;
|
||||
}
|
||||
setState((prev) => ({ ...prev, available: false, downloaded: false, info: null }));
|
||||
@@ -189,7 +212,7 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
|
||||
}
|
||||
}, [checkOnMount, checkForUpdates, mockMode]);
|
||||
|
||||
const currentState = mockMode ? mockState : state;
|
||||
const currentState = (mockMode && mockState) ? mockState : state;
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
|
||||
@@ -274,3 +274,4 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user