feat: implement update management system with new update store and UI components
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user