Chronicler #85 direct fix (Code unavailable on mobile): - ModpackAPIController: add pending_calibration flag to serverStatus response - wrapper.tsx: add pending_calibration to StatusData interface - wrapper.tsx: render Identify Version button when pending_calibration is true Replaces false green checkmark for servers needing calibration.
308 lines
13 KiB
TypeScript
308 lines
13 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;
|
|
pending_calibration?: 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 [error, setError] = useState<string | null>(null);
|
|
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); setError(null); })
|
|
.catch(() => setError('Unable to load modpack status.'))
|
|
.finally(() => setLoading(false));
|
|
}, [uuid]);
|
|
|
|
// Manual refresh
|
|
const refresh = async () => {
|
|
if (!uuid || checking) return;
|
|
setChecking(true);
|
|
setError(null);
|
|
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 {
|
|
setError('Check failed. Try again.');
|
|
}
|
|
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;
|
|
|
|
// Muted card for ignored servers (with Resume button)
|
|
if (data?.is_ignored) {
|
|
return (
|
|
<div className={classNames(
|
|
'rounded shadow-lg bg-gray-600 opacity-50',
|
|
'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={'flex items-center justify-between'}>
|
|
<div className={'flex items-center'}>
|
|
<FontAwesomeIcon icon={faCube} className={'w-4 h-4 mr-2 text-gray-400'} />
|
|
<span className={'text-gray-400 text-sm'}>
|
|
{data.modpack_name || 'Modpack'} — Updates ignored
|
|
</span>
|
|
</div>
|
|
<button onClick={toggleIgnore}
|
|
className={'text-xs px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-200 rounded transition-colors'}>
|
|
Resume
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error && !data) return (
|
|
<div className={classNames(
|
|
'rounded shadow-lg bg-gray-600 text-gray-400 text-xs',
|
|
'col-span-3 md:col-span-2 lg:col-span-6',
|
|
'px-3 py-2 mt-2'
|
|
)}>
|
|
<FontAwesomeIcon icon={faCube} className={'w-3 h-3 mr-2'} />{error}
|
|
</div>
|
|
);
|
|
|
|
// Pending calibration — version unknown, show Identify Version button
|
|
if (data?.pending_calibration) {
|
|
return (
|
|
<div className={classNames(
|
|
'rounded shadow-lg 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={'flex items-center justify-between'}>
|
|
<div className={'flex items-center'}>
|
|
<FontAwesomeIcon icon={faCube} className={'w-4 h-4 mr-2 text-gray-400'} />
|
|
<span className={'text-gray-400 text-sm'}>
|
|
{data.modpack_name || 'Modpack'} — Version unknown
|
|
</span>
|
|
</div>
|
|
<button onClick={openCalibrate}
|
|
className={'text-xs px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded transition-colors'}>
|
|
Identify Version
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Calibrate dropdown (reusable)
|
|
const renderCalibrateDropdown = () => (
|
|
<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>
|
|
);
|
|
|
|
// Pending calibration — show "Identify Version" prompt
|
|
if (data && !data.configured && data.modpack_name) {
|
|
return (
|
|
<div className={classNames(
|
|
'rounded shadow-lg 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={'flex items-center justify-between'}>
|
|
<div className={'flex items-center'}>
|
|
<FontAwesomeIcon icon={faCube} className={'w-4 h-4 mr-2 text-gray-400'} />
|
|
<span className={'text-gray-400 text-sm'}>
|
|
{data.modpack_name} — Version unknown
|
|
</span>
|
|
</div>
|
|
<div className={'flex gap-1'}>
|
|
<button onClick={openCalibrate}
|
|
className={'text-xs px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded transition-colors'}>
|
|
Identify Version
|
|
</button>
|
|
<button onClick={toggleIgnore}
|
|
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Hide'}>
|
|
<FontAwesomeIcon icon={faEyeSlash} className={'w-3 h-3'} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{showCalibrate && renderCalibrateDropdown()}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const hasUpdate = data.update_available;
|
|
const configured = data.configured;
|
|
|
|
// Short name: "All the Mods 9 - ATM9" → "ATM9"
|
|
const shortName = (data.modpack_name?.includes(' - ')
|
|
? data.modpack_name.split(' - ').pop()
|
|
: data.modpack_name) || 'Modpack';
|
|
|
|
// Extract semver from version strings (e.g. "All the Mods 9-0.1.0" → "0.1.0")
|
|
const extractVersion = (v?: string) => {
|
|
if (!v) return '?';
|
|
const match = v.match(/[\d]+\.[\d]+[\d.]*$/);
|
|
return match ? match[0] : v;
|
|
};
|
|
|
|
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'}>✓ {shortName} — {extractVersion(data.latest_version)}</span>
|
|
)}
|
|
{configured && hasUpdate && (
|
|
<span className={'text-orange-300'}>
|
|
{shortName} {extractVersion(data.current_version)} → {extractVersion(data.latest_version)} ↑
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Calibrate dropdown */}
|
|
{showCalibrate && renderCalibrateDropdown()}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ModpackVersionCard;
|