gerbil/src/components/settings/VersionsTab.tsx

317 lines
8.8 KiB
TypeScript

import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import {
Stack,
Text,
Group,
Card,
Loader,
Center,
Anchor,
} from '@mantine/core';
import { ExternalLink } from 'lucide-react';
import { DownloadCard } from '@/components/DownloadCard';
import { getAssetDescription } from '@/utils/assets';
import {
getDisplayNameFromPath,
stripAssetExtensions,
compareVersions,
} from '@/utils/version';
import { safeExecute } from '@/utils/logger';
import { formatDownloadSize } from '@/utils/format';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
interface VersionInfo {
name: string;
version: string;
size?: number;
isInstalled: boolean;
isCurrent: boolean;
downloadUrl?: string;
installedPath?: string;
hasUpdate?: boolean;
newerVersion?: string;
}
export const VersionsTab = () => {
const {
availableDownloads,
loadingPlatform,
loadingRemote,
downloading,
downloadProgress,
handleDownload: sharedHandleDownload,
getLatestReleaseWithDownloadStatus,
} = useKoboldVersions();
const [installedVersions, setInstalledVersions] = useState<
InstalledVersion[]
>([]);
const [currentVersion, setCurrentVersion] = useState<InstalledVersion | null>(
null
);
const [loadingInstalled, setLoadingInstalled] = useState(true);
const [latestRelease, setLatestRelease] = useState<ReleaseWithStatus | null>(
null
);
const downloadingItemRef = useRef<HTMLDivElement>(null);
const loadInstalledVersions = useCallback(async () => {
setLoadingInstalled(true);
await safeExecute(async () => {
const [versions, currentVersion] = await Promise.all([
window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.kobold.getCurrentVersion(),
]);
setInstalledVersions(versions);
setCurrentVersion(currentVersion);
}, 'Failed to load installed versions:');
setLoadingInstalled(false);
}, []);
const loadLatestRelease = useCallback(async () => {
const release = await safeExecute(
() => getLatestReleaseWithDownloadStatus(),
'Failed to load latest release:'
);
if (release) {
setLatestRelease(release);
}
}, [getLatestReleaseWithDownloadStatus]);
useEffect(() => {
loadInstalledVersions();
loadLatestRelease();
}, [loadInstalledVersions, loadLatestRelease]);
const allVersions = useMemo((): VersionInfo[] => {
const versions: VersionInfo[] = [];
const processedInstalled = new Set<string>();
availableDownloads.forEach((download) => {
const downloadBaseName = stripAssetExtensions(download.name);
const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v);
return displayName === downloadBaseName;
});
const isCurrent = Boolean(
installedVersion &&
currentVersion &&
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,
size: undefined,
isInstalled: true,
isCurrent,
downloadUrl: download.url,
installedPath: installedVersion.path,
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,
});
}
});
installedVersions.forEach((installed) => {
if (!processedInstalled.has(installed.path)) {
const displayName = getDisplayNameFromPath(installed);
const isCurrent = Boolean(
currentVersion && currentVersion.path === installed.path
);
versions.push({
name: displayName,
version: installed.version,
size: undefined,
isInstalled: true,
isCurrent,
installedPath: installed.path,
});
}
});
return versions.sort((a, b) => {
if (a.isInstalled && !b.isInstalled) return -1;
if (!a.isInstalled && b.isInstalled) return 1;
return 0;
});
}, [availableDownloads, installedVersions, currentVersion]);
useEffect(() => {
if (downloading && downloadingItemRef.current) {
downloadingItemRef.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
}, [downloading]);
const handleDownload = async (version: VersionInfo) => {
await safeExecute(async () => {
const download = availableDownloads.find((d) => d.name === version.name);
if (!download) {
throw new Error('Download not found');
}
const success = await sharedHandleDownload({
item: download,
isUpdate: false,
wasCurrentBinary: false,
});
if (success) {
await loadInstalledVersions();
}
}, 'Failed to download:');
};
const handleUpdate = async (version: VersionInfo) => {
await safeExecute(async () => {
const download = availableDownloads.find((d) => d.name === version.name);
if (!download) {
throw new Error('Download not found');
}
const success = await sharedHandleDownload({
item: download,
isUpdate: true,
wasCurrentBinary: version.isCurrent,
});
if (success) {
await loadInstalledVersions();
}
}, 'Failed to update:');
};
const makeCurrent = async (version: VersionInfo) => {
if (!version.installedPath) return;
await safeExecute(async () => {
const success = await window.electronAPI.kobold.setCurrentVersion(
version.installedPath!
);
if (success) {
await loadInstalledVersions();
}
}, 'Failed to set current version:');
};
if (loadingInstalled || loadingPlatform || loadingRemote) {
return (
<Center h="100%">
<Stack align="center" gap="md">
<Loader size="lg" />
<Text c="dimmed">
{loadingInstalled && (loadingPlatform || loadingRemote)
? 'Loading versions...'
: loadingInstalled
? 'Scanning installed versions...'
: 'Checking for updates...'}
</Text>
</Stack>
</Center>
);
}
return (
<>
<Group justify="space-between" align="center" mb="sm">
{latestRelease && (
<Group gap="xs">
<Text size="sm" c="dimmed">
Latest release: {latestRelease.release.tag_name}
</Text>
<Anchor
href={latestRelease.release.html_url}
target="_blank"
rel="noopener noreferrer"
size="sm"
c="blue"
>
<Group gap={4} align="center">
<span>Release notes</span>
<ExternalLink size={12} />
</Group>
</Anchor>
</Group>
)}
</Group>
{allVersions.map((version, index) => {
const isDownloading = downloading === version.name;
return (
<div
key={`${version.name}-${version.version}-${index}`}
style={{ paddingBottom: '0.5rem' }}
ref={isDownloading ? downloadingItemRef : null}
>
<DownloadCard
name={version.name}
size={
version.size
? formatDownloadSize(version.size, version.downloadUrl)
: ''
}
version={version.version}
description={getAssetDescription(version.name)}
isCurrent={version.isCurrent}
isInstalled={version.isInstalled}
isDownloading={isDownloading}
downloadProgress={downloadProgress[version.name]}
disabled={downloading !== null}
hasUpdate={version.hasUpdate}
newerVersion={version.newerVersion}
onDownload={(e) => {
e.stopPropagation();
handleDownload(version);
}}
onUpdate={(e) => {
e.stopPropagation();
handleUpdate(version);
}}
onMakeCurrent={() => makeCurrent(version)}
/>
</div>
);
})}
{allVersions.length === 0 && (
<Card withBorder radius="md" padding="md">
<Text size="sm" c="dimmed" ta="center">
No versions found
</Text>
</Card>
)}
</>
);
};