mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
📝 Add docstrings to main
Docstrings generation was requested by @amoscicki. * https://github.com/AutoMaker-Org/automaker/pull/290#issuecomment-3694458998 The following files were modified: * `apps/server/src/routes/updates/common.ts` * `apps/server/src/routes/updates/index.ts` * `apps/server/src/routes/updates/routes/check.ts` * `apps/server/src/routes/updates/routes/info.ts` * `apps/server/src/routes/updates/routes/pull.ts` * `apps/ui/src/components/updates/update-notifier.tsx` * `apps/ui/src/components/views/settings-view.tsx` * `apps/ui/src/components/views/settings-view/updates/updates-section.tsx` * `apps/ui/src/hooks/use-settings-migration.ts` * `apps/ui/src/hooks/use-update-polling.ts` * `apps/ui/src/lib/utils.ts` * `apps/ui/src/routes/__root.tsx`
This commit is contained in:
committed by
GitHub
parent
4a708aa305
commit
c9f164a1b4
173
apps/ui/src/components/updates/update-notifier.tsx
Normal file
173
apps/ui/src/components/updates/update-notifier.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Update Notifier Component
|
||||
*
|
||||
* Responsible for displaying toast notifications related to updates.
|
||||
* Subscribes to the updates store and reacts to state changes.
|
||||
*
|
||||
* This component handles the UI notifications, keeping them separate
|
||||
* from the business logic in the store.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useUpdatesStore } from '@/store/updates-store';
|
||||
import { useUpdatePolling } from '@/hooks/use-update-polling';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { getRepoDisplayName } from '@/lib/utils';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface UpdateNotifierProps {
|
||||
/** Custom handler for update available (for testing/DI) */
|
||||
onUpdateAvailable?: (remoteVersion: string) => void;
|
||||
|
||||
/** Custom handler for update installed (for testing/DI) */
|
||||
onUpdateInstalled?: (newVersion: string, alreadyUpToDate: boolean) => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Displays persistent toasts for available and installed application updates.
|
||||
*
|
||||
* Shows a persistent "Update Available" toast when a new remote version is detected and,
|
||||
* after initiating an update, shows success toasts for either "Already up to date!" or
|
||||
* "Update installed!" with actions to restart now or later.
|
||||
*
|
||||
* @param onUpdateAvailable - Optional callback invoked with `remoteVersion` when an update is detected; providing this prevents the default availability toast.
|
||||
* @param onUpdateInstalled - Optional callback invoked with `(newVersion, alreadyUpToDate)` after attempting to install updates; providing this prevents the default installation toasts.
|
||||
* @returns Null (this component renders no visible UI; it manages global toast notifications).
|
||||
*/
|
||||
export function UpdateNotifier({ onUpdateAvailable, onUpdateInstalled }: UpdateNotifierProps = {}) {
|
||||
// Store state
|
||||
const { updateAvailable, remoteVersionShort, pullUpdates, isPulling } = useUpdatesStore();
|
||||
|
||||
const { autoUpdate } = useAppStore();
|
||||
|
||||
// Start polling
|
||||
useUpdatePolling();
|
||||
|
||||
// Track shown toasts to avoid duplicates
|
||||
const shownToastForCommitRef = useRef<string | null>(null);
|
||||
const toastIdRef = useRef<string | number | null>(null);
|
||||
|
||||
// Handle "Update Now" click
|
||||
const handleUpdateNow = useCallback(async () => {
|
||||
const result = await pullUpdates();
|
||||
|
||||
if (result) {
|
||||
// Dismiss the "update available" toast
|
||||
if (toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current);
|
||||
toastIdRef.current = null;
|
||||
}
|
||||
|
||||
// Call custom handler if provided
|
||||
if (onUpdateInstalled) {
|
||||
onUpdateInstalled(result.newVersionShort, result.alreadyUpToDate);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show appropriate toast based on result
|
||||
if (result.alreadyUpToDate) {
|
||||
toast.success('Already up to date!');
|
||||
} else {
|
||||
toast.success('Update installed!', {
|
||||
description: result.message,
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Restart Now',
|
||||
onClick: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
label: 'Later',
|
||||
onClick: () => {
|
||||
// Just dismiss - user will restart manually later
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [pullUpdates, onUpdateInstalled]);
|
||||
|
||||
// Show toast when update becomes available
|
||||
useEffect(() => {
|
||||
if (!updateAvailable || !remoteVersionShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show toast if we've already shown it for this version
|
||||
if (shownToastForCommitRef.current === remoteVersionShort) {
|
||||
return;
|
||||
}
|
||||
|
||||
shownToastForCommitRef.current = remoteVersionShort;
|
||||
|
||||
// Call custom handler if provided
|
||||
if (onUpdateAvailable) {
|
||||
onUpdateAvailable(remoteVersionShort);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dismiss any existing toast
|
||||
if (toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current);
|
||||
}
|
||||
|
||||
// Extract repo name for display
|
||||
const repoName = getRepoDisplayName(autoUpdate.upstreamUrl);
|
||||
|
||||
// Show persistent toast with update button
|
||||
toastIdRef.current = toast.info('Update Available', {
|
||||
description: `New version (${remoteVersionShort}) available from ${repoName}`,
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: isPulling ? 'Updating...' : 'Update Now',
|
||||
onClick: handleUpdateNow,
|
||||
},
|
||||
cancel: {
|
||||
label: 'Later',
|
||||
onClick: () => {
|
||||
// Dismiss toast - won't show again for this version until a new version appears
|
||||
shownToastForCommitRef.current = remoteVersionShort;
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [
|
||||
updateAvailable,
|
||||
remoteVersionShort,
|
||||
autoUpdate.upstreamUrl,
|
||||
isPulling,
|
||||
handleUpdateNow,
|
||||
onUpdateAvailable,
|
||||
]);
|
||||
|
||||
// Clean up toast on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Reset shown toast when update is no longer available
|
||||
useEffect(() => {
|
||||
if (!updateAvailable) {
|
||||
shownToastForCommitRef.current = null;
|
||||
if (toastIdRef.current) {
|
||||
toast.dismiss(toastIdRef.current);
|
||||
toastIdRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [updateAvailable]);
|
||||
|
||||
// This component doesn't render anything visible
|
||||
return null;
|
||||
}
|
||||
@@ -17,10 +17,18 @@ import { TerminalSection } from './settings-view/terminal/terminal-section';
|
||||
import { AudioSection } from './settings-view/audio/audio-section';
|
||||
import { KeyboardShortcutsSection } from './settings-view/keyboard-shortcuts/keyboard-shortcuts-section';
|
||||
import { FeatureDefaultsSection } from './settings-view/feature-defaults/feature-defaults-section';
|
||||
import { UpdatesSection } from './settings-view/updates/updates-section';
|
||||
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
|
||||
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
|
||||
import type { Project as ElectronProject } from '@/lib/electron';
|
||||
|
||||
/**
|
||||
* Render the application settings view, including navigation, section panels, theme and project preferences, CLI status, and global dialogs.
|
||||
*
|
||||
* Renders the appropriate settings subsection based on the active view, wires UI controls to the app store for reading and updating settings (theme, defaults, features, updates, audio, keyboard, API keys, Claude/CLI settings, etc.), and manages visibility of the keyboard map and delete-project dialogs.
|
||||
*
|
||||
* @returns The settings view JSX element
|
||||
*/
|
||||
export function SettingsView() {
|
||||
const {
|
||||
theme,
|
||||
@@ -52,6 +60,8 @@ export function SettingsView() {
|
||||
setAutoLoadClaudeMd,
|
||||
enableSandboxMode,
|
||||
setEnableSandboxMode,
|
||||
autoUpdate,
|
||||
setAutoUpdate,
|
||||
} = useAppStore();
|
||||
|
||||
// Hide usage tracking when using API key (only show for Claude Code CLI users)
|
||||
@@ -158,6 +168,8 @@ export function SettingsView() {
|
||||
onValidationModelChange={setValidationModel}
|
||||
/>
|
||||
);
|
||||
case 'updates':
|
||||
return <UpdatesSection autoUpdate={autoUpdate} onAutoUpdateChange={setAutoUpdate} />;
|
||||
case 'danger':
|
||||
return (
|
||||
<DangerZoneSection
|
||||
@@ -203,4 +215,4 @@ export function SettingsView() {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Updates Section Component
|
||||
*
|
||||
* Settings panel for configuring and managing auto-updates.
|
||||
* Uses the centralized updates-store for state and actions.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
RefreshCw,
|
||||
GitBranch,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { cn, getRepoDisplayName } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { useUpdatesStore } from '@/store/updates-store';
|
||||
import type { AutoUpdateSettings } from '@automaker/types';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
interface UpdatesSectionProps {
|
||||
autoUpdate: AutoUpdateSettings;
|
||||
onAutoUpdateChange: (settings: Partial<AutoUpdateSettings>) => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Component
|
||||
/**
|
||||
* Renders the Updates settings panel and manages update-related actions and UI.
|
||||
*
|
||||
* Fetches current update info on mount, exposes controls for auto-update settings
|
||||
* (enabled, check interval, upstream URL), and provides actions to check for and
|
||||
* pull updates with user-facing notifications.
|
||||
*
|
||||
* @param autoUpdate - Current auto-update configuration (enabled, checkIntervalMinutes, upstreamUrl).
|
||||
* @param onAutoUpdateChange - Callback invoked with partial updates to apply to the auto-update configuration.
|
||||
* @returns The Updates settings React element.
|
||||
*/
|
||||
|
||||
export function UpdatesSection({ autoUpdate, onAutoUpdateChange }: UpdatesSectionProps) {
|
||||
// Use centralized store
|
||||
const {
|
||||
info,
|
||||
updateAvailable,
|
||||
remoteVersionShort,
|
||||
isChecking,
|
||||
isPulling,
|
||||
isLoadingInfo,
|
||||
error,
|
||||
fetchInfo,
|
||||
checkForUpdates,
|
||||
pullUpdates,
|
||||
clearError,
|
||||
} = useUpdatesStore();
|
||||
|
||||
// Fetch info on mount
|
||||
useEffect(() => {
|
||||
fetchInfo();
|
||||
}, [fetchInfo]);
|
||||
|
||||
// Handle check for updates with toast notifications
|
||||
const handleCheckForUpdates = async () => {
|
||||
clearError();
|
||||
const hasUpdate = await checkForUpdates();
|
||||
|
||||
if (hasUpdate) {
|
||||
toast.success('Update available!', {
|
||||
description: `New version: ${useUpdatesStore.getState().remoteVersionShort}`,
|
||||
});
|
||||
} else if (!useUpdatesStore.getState().error) {
|
||||
toast.success('You are up to date!');
|
||||
} else {
|
||||
toast.error(useUpdatesStore.getState().error || 'Failed to check for updates');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle pull updates with toast notifications
|
||||
const handlePullUpdates = async () => {
|
||||
clearError();
|
||||
const result = await pullUpdates();
|
||||
|
||||
if (result) {
|
||||
if (result.alreadyUpToDate) {
|
||||
toast.success('Already up to date!');
|
||||
} else {
|
||||
toast.success('Update installed!', {
|
||||
description: result.message,
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Restart Now',
|
||||
onClick: () => {
|
||||
window.location.reload();
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
label: 'Later',
|
||||
onClick: () => {
|
||||
// Just dismiss - user will restart manually later
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (useUpdatesStore.getState().error) {
|
||||
toast.error(useUpdatesStore.getState().error || 'Failed to pull updates');
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = isChecking || isPulling || isLoadingInfo;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl overflow-hidden',
|
||||
'border border-border/50',
|
||||
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||
'shadow-sm shadow-black/5'
|
||||
)}
|
||||
>
|
||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||
<RefreshCw className="w-5 h-5 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold text-foreground tracking-tight">Updates</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||
Check for and install updates from the upstream repository.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Current Version Info */}
|
||||
{info && (
|
||||
<div className="p-4 rounded-xl bg-accent/20 border border-border/30">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
<span className="text-sm font-medium">Current Installation</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Version:</span>
|
||||
<code className="font-mono text-foreground">
|
||||
{info.currentVersionShort || 'Unknown'}
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Branch:</span>
|
||||
<span className="text-foreground">{info.currentBranch || 'Unknown'}</span>
|
||||
</div>
|
||||
{info.hasLocalChanges && (
|
||||
<div className="flex items-center gap-2 text-yellow-500">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
<span>Local changes detected</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Update Status */}
|
||||
{updateAvailable && remoteVersionShort && (
|
||||
<div className="p-4 rounded-xl bg-green-500/10 border border-green-500/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-500" />
|
||||
<span className="font-medium text-green-500">Update Available</span>
|
||||
</div>
|
||||
<code className="font-mono text-sm text-green-500">{remoteVersionShort}</code>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle className="w-5 h-5 text-red-500" />
|
||||
<span className="text-red-500">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Auto-Update Toggle */}
|
||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||
<Checkbox
|
||||
id="auto-update-enabled"
|
||||
checked={autoUpdate.enabled}
|
||||
onCheckedChange={(checked) => onAutoUpdateChange({ enabled: !!checked })}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label
|
||||
htmlFor="auto-update-enabled"
|
||||
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 text-brand-500" />
|
||||
Enable automatic update checks
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||
Periodically check for new updates from the upstream repository.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Check Interval */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="check-interval" className="text-sm font-medium">
|
||||
Check interval (minutes)
|
||||
</Label>
|
||||
<Input
|
||||
id="check-interval"
|
||||
type="number"
|
||||
min={1}
|
||||
max={60}
|
||||
value={autoUpdate.checkIntervalMinutes}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value, 10);
|
||||
if (!isNaN(value) && value >= 1 && value <= 60) {
|
||||
onAutoUpdateChange({ checkIntervalMinutes: value });
|
||||
}
|
||||
}}
|
||||
className="w-32"
|
||||
disabled={!autoUpdate.enabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
How often to check for updates (1-60 minutes).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Upstream URL */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="upstream-url" className="text-sm font-medium">
|
||||
Upstream repository URL
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="upstream-url"
|
||||
type="url"
|
||||
value={autoUpdate.upstreamUrl}
|
||||
onChange={(e) => onAutoUpdateChange({ upstreamUrl: e.target.value })}
|
||||
placeholder="https://github.com/AutoMaker-Org/automaker.git"
|
||||
className="flex-1 font-mono text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
const url = autoUpdate.upstreamUrl.replace(/\.git$/, '');
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
title="Open repository"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Repository to check for updates. Default: {getRepoDisplayName(autoUpdate.upstreamUrl)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-3 pt-4 border-t border-border/30">
|
||||
<Button onClick={handleCheckForUpdates} disabled={isLoading} variant="outline">
|
||||
{isChecking ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Checking...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Check for Updates
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{updateAvailable && (
|
||||
<Button onClick={handlePullUpdates} disabled={isLoading}>
|
||||
{isPulling ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Updating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||
Update Now
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -180,17 +180,12 @@ export function useSettingsMigration(): MigrationState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync current global settings to file-based server storage
|
||||
* Persist selected global settings from localStorage to the file-based server storage.
|
||||
*
|
||||
* Reads the current Zustand state from localStorage and sends all global settings
|
||||
* to the server to be written to {dataDir}/settings.json.
|
||||
* Reads the `automaker-storage` entry (Zustand state) and sends the relevant global settings
|
||||
* to the server so they are written to the application's settings file.
|
||||
*
|
||||
* Call this when important global settings change (theme, UI preferences, profiles, etc.)
|
||||
* Safe to call from store subscribers or change handlers.
|
||||
*
|
||||
* Only functions in Electron mode. Returns false if not in Electron or on error.
|
||||
*
|
||||
* @returns Promise resolving to true if sync succeeded, false otherwise
|
||||
* @returns `true` if the server acknowledged the update, `false` otherwise.
|
||||
*/
|
||||
export async function syncSettingsToServer(): Promise<boolean> {
|
||||
if (!isElectron()) return false;
|
||||
@@ -232,6 +227,7 @@ export async function syncSettingsToServer(): Promise<boolean> {
|
||||
projectHistory: state.projectHistory,
|
||||
projectHistoryIndex: state.projectHistoryIndex,
|
||||
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
||||
autoUpdate: state.autoUpdate,
|
||||
};
|
||||
|
||||
const result = await api.settings.updateGlobal(updates);
|
||||
@@ -315,4 +311,4 @@ export async function syncProjectSettingsToServer(
|
||||
console.error('[Settings Sync] Failed to sync project settings:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
apps/ui/src/hooks/use-update-polling.ts
Normal file
105
apps/ui/src/hooks/use-update-polling.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Update Polling Hook
|
||||
*
|
||||
* Handles the background polling logic for checking updates.
|
||||
* Separated from the store to follow single responsibility principle.
|
||||
*
|
||||
* This hook only manages WHEN to check, not HOW to check.
|
||||
* The actual check logic lives in the updates-store.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useUpdatesStore } from '@/store/updates-store';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface UseUpdatePollingOptions {
|
||||
/** Override the check function (for testing/DI) */
|
||||
onCheck?: () => Promise<boolean>;
|
||||
|
||||
/** Override enabled state (for testing) */
|
||||
enabled?: boolean;
|
||||
|
||||
/** Override interval in minutes (for testing) */
|
||||
intervalMinutes?: number;
|
||||
}
|
||||
|
||||
export interface UseUpdatePollingResult {
|
||||
/** Whether polling is currently active */
|
||||
isPollingActive: boolean;
|
||||
|
||||
/** Manually trigger a check */
|
||||
checkNow: () => Promise<boolean>;
|
||||
|
||||
/** Last check timestamp */
|
||||
lastChecked: Date | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hook
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Manages periodic background checks for updates and exposes controls and status.
|
||||
*
|
||||
* @param options - Optional overrides for testing or dependency injection:
|
||||
* - `onCheck`: override the function used to perform an update check
|
||||
* - `enabled`: force polling enabled or disabled
|
||||
* - `intervalMinutes`: override the polling interval in minutes
|
||||
* @returns An object with polling status and controls:
|
||||
* - `isPollingActive`: `true` when polling is enabled, `false` otherwise
|
||||
* - `checkNow`: a function that triggers an immediate update check and returns `true` if an update was found, `false` otherwise
|
||||
* - `lastChecked`: timestamp of the last performed check, or `null` if never checked
|
||||
*/
|
||||
export function useUpdatePolling(options: UseUpdatePollingOptions = {}): UseUpdatePollingResult {
|
||||
const { autoUpdate } = useAppStore();
|
||||
const { checkForUpdates, lastChecked } = useUpdatesStore();
|
||||
|
||||
// Allow overrides for testing
|
||||
const isEnabled = options.enabled ?? autoUpdate.enabled;
|
||||
const intervalMinutes = options.intervalMinutes ?? autoUpdate.checkIntervalMinutes;
|
||||
|
||||
// Stabilize the check function reference to prevent interval resets
|
||||
const onCheckRef = useRef(options.onCheck ?? checkForUpdates);
|
||||
onCheckRef.current = options.onCheck ?? checkForUpdates;
|
||||
|
||||
const stableOnCheck = useCallback(() => onCheckRef.current(), []);
|
||||
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear any existing interval
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
// Don't set up polling if disabled
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check immediately on enable
|
||||
stableOnCheck();
|
||||
|
||||
// Set up interval
|
||||
const intervalMs = intervalMinutes * 60 * 1000;
|
||||
intervalRef.current = setInterval(stableOnCheck, intervalMs);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isEnabled, intervalMinutes, stableOnCheck]);
|
||||
|
||||
return {
|
||||
isPollingActive: isEnabled,
|
||||
checkNow: stableOnCheck,
|
||||
lastChecked,
|
||||
};
|
||||
}
|
||||
@@ -63,3 +63,14 @@ export const isMac =
|
||||
: typeof navigator !== 'undefined' &&
|
||||
(/Mac/.test(navigator.userAgent) ||
|
||||
(navigator.platform ? navigator.platform.toLowerCase().includes('mac') : false));
|
||||
|
||||
/**
|
||||
* Get the owner/repo display name from a GitHub repository URL.
|
||||
*
|
||||
* @param url - A repository URL (common GitHub formats like `https://github.com/owner/repo`, `git@github.com:owner/repo.git`, or `github.com/owner/repo`)
|
||||
* @returns The `owner/repo` string when the URL is a GitHub repository, otherwise `'upstream'`
|
||||
*/
|
||||
export function getRepoDisplayName(url: string): string {
|
||||
const match = url.match(/github\.com[/:]([^/]+\/[^/.]+)/);
|
||||
return match ? match[1] : 'upstream';
|
||||
}
|
||||
@@ -11,7 +11,17 @@ import { useSetupStore } from '@/store/setup-store';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { Toaster } from 'sonner';
|
||||
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||
import { UpdateNotifier } from '@/components/updates';
|
||||
|
||||
/**
|
||||
* Render the application's root layout and manage global UI state, routing, and integrations.
|
||||
*
|
||||
* This component provides the main application shell: sidebar, route Outlet, a hidden streamer panel,
|
||||
* theme application, first-run/setup routing handling, file browser binding, IPC connection testing,
|
||||
* global keyboard shortcut handling, and global UI utilities like the Toaster and UpdateNotifier.
|
||||
*
|
||||
* @returns The root layout element containing the sidebar, main content Outlet, hidden streamer panel, Toaster, and UpdateNotifier.
|
||||
*/
|
||||
function RootLayoutContent() {
|
||||
const location = useLocation();
|
||||
const { setIpcConnected, currentProject, getEffectiveTheme } = useAppStore();
|
||||
@@ -175,6 +185,7 @@ function RootLayoutContent() {
|
||||
}`}
|
||||
/>
|
||||
<Toaster richColors position="bottom-right" />
|
||||
<UpdateNotifier />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -189,4 +200,4 @@ function RootLayout() {
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootLayout,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user