feat: implement update management system with new update store and UI components

This commit is contained in:
Bohdan Triapitsyn
2025-12-20 01:04:32 +02:00
parent 5982767bdf
commit 79ed6abab7
8 changed files with 317 additions and 288 deletions
+10 -2
View File
@@ -7,6 +7,7 @@ import {
import { RiChat4Line, RiCodeLine, RiCommandLine, RiGitBranchLine, RiLayoutLeftLine, RiPlayListAddLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, type RemixiconComponentType } from '@remixicon/react';
import { useUIStore, type MainTab } from '@/stores/useUIStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { useSessionStore } from '@/stores/useSessionStore';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
@@ -95,6 +96,7 @@ export const Header: React.FC = () => {
const getContextUsage = useSessionStore((state) => state.getContextUsage);
const { isMobile } = useDeviceInfo();
const diffFileCount = useDiffFileCount();
const updateAvailable = useUpdateStore((state) => state.available);
const headerRef = React.useRef<HTMLElement | null>(null);
@@ -491,13 +493,19 @@ export const Header: React.FC = () => {
type="button"
onClick={handleOpenSettings}
aria-label="Open settings"
className={headerIconButtonClass}
className={cn(headerIconButtonClass, 'relative')}
>
<RiSettings3Line className="h-5 w-5" />
{updateAvailable && (
<span
className="absolute top-1.5 right-1.5 h-2 w-2 rounded-full bg-primary"
aria-label="Update available"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent>
<p>Settings</p>
<p>{updateAvailable ? 'Settings (Update available)' : 'Settings'}</p>
</TooltipContent>
</Tooltip>
</div>
@@ -11,6 +11,7 @@ import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { DiffWorkerProvider } from '@/contexts/DiffWorkerProvider';
import { useUIStore } from '@/stores/useUIStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { useDeviceInfo } from '@/lib/device';
import { useEdgeSwipe } from '@/hooks/useEdgeSwipe';
import { cn } from '@/lib/utils';
@@ -44,6 +45,15 @@ export const MainLayout: React.FC = () => {
useEdgeSwipe({ enabled: true });
// Trigger update check 3 seconds after mount (for both mobile and desktop)
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
React.useEffect(() => {
const timer = setTimeout(() => {
checkForUpdates();
}, 3000);
return () => clearTimeout(timer);
}, [checkForUpdates]);
React.useEffect(() => {
const previous = useUIStore.getState().isMobile;
if (previous !== isMobile) {
+12 -12
View File
@@ -4,7 +4,7 @@ import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { ErrorBoundary } from '../ui/ErrorBoundary';
import { useUIStore } from '@/stores/useUIStore';
import { useUpdateCheck } from '@/hooks/useUpdateCheck';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { UpdateDialog } from '../ui/UpdateDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
@@ -27,11 +27,11 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
const startWidthRef = React.useRef(sidebarWidth || SIDEBAR_CONTENT_WIDTH);
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
const update = useUpdateCheck();
const updateStore = useUpdateStore();
const pendingMenuUpdateCheckRef = React.useRef(false);
const checkForUpdates = update.checkForUpdates;
const { available, downloaded, checking } = update;
const checkForUpdates = updateStore.checkForUpdates;
const { available, downloaded, checking } = updateStore;
const [isDesktopApp, setIsDesktopApp] = React.useState<boolean>(() => {
if (typeof window === 'undefined') {
@@ -267,14 +267,14 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
<UpdateDialog
open={updateDialogOpen}
onOpenChange={setUpdateDialogOpen}
info={update.info}
downloading={update.downloading}
downloaded={update.downloaded}
progress={update.progress}
error={update.error}
onDownload={update.downloadUpdate}
onRestart={update.restartToUpdate}
runtimeType={update.runtimeType}
info={updateStore.info}
downloading={updateStore.downloading}
downloaded={updateStore.downloaded}
progress={updateStore.progress}
error={updateStore.error}
onDownload={updateStore.downloadUpdate}
onRestart={updateStore.restartToUpdate}
runtimeType={updateStore.runtimeType}
/>
</div>
</aside>
@@ -0,0 +1,120 @@
import React from 'react';
import { RiDownloadLine, RiGithubFill, RiLoaderLine, RiTwitterXFill } from '@remixicon/react';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { UpdateDialog } from '@/components/ui/UpdateDialog';
import { cn } from '@/lib/utils';
const GITHUB_URL = 'https://github.com/btriapitsyn/openchamber';
export const AboutSettings: React.FC = () => {
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
const updateStore = useUpdateStore();
const currentVersion = updateStore.info?.currentVersion || 'unknown';
return (
<div className="w-full space-y-6">
<div className="space-y-1">
<h3 className="typography-ui-header font-semibold text-foreground">
About OpenChamber
</h3>
</div>
{/* Version and Update */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<div className="typography-ui-label text-muted-foreground">Version</div>
<div className="typography-ui-header font-mono">{currentVersion}</div>
</div>
{updateStore.checking && (
<div className="flex items-center gap-2 text-muted-foreground">
<RiLoaderLine className="h-4 w-4 animate-spin" />
<span className="typography-meta">Checking...</span>
</div>
)}
{!updateStore.checking && updateStore.available && (
<button
onClick={() => setUpdateDialogOpen(true)}
className={cn(
'flex items-center gap-2 px-3 py-1.5 rounded-md',
'text-sm font-medium',
'bg-primary text-primary-foreground',
'hover:bg-primary/90',
'transition-colors'
)}
>
<RiDownloadLine className="h-4 w-4" />
Update to {updateStore.info?.version}
</button>
)}
{!updateStore.checking && !updateStore.available && !updateStore.error && (
<span className="typography-meta text-muted-foreground">Up to date</span>
)}
</div>
{updateStore.error && (
<p className="typography-meta text-destructive">{updateStore.error}</p>
)}
<button
onClick={() => updateStore.checkForUpdates()}
disabled={updateStore.checking}
className={cn(
'typography-meta text-muted-foreground hover:text-foreground',
'underline-offset-2 hover:underline',
'disabled:opacity-50 disabled:cursor-not-allowed'
)}
>
Check for updates
</button>
</div>
{/* Links */}
<div className="flex items-center gap-4">
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className={cn(
'flex items-center gap-1.5 text-muted-foreground hover:text-foreground',
'typography-meta transition-colors'
)}
>
<RiGithubFill className="h-4 w-4" />
<span>GitHub</span>
</a>
<a
href="https://x.com/btriapitsyn"
target="_blank"
rel="noopener noreferrer"
className={cn(
'flex items-center gap-1.5 text-muted-foreground hover:text-foreground',
'typography-meta transition-colors'
)}
>
<RiTwitterXFill className="h-4 w-4" />
<span>@btriapitsyn</span>
</a>
</div>
{/* Update Dialog */}
<UpdateDialog
open={updateDialogOpen}
onOpenChange={setUpdateDialogOpen}
info={updateStore.info}
downloading={updateStore.downloading}
downloaded={updateStore.downloaded}
progress={updateStore.progress}
error={updateStore.error}
onDownload={updateStore.downloadUpdate}
onRestart={updateStore.restartToUpdate}
runtimeType={updateStore.runtimeType}
/>
</div>
);
};
@@ -1,14 +1,25 @@
import React from 'react';
import { AppearanceSettings } from './AppearanceSettings';
import { AboutSettings } from './AboutSettings';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { useDeviceInfo } from '@/lib/device';
import { isWebRuntime } from '@/lib/desktop';
export const SettingsPage: React.FC = () => {
const { isMobile } = useDeviceInfo();
const showAbout = isMobile && isWebRuntime();
return (
<ScrollableOverlay
outerClassName="h-full"
className="settings-page-body mx-auto max-w-3xl space-y-3 p-3 sm:space-y-6 sm:p-6"
>
<AppearanceSettings />
{showAbout && (
<div className="border-t border-border/40 pt-6">
<AboutSettings />
</div>
)}
</ScrollableOverlay>
);
};