mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
versions -> update and check for updates on app boot
This commit is contained in:
parent
45a095d20b
commit
6894614b54
13 changed files with 579 additions and 53 deletions
|
|
@ -55,10 +55,10 @@
|
|||
"@types/react-dom": "^19.1.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.40.0",
|
||||
"@typescript-eslint/parser": "^8.40.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.1",
|
||||
"cross-env": "^10.0.0",
|
||||
"cspell": "^9.2.0",
|
||||
"electron": "^37.3.0",
|
||||
"electron": "^37.3.1",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-vite": "^4.0.0",
|
||||
"eslint": "^9.33.0",
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
"prettier": "^3.6.2",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.2",
|
||||
"vite": "^7.1.3",
|
||||
"wait-on": "^8.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
62
src/App.tsx
62
src/App.tsx
|
|
@ -4,11 +4,15 @@ import { DownloadScreen } from '@/components/screens/Download';
|
|||
import { LaunchScreen } from '@/components/screens/Launch';
|
||||
import { InterfaceScreen } from '@/components/screens/Interface';
|
||||
import { UpdateDialog } from '@/components/UpdateDialog';
|
||||
import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
|
||||
import { SettingsModal } from '@/components/settings/SettingsModal';
|
||||
import { ScreenTransition } from '@/components/ScreenTransition';
|
||||
import { AppHeader } from '@/components/AppHeader';
|
||||
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import { UI } from '@/constants';
|
||||
import type { UpdateInfo } from '@/types';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
|
||||
type Screen = 'download' | 'launch' | 'interface';
|
||||
|
||||
|
|
@ -24,6 +28,19 @@ export const App = () => {
|
|||
const [isImageGenerationMode, setIsImageGenerationMode] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const {
|
||||
updateInfo: binaryUpdateInfo,
|
||||
showUpdateModal,
|
||||
checkForUpdates,
|
||||
dismissUpdate,
|
||||
} = useUpdateChecker();
|
||||
|
||||
const {
|
||||
handleDownload: sharedHandleDownload,
|
||||
downloading,
|
||||
downloadProgress,
|
||||
} = useKoboldVersions();
|
||||
|
||||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
try {
|
||||
|
|
@ -54,6 +71,12 @@ export const App = () => {
|
|||
}
|
||||
|
||||
setHasInitialized(true);
|
||||
|
||||
if (versions.length > 0) {
|
||||
setTimeout(() => {
|
||||
checkForUpdates();
|
||||
}, 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Error checking installation:',
|
||||
|
|
@ -86,7 +109,28 @@ export const App = () => {
|
|||
cleanupInstallDirListener();
|
||||
cleanupVersionsListener();
|
||||
};
|
||||
}, []);
|
||||
}, [checkForUpdates]);
|
||||
|
||||
const handleBinaryUpdate = async (download: DownloadItem) => {
|
||||
try {
|
||||
const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
|
||||
const success = await sharedHandleDownload(
|
||||
downloadType,
|
||||
download,
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
if (success) {
|
||||
dismissUpdate();
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to update binary:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadComplete = async () => {
|
||||
try {
|
||||
|
|
@ -235,6 +279,22 @@ export const App = () => {
|
|||
onAccept={handleUpdateAccept}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showUpdateModal && binaryUpdateInfo && (
|
||||
<UpdateAvailableModal
|
||||
opened={showUpdateModal}
|
||||
onClose={dismissUpdate}
|
||||
currentVersion={binaryUpdateInfo.currentVersion}
|
||||
availableUpdate={binaryUpdateInfo.availableUpdate}
|
||||
onUpdate={handleBinaryUpdate}
|
||||
isDownloading={
|
||||
downloading === binaryUpdateInfo.availableUpdate.name
|
||||
}
|
||||
downloadProgress={
|
||||
downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</AppShell.Main>
|
||||
<SettingsModal
|
||||
opened={settingsOpened}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,11 @@ interface DownloadCardProps {
|
|||
isDownloading?: boolean;
|
||||
downloadProgress?: number;
|
||||
disabled?: boolean;
|
||||
hasUpdate?: boolean;
|
||||
newerVersion?: string;
|
||||
onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
onMakeCurrent?: () => void;
|
||||
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export const DownloadCard = ({
|
||||
|
|
@ -39,10 +42,35 @@ export const DownloadCard = ({
|
|||
isDownloading = false,
|
||||
downloadProgress = 0,
|
||||
disabled = false,
|
||||
hasUpdate = false,
|
||||
newerVersion,
|
||||
onDownload,
|
||||
onMakeCurrent,
|
||||
onUpdate,
|
||||
}: DownloadCardProps) => {
|
||||
const renderActionButton = () => {
|
||||
if (hasUpdate && onUpdate) {
|
||||
return (
|
||||
<Button
|
||||
variant="filled"
|
||||
size="xs"
|
||||
onClick={onUpdate}
|
||||
loading={isDownloading}
|
||||
disabled={disabled}
|
||||
color="orange"
|
||||
leftSection={
|
||||
isDownloading ? (
|
||||
<Loader size="1rem" />
|
||||
) : (
|
||||
<Download style={{ width: rem(14), height: rem(14) }} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{isDownloading ? 'Updating...' : `Update to ${newerVersion}`}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isInstalled && onDownload) {
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -99,6 +127,11 @@ export const DownloadCard = ({
|
|||
Recommended
|
||||
</Badge>
|
||||
)}
|
||||
{hasUpdate && (
|
||||
<Badge variant="light" color="orange" size="sm">
|
||||
Update Available
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{description && (
|
||||
<Text size="xs" c="dimmed" mb="xs">
|
||||
|
|
|
|||
192
src/components/UpdateAvailableModal.tsx
Normal file
192
src/components/UpdateAvailableModal.tsx
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import {
|
||||
Modal,
|
||||
Stack,
|
||||
Text,
|
||||
Group,
|
||||
Button,
|
||||
Card,
|
||||
Loader,
|
||||
useMantineColorScheme,
|
||||
Anchor,
|
||||
Progress,
|
||||
} from '@mantine/core';
|
||||
import { Download, X, ExternalLink } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import type { InstalledVersion, DownloadItem } from '@/types/electron';
|
||||
import { getDisplayNameFromPath } from '@/utils/versionUtils';
|
||||
import { GITHUB_API } from '@/constants';
|
||||
|
||||
interface UpdateAvailableModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
currentVersion: InstalledVersion;
|
||||
availableUpdate: DownloadItem;
|
||||
onUpdate: (download: DownloadItem) => Promise<void>;
|
||||
isDownloading?: boolean;
|
||||
downloadProgress?: number;
|
||||
}
|
||||
|
||||
export const UpdateAvailableModal = ({
|
||||
opened,
|
||||
onClose,
|
||||
currentVersion,
|
||||
availableUpdate,
|
||||
onUpdate,
|
||||
isDownloading = false,
|
||||
downloadProgress = 0,
|
||||
}: UpdateAvailableModalProps) => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
setIsUpdating(true);
|
||||
await onUpdate(availableUpdate);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError('Failed to update:', error as Error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
size="sm"
|
||||
title="A newer version is available"
|
||||
centered
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={!isDownloading && !isUpdating}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
p="md"
|
||||
bg={colorScheme === 'dark' ? 'dark.6' : 'gray.0'}
|
||||
style={{
|
||||
borderColor:
|
||||
colorScheme === 'dark'
|
||||
? 'var(--mantine-color-orange-7)'
|
||||
: 'var(--mantine-color-orange-3)',
|
||||
borderWidth: '2px',
|
||||
}}
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Group gap="md" align="center">
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">
|
||||
Current Version
|
||||
</Text>
|
||||
<Text fw={500} size="sm">
|
||||
{currentVersion.version}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text
|
||||
size="lg"
|
||||
c={colorScheme === 'dark' ? 'orange.4' : 'orange.6'}
|
||||
fw={500}
|
||||
>
|
||||
→
|
||||
</Text>
|
||||
|
||||
<div>
|
||||
<Text size="xs" c="dimmed">
|
||||
Available Version
|
||||
</Text>
|
||||
<Text
|
||||
fw={500}
|
||||
size="sm"
|
||||
c={colorScheme === 'dark' ? 'orange.4' : 'orange.6'}
|
||||
>
|
||||
{availableUpdate.version}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Text size="xs" c="dimmed">
|
||||
Binary: {getDisplayNameFromPath(currentVersion)}
|
||||
</Text>
|
||||
|
||||
<Group gap="xs" align="center" mt="xs">
|
||||
<Text size="xs" c="dimmed">
|
||||
View release notes:
|
||||
</Text>
|
||||
<Anchor
|
||||
size="xs"
|
||||
href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate.version}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={() =>
|
||||
window.electronAPI.app.openExternal(
|
||||
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate.version}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<Group gap={4} align="center">
|
||||
<span>v{availableUpdate.version}</span>
|
||||
<ExternalLink size={12} />
|
||||
</Group>
|
||||
</Anchor>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Stack gap="xs" style={{ minHeight: '44px' }}>
|
||||
<Progress
|
||||
value={isDownloading ? Math.min(downloadProgress, 100) : 0}
|
||||
color="orange"
|
||||
radius="xl"
|
||||
style={{
|
||||
opacity: isDownloading || isUpdating ? 1 : 0,
|
||||
transition: 'opacity 200ms ease',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
size="xs"
|
||||
c="dimmed"
|
||||
ta="center"
|
||||
style={{
|
||||
opacity: isDownloading || isUpdating ? 1 : 0,
|
||||
transition: 'opacity 200ms ease',
|
||||
}}
|
||||
>
|
||||
{isDownloading
|
||||
? `${Math.min(downloadProgress, 100).toFixed(1)}% complete`
|
||||
: 'Preparing update...'}
|
||||
</Text>
|
||||
</Stack>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isDownloading || isUpdating}
|
||||
leftSection={<X size={16} />}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleUpdate}
|
||||
loading={isDownloading || isUpdating}
|
||||
disabled={isDownloading || isUpdating}
|
||||
leftSection={
|
||||
isDownloading || isUpdating ? (
|
||||
<Loader size="1rem" />
|
||||
) : (
|
||||
<Download size={16} />
|
||||
)
|
||||
}
|
||||
color="orange"
|
||||
>
|
||||
{isDownloading || isUpdating ? 'Updating...' : 'Update Now'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
@ -52,7 +52,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
|
|||
setDownloadingAsset(type === 'asset' ? download!.name : null);
|
||||
|
||||
try {
|
||||
const success = await sharedHandleDownload(type, download);
|
||||
const success = await sharedHandleDownload(
|
||||
type,
|
||||
download,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if (success) {
|
||||
onDownloadComplete();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,31 @@ import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils';
|
|||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
|
||||
|
||||
const compareVersions = (versionA: string, versionB: string): number => {
|
||||
const cleanVersion = (version: string): string =>
|
||||
version.replace(/^v/, '').replace(/[^0-9.]/g, '');
|
||||
|
||||
const parseVersion = (version: string): number[] =>
|
||||
cleanVersion(version)
|
||||
.split('.')
|
||||
.map((num) => parseInt(num, 10) || 0);
|
||||
|
||||
const a = parseVersion(versionA);
|
||||
const b = parseVersion(versionB);
|
||||
const maxLength = Math.max(a.length, b.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const aVal = a[i] || 0;
|
||||
const bVal = b[i] || 0;
|
||||
|
||||
if (aVal !== bVal) {
|
||||
return aVal - bVal;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
interface VersionInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
|
|
@ -30,6 +55,8 @@ interface VersionInfo {
|
|||
downloadUrl?: string;
|
||||
installedPath?: string;
|
||||
isROCm?: boolean;
|
||||
hasUpdate?: boolean;
|
||||
newerVersion?: string;
|
||||
}
|
||||
|
||||
export const VersionsTab = () => {
|
||||
|
|
@ -68,6 +95,7 @@ export const VersionsTab = () => {
|
|||
|
||||
if (currentBinaryPath && versions.length > 0) {
|
||||
const current = versions.find((v) => v.path === currentBinaryPath);
|
||||
|
||||
if (current) {
|
||||
setCurrentVersion(current);
|
||||
} else {
|
||||
|
|
@ -79,6 +107,7 @@ export const VersionsTab = () => {
|
|||
}
|
||||
} else if (versions.length > 0) {
|
||||
setCurrentVersion(versions[0]);
|
||||
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
versions[0].path
|
||||
|
|
@ -119,13 +148,15 @@ export const VersionsTab = () => {
|
|||
|
||||
const getAllVersions = (): VersionInfo[] => {
|
||||
const versions: VersionInfo[] = [];
|
||||
const processedInstalled = new Set<string>();
|
||||
|
||||
availableDownloads.forEach((download) => {
|
||||
const installedVersion = installedVersions.find((v) => {
|
||||
const displayName = getDisplayNameFromPath(v);
|
||||
const downloadBaseName = download.name
|
||||
.replace(/\.(tar\.gz|zip|exe)$/i, '')
|
||||
.replace(/\.packed$/, '');
|
||||
|
||||
const installedVersion = installedVersions.find((v) => {
|
||||
const displayName = getDisplayNameFromPath(v);
|
||||
return displayName === downloadBaseName;
|
||||
});
|
||||
|
||||
|
|
@ -135,23 +166,43 @@ export const VersionsTab = () => {
|
|||
currentVersion.path === installedVersion.path
|
||||
);
|
||||
|
||||
if (installedVersion) {
|
||||
processedInstalled.add(installedVersion.path);
|
||||
|
||||
const hasUpdate =
|
||||
compareVersions(
|
||||
download.version || 'unknown',
|
||||
installedVersion.version
|
||||
) > 0;
|
||||
|
||||
versions.push({
|
||||
name: download.name,
|
||||
version: installedVersion?.version || download.version || 'unknown',
|
||||
size: installedVersion ? undefined : download.size,
|
||||
isInstalled: Boolean(installedVersion),
|
||||
version: installedVersion.version,
|
||||
size: undefined,
|
||||
isInstalled: true,
|
||||
isCurrent,
|
||||
downloadUrl: download.url,
|
||||
installedPath: installedVersion?.path,
|
||||
installedPath: installedVersion.path,
|
||||
isROCm: download.type === 'rocm',
|
||||
hasUpdate,
|
||||
newerVersion: hasUpdate ? download.version : undefined,
|
||||
});
|
||||
} else {
|
||||
versions.push({
|
||||
name: download.name,
|
||||
version: download.version || 'unknown',
|
||||
size: download.size,
|
||||
isInstalled: false,
|
||||
isCurrent: false,
|
||||
downloadUrl: download.url,
|
||||
isROCm: download.type === 'rocm',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
installedVersions.forEach((installed) => {
|
||||
if (!processedInstalled.has(installed.path)) {
|
||||
const displayName = getDisplayNameFromPath(installed);
|
||||
const existsInRemote = versions.some((v) => v.name === displayName);
|
||||
|
||||
if (!existsInRemote) {
|
||||
const isCurrent = Boolean(
|
||||
currentVersion && currentVersion.path === installed.path
|
||||
);
|
||||
|
|
@ -179,7 +230,12 @@ export const VersionsTab = () => {
|
|||
}
|
||||
|
||||
const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
|
||||
const success = await sharedHandleDownload(downloadType, download);
|
||||
const success = await sharedHandleDownload(
|
||||
downloadType,
|
||||
download,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await loadInstalledVersions();
|
||||
|
|
@ -189,6 +245,30 @@ export const VersionsTab = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (version: VersionInfo) => {
|
||||
try {
|
||||
const download = availableDownloads.find((d) => d.name === version.name);
|
||||
if (!download) {
|
||||
throw new Error('Download not found');
|
||||
}
|
||||
|
||||
const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
|
||||
const wasCurrentBinary = version.isCurrent;
|
||||
const success = await sharedHandleDownload(
|
||||
downloadType,
|
||||
download,
|
||||
true,
|
||||
wasCurrentBinary
|
||||
);
|
||||
|
||||
if (success) {
|
||||
await loadInstalledVersions();
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError('Failed to update:', error as Error);
|
||||
}
|
||||
};
|
||||
|
||||
const makeCurrent = async (version: VersionInfo) => {
|
||||
if (!version.installedPath) return;
|
||||
|
||||
|
|
@ -310,6 +390,8 @@ export const VersionsTab = () => {
|
|||
isDownloading={isDownloading}
|
||||
downloadProgress={downloadProgress[version.name]}
|
||||
disabled={downloading !== null}
|
||||
hasUpdate={version.hasUpdate}
|
||||
newerVersion={version.newerVersion}
|
||||
onDownload={
|
||||
!version.isInstalled
|
||||
? (e) => {
|
||||
|
|
@ -318,8 +400,16 @@ export const VersionsTab = () => {
|
|||
}
|
||||
: undefined
|
||||
}
|
||||
onUpdate={
|
||||
version.hasUpdate
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
handleUpdate(version);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onMakeCurrent={
|
||||
version.isInstalled && !version.isCurrent
|
||||
version.isInstalled && !version.isCurrent && !version.hasUpdate
|
||||
? () => makeCurrent(version)
|
||||
: undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const GITHUB_API = {
|
|||
BASE_URL: 'https://api.github.com',
|
||||
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
|
||||
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
|
||||
FRIENDLY_KOBOLD_REPO: 'lone-cloud/friendly-kobold',
|
||||
get LATEST_RELEASE_URL() {
|
||||
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ interface UseKoboldVersionsReturn {
|
|||
loadRemoteVersions: () => Promise<void>;
|
||||
handleDownload: (
|
||||
type: 'asset' | 'rocm',
|
||||
item?: DownloadItem
|
||||
item?: DownloadItem,
|
||||
isUpdate?: boolean,
|
||||
wasCurrentBinary?: boolean
|
||||
) => Promise<boolean>;
|
||||
setDownloading: (value: string | null) => void;
|
||||
setDownloadProgress: (
|
||||
|
|
@ -113,7 +115,12 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
|
|||
}, [platformInfo.platform]);
|
||||
|
||||
const handleDownload = useCallback(
|
||||
async (type: 'asset' | 'rocm', item?: DownloadItem): Promise<boolean> => {
|
||||
async (
|
||||
type: 'asset' | 'rocm',
|
||||
item?: DownloadItem,
|
||||
isUpdate = false,
|
||||
wasCurrentBinary = false
|
||||
): Promise<boolean> => {
|
||||
if (type === 'asset' && !item) return false;
|
||||
|
||||
const downloadName = item?.name || 'download';
|
||||
|
|
@ -130,6 +137,8 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
|
|||
browser_download_url: item!.url,
|
||||
size: item!.size,
|
||||
created_at: new Date().toISOString(),
|
||||
isUpdate,
|
||||
wasCurrentBinary,
|
||||
});
|
||||
|
||||
return result.success !== false;
|
||||
|
|
|
|||
115
src/hooks/useUpdateChecker.ts
Normal file
115
src/hooks/useUpdateChecker.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { getDisplayNameFromPath } from '@/utils/versionUtils';
|
||||
import type { InstalledVersion, DownloadItem } from '@/types/electron';
|
||||
|
||||
const compareVersions = (versionA: string, versionB: string): number => {
|
||||
const cleanVersion = (version: string): string =>
|
||||
version.replace(/^v/, '').replace(/[^0-9.]/g, '');
|
||||
|
||||
const parseVersion = (version: string): number[] =>
|
||||
cleanVersion(version)
|
||||
.split('.')
|
||||
.map((num) => parseInt(num, 10) || 0);
|
||||
|
||||
const a = parseVersion(versionA);
|
||||
const b = parseVersion(versionB);
|
||||
const maxLength = Math.max(a.length, b.length);
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
const aVal = a[i] || 0;
|
||||
const bVal = b[i] || 0;
|
||||
|
||||
if (aVal !== bVal) {
|
||||
return aVal - bVal;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
interface UpdateInfo {
|
||||
currentVersion: InstalledVersion;
|
||||
availableUpdate: DownloadItem;
|
||||
}
|
||||
|
||||
export const useUpdateChecker = () => {
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
|
||||
const [isChecking, setIsChecking] = useState(false);
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
setIsChecking(true);
|
||||
|
||||
try {
|
||||
const [currentBinaryPath, installedVersions, releases, rocm] =
|
||||
await Promise.all([
|
||||
window.electronAPI.config.get(
|
||||
'currentKoboldBinary'
|
||||
) as Promise<string>,
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
window.electronAPI.kobold.getLatestRelease(),
|
||||
window.electronAPI.kobold.getROCmDownload(),
|
||||
]);
|
||||
|
||||
if (!currentBinaryPath || installedVersions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = installedVersions.find(
|
||||
(v: InstalledVersion) => v.path === currentBinaryPath
|
||||
);
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const availableDownloads: DownloadItem[] = [...releases];
|
||||
if (rocm) {
|
||||
availableDownloads.push(rocm);
|
||||
}
|
||||
|
||||
const currentDisplayName = getDisplayNameFromPath(currentVersion);
|
||||
|
||||
const matchingDownload = availableDownloads.find(
|
||||
(download: DownloadItem) => {
|
||||
const downloadBaseName = download.name
|
||||
.replace(/\.(tar\.gz|zip|exe)$/i, '')
|
||||
.replace(/\.packed$/, '');
|
||||
return downloadBaseName === currentDisplayName;
|
||||
}
|
||||
);
|
||||
|
||||
if (matchingDownload && matchingDownload.version) {
|
||||
const hasUpdate =
|
||||
compareVersions(matchingDownload.version, currentVersion.version) > 0;
|
||||
|
||||
if (hasUpdate) {
|
||||
setUpdateInfo({
|
||||
currentVersion,
|
||||
availableUpdate: matchingDownload,
|
||||
});
|
||||
setShowUpdateModal(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
window.electronAPI.logs.logError(
|
||||
'Failed to check for updates:',
|
||||
error as Error
|
||||
);
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const dismissUpdate = useCallback(() => {
|
||||
setShowUpdateModal(false);
|
||||
setUpdateInfo(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
updateInfo,
|
||||
showUpdateModal,
|
||||
isChecking,
|
||||
checkForUpdates,
|
||||
dismissUpdate,
|
||||
};
|
||||
};
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
writeFileSync,
|
||||
unlinkSync,
|
||||
} from 'fs';
|
||||
import { rm } from 'fs/promises';
|
||||
import { dialog } from 'electron';
|
||||
import { GitHubService } from '@/main/services/GitHubService';
|
||||
import { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
|
|
@ -23,6 +24,8 @@ interface GitHubAsset {
|
|||
browser_download_url: string;
|
||||
size: number;
|
||||
created_at: string;
|
||||
isUpdate?: boolean;
|
||||
wasCurrentBinary?: boolean;
|
||||
}
|
||||
|
||||
interface GitHubRelease {
|
||||
|
|
@ -85,6 +88,17 @@ export class KoboldCppManager {
|
|||
const baseFilename = asset.name.replace(/\.exe$/, '');
|
||||
const unpackedDirPath = join(this.installDir, baseFilename);
|
||||
|
||||
if (asset.isUpdate && existsSync(unpackedDirPath)) {
|
||||
try {
|
||||
await rm(unpackedDirPath, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to remove existing directory for update:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(asset.browser_download_url);
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
@ -150,7 +164,7 @@ export class KoboldCppManager {
|
|||
const launcherPath = this.getLauncherPath(unpackedDirPath);
|
||||
if (launcherPath && existsSync(launcherPath)) {
|
||||
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||
if (!currentBinary) {
|
||||
if (!currentBinary || (asset.isUpdate && asset.wasCurrentBinary)) {
|
||||
this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from 'electron';
|
||||
import * as os from 'os';
|
||||
import { join } from 'path';
|
||||
import { GITHUB_API } from '../../constants';
|
||||
|
||||
export class WindowManager {
|
||||
private mainWindow: BrowserWindow | null = null;
|
||||
|
|
@ -255,7 +256,9 @@ export class WindowManager {
|
|||
{
|
||||
label: 'KoboldCpp Wiki',
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/LostRuins/koboldcpp/wiki');
|
||||
shell.openExternal(
|
||||
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/wiki`
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -327,7 +330,9 @@ OS: ${osInfo}`;
|
|||
});
|
||||
|
||||
ipcMain.once('open-github', () => {
|
||||
shell.openExternal('https://github.com/lone-cloud/friendly-kobold');
|
||||
shell.openExternal(
|
||||
`https://github.com/${GITHUB_API.FRIENDLY_KOBOLD_REPO}`
|
||||
);
|
||||
});
|
||||
|
||||
ipcMain.once('close-about-dialog', () => {
|
||||
|
|
|
|||
2
src/types/electron.d.ts
vendored
2
src/types/electron.d.ts
vendored
|
|
@ -11,6 +11,8 @@ interface GitHubAsset {
|
|||
browser_download_url: string;
|
||||
size: number;
|
||||
created_at: string;
|
||||
isUpdate?: boolean;
|
||||
wasCurrentBinary?: boolean;
|
||||
}
|
||||
|
||||
export interface GitHubRelease {
|
||||
|
|
|
|||
48
yarn.lock
48
yarn.lock
|
|
@ -40,7 +40,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/core@npm:^7.27.7, @babel/core@npm:^7.28.0":
|
||||
"@babel/core@npm:^7.27.7, @babel/core@npm:^7.28.3":
|
||||
version: 7.28.3
|
||||
resolution: "@babel/core@npm:7.28.3"
|
||||
dependencies:
|
||||
|
|
@ -1661,10 +1661,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rolldown/pluginutils@npm:1.0.0-beta.30":
|
||||
version: 1.0.0-beta.30
|
||||
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.30"
|
||||
checksum: 10c0/aff8b532cb9d82d94c9a4101fa12ecb10620ad47d52dbb9135a5c65bde1ad19895b41026b821f4d607083699239a5d0010198401b6a6a54ab6a10d0015302768
|
||||
"@rolldown/pluginutils@npm:1.0.0-beta.32":
|
||||
version: 1.0.0-beta.32
|
||||
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.32"
|
||||
checksum: 10c0/ba3582fc3c35c8eb57b0df2d22d0733b1be83d37edcc258203364773f094f58fc0cb7a056d604603573a69dd0105a466506cad467f59074e1e53d0dc26191f06
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -2191,19 +2191,19 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "@vitejs/plugin-react@npm:5.0.0"
|
||||
"@vitejs/plugin-react@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "@vitejs/plugin-react@npm:5.0.1"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.28.0"
|
||||
"@babel/core": "npm:^7.28.3"
|
||||
"@babel/plugin-transform-react-jsx-self": "npm:^7.27.1"
|
||||
"@babel/plugin-transform-react-jsx-source": "npm:^7.27.1"
|
||||
"@rolldown/pluginutils": "npm:1.0.0-beta.30"
|
||||
"@rolldown/pluginutils": "npm:1.0.0-beta.32"
|
||||
"@types/babel__core": "npm:^7.20.5"
|
||||
react-refresh: "npm:^0.17.0"
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
checksum: 10c0/e5813839d319ab5dc1b90cab40b6c08388f26e456166ba9df10ffc3c3f4ecc594cec06715b5c93390bba56140ca5f68a18f2233f7d275d77e5bbfeb979e4fd9b
|
||||
checksum: 10c0/2641171beedfc38edc5671abb47706906f9af2a79a6dfff4e946106c9550de4f83ccae41c164f3ee26a3edf07127ecc0e415fe5cddbf7abc71fbb2540016c27d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -3615,16 +3615,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron@npm:^37.3.0":
|
||||
version: 37.3.0
|
||||
resolution: "electron@npm:37.3.0"
|
||||
"electron@npm:^37.3.1":
|
||||
version: 37.3.1
|
||||
resolution: "electron@npm:37.3.1"
|
||||
dependencies:
|
||||
"@electron/get": "npm:^2.0.0"
|
||||
"@types/node": "npm:^22.7.7"
|
||||
extract-zip: "npm:^2.0.1"
|
||||
bin:
|
||||
electron: cli.js
|
||||
checksum: 10c0/6f08ca2955c65591368cb0dadbd637d86749040c8d4cd616d7265e1c71ece854f23c678b7cfc23746100327dcdb34f4815504745e97ca3ac2f40c72b951e1044
|
||||
checksum: 10c0/1b171804646f229768c29413629b70b968c4c06686913b69e8615da09c58d63a95817021c31ed3c3bdbba0ad360ad4469773b9dda2662884d7df2866559f95f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
@ -4304,7 +4304,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fdir@npm:^6.4.4, fdir@npm:^6.4.6":
|
||||
"fdir@npm:^6.4.4, fdir@npm:^6.5.0":
|
||||
version: 6.5.0
|
||||
resolution: "fdir@npm:6.5.0"
|
||||
peerDependencies:
|
||||
|
|
@ -4433,10 +4433,10 @@ __metadata:
|
|||
"@types/react-dom": "npm:^19.1.7"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^8.40.0"
|
||||
"@typescript-eslint/parser": "npm:^8.40.0"
|
||||
"@vitejs/plugin-react": "npm:^5.0.0"
|
||||
"@vitejs/plugin-react": "npm:^5.0.1"
|
||||
cross-env: "npm:^10.0.0"
|
||||
cspell: "npm:^9.2.0"
|
||||
electron: "npm:^37.3.0"
|
||||
electron: "npm:^37.3.1"
|
||||
electron-builder: "npm:^26.0.12"
|
||||
electron-vite: "npm:^4.0.0"
|
||||
eslint: "npm:^9.33.0"
|
||||
|
|
@ -4457,7 +4457,7 @@ __metadata:
|
|||
rollup-plugin-visualizer: "npm:^6.0.3"
|
||||
systeminformation: "npm:^5.27.7"
|
||||
typescript: "npm:^5.9.2"
|
||||
vite: "npm:^7.1.2"
|
||||
vite: "npm:^7.1.3"
|
||||
wait-on: "npm:^8.0.4"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
|
@ -8180,12 +8180,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^7.1.2":
|
||||
version: 7.1.2
|
||||
resolution: "vite@npm:7.1.2"
|
||||
"vite@npm:^7.1.3":
|
||||
version: 7.1.3
|
||||
resolution: "vite@npm:7.1.3"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.25.0"
|
||||
fdir: "npm:^6.4.6"
|
||||
fdir: "npm:^6.5.0"
|
||||
fsevents: "npm:~2.3.3"
|
||||
picomatch: "npm:^4.0.3"
|
||||
postcss: "npm:^8.5.6"
|
||||
|
|
@ -8231,7 +8231,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 10c0/4ed825b20bc0f49db99cd382de9506b2721ccd47dcebd4a68e0ef65e3cdd2347fded52b306c34178308e0fd7fe78fd5ff517623002cb00710182ad3012c92ced
|
||||
checksum: 10c0/a0aa418beab80673dc9a3e9d1fa49472955d6ef9d41a4c9c6bd402953f411346f612864dae267adfb2bb8ceeb894482369316ffae5816c84fd45990e352b727d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue