Widget redesign: - Zero-click: loads cached status on mount via GET /status (no API calls) - Shows platform icon + modpack name + version comparison - Orange background + arrow (current → latest) when update available - Cyan + checkmark when up to date - Refresh button triggers manual check - Calibrate button opens dropdown with last 10 releases - Ignore button hides non-modpack servers - Current release highlighted in calibrate dropdown Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
201 lines
8.3 KiB
TypeScript
201 lines
8.3 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { ServerContext } from '@/state/server';
|
|
import http from '@/api/http';
|
|
import { faCube, faSync, faEyeSlash, faCog } from '@fortawesome/free-solid-svg-icons';
|
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
import classNames from 'classnames';
|
|
|
|
interface StatusData {
|
|
configured: boolean;
|
|
platform?: string;
|
|
modpack_name?: string;
|
|
current_version?: string;
|
|
latest_version?: string;
|
|
update_available?: boolean;
|
|
last_checked?: string;
|
|
detection_method?: string;
|
|
is_ignored?: boolean;
|
|
}
|
|
|
|
interface Release {
|
|
file_id: string;
|
|
version: string;
|
|
display_name: string;
|
|
release_date?: string;
|
|
}
|
|
|
|
const platformIcons: Record<string, string> = {
|
|
curseforge: '🔥',
|
|
modrinth: '🍃',
|
|
technic: '⚙️',
|
|
ftb: '📦',
|
|
};
|
|
|
|
const ModpackVersionCard: React.FC = () => {
|
|
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
|
|
const [data, setData] = useState<StatusData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [checking, setChecking] = useState(false);
|
|
const [showCalibrate, setShowCalibrate] = useState(false);
|
|
const [releases, setReleases] = useState<Release[]>([]);
|
|
const [loadingReleases, setLoadingReleases] = useState(false);
|
|
|
|
// Zero-click: load cached status on mount
|
|
useEffect(() => {
|
|
if (!uuid) return;
|
|
http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`)
|
|
.then((res) => setData(res.data))
|
|
.catch(() => setData(null))
|
|
.finally(() => setLoading(false));
|
|
}, [uuid]);
|
|
|
|
// Manual refresh
|
|
const refresh = async () => {
|
|
if (!uuid || checking) return;
|
|
setChecking(true);
|
|
try {
|
|
await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/check`);
|
|
const res = await http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`);
|
|
setData(res.data);
|
|
} catch {}
|
|
setChecking(false);
|
|
};
|
|
|
|
// Open calibrate dropdown
|
|
const openCalibrate = async () => {
|
|
if (!uuid) return;
|
|
setShowCalibrate(true);
|
|
setLoadingReleases(true);
|
|
try {
|
|
const res = await http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/releases`);
|
|
setReleases(res.data.releases || []);
|
|
} catch {
|
|
setReleases([]);
|
|
}
|
|
setLoadingReleases(false);
|
|
};
|
|
|
|
// Select a release to calibrate
|
|
const selectRelease = async (release: Release) => {
|
|
if (!uuid) return;
|
|
try {
|
|
const res = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/calibrate`, {
|
|
file_id: release.file_id,
|
|
version: release.version,
|
|
});
|
|
setData((prev) => prev ? {
|
|
...prev,
|
|
current_version: release.version,
|
|
update_available: res.data.update_available,
|
|
} : prev);
|
|
} catch {}
|
|
setShowCalibrate(false);
|
|
};
|
|
|
|
// Ignore server
|
|
const toggleIgnore = async () => {
|
|
if (!uuid) return;
|
|
try {
|
|
const res = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/ignore`);
|
|
setData((prev) => prev ? { ...prev, is_ignored: res.data.is_ignored } : prev);
|
|
} catch {}
|
|
};
|
|
|
|
if (loading) return null;
|
|
if (!data) return null;
|
|
if (data.is_ignored) return null;
|
|
|
|
const hasUpdate = data.update_available;
|
|
const configured = data.configured;
|
|
|
|
const bgColor = hasUpdate ? 'bg-orange-500' : configured ? 'bg-cyan-500' : 'bg-gray-700';
|
|
const icon = data.platform ? (platformIcons[data.platform] || '📦') : '📦';
|
|
|
|
return (
|
|
<div className={classNames(
|
|
'rounded shadow-lg relative bg-gray-600',
|
|
'col-span-3 md:col-span-2 lg:col-span-6',
|
|
'px-3 py-2 md:p-3 lg:p-4 mt-2'
|
|
)}>
|
|
<div className={classNames('w-1 h-full absolute left-0 top-0 rounded-l sm:hidden', bgColor)} />
|
|
|
|
<div className={'flex items-center'}>
|
|
<div className={classNames(
|
|
'hidden flex-shrink-0 items-center justify-center rounded-lg shadow-md w-12 h-12',
|
|
'sm:flex sm:mr-4', bgColor
|
|
)}>
|
|
<FontAwesomeIcon icon={faCube} className={'w-6 h-6 text-gray-50'} />
|
|
</div>
|
|
|
|
<div className={'flex flex-col justify-center overflow-hidden w-full'}>
|
|
<div className={'flex items-center justify-between'}>
|
|
<p className={'font-header font-medium leading-tight text-xs md:text-sm text-gray-200'}>
|
|
{configured ? `${icon} ${data.modpack_name || 'Modpack'}` : 'Modpack Version'}
|
|
</p>
|
|
<div className={'flex gap-1'}>
|
|
<button onClick={refresh} disabled={checking}
|
|
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Refresh'}>
|
|
<FontAwesomeIcon icon={faSync} spin={checking} className={'w-3 h-3'} />
|
|
</button>
|
|
{configured && (
|
|
<button onClick={openCalibrate}
|
|
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Calibrate version'}>
|
|
<FontAwesomeIcon icon={faCog} className={'w-3 h-3'} />
|
|
</button>
|
|
)}
|
|
<button onClick={toggleIgnore}
|
|
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Hide (not a modpack)'}>
|
|
<FontAwesomeIcon icon={faEyeSlash} className={'w-3 h-3'} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={'font-semibold text-gray-50 truncate text-sm'}>
|
|
{!configured && <span className={'text-gray-400'}>Not detected — waiting for cron</span>}
|
|
{configured && !hasUpdate && (
|
|
<span className={'text-cyan-300'}>✓ Up to date — {data.latest_version}</span>
|
|
)}
|
|
{configured && hasUpdate && (
|
|
<span className={'text-orange-300'}>
|
|
↑ {data.current_version} → {data.latest_version}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Calibrate dropdown */}
|
|
{showCalibrate && (
|
|
<div className={'mt-2 bg-gray-800 rounded p-2 max-h-48 overflow-y-auto'}>
|
|
<p className={'text-xs text-gray-400 mb-1'}>Select your installed version:</p>
|
|
{loadingReleases && <p className={'text-xs text-gray-500'}>Loading releases...</p>}
|
|
{!loadingReleases && releases.length === 0 && (
|
|
<p className={'text-xs text-gray-500'}>No releases found</p>
|
|
)}
|
|
{releases.map((r) => (
|
|
<button key={r.file_id} onClick={() => selectRelease(r)}
|
|
className={classNames(
|
|
'block w-full text-left px-2 py-1 text-sm rounded',
|
|
'hover:bg-gray-700 text-gray-200',
|
|
data.current_version === r.version ? 'bg-gray-700 text-cyan-300' : ''
|
|
)}>
|
|
{r.display_name}
|
|
{r.release_date && (
|
|
<span className={'text-gray-500 text-xs ml-2'}>
|
|
{new Date(r.release_date).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
<button onClick={() => setShowCalibrate(false)}
|
|
className={'mt-1 text-xs text-gray-500 hover:text-gray-300'}>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ModpackVersionCard;
|