Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -7,8 +7,22 @@
|
||||
import Button from './primitives/Button.svelte';
|
||||
import Badge from './primitives/Badge.svelte';
|
||||
|
||||
let { pluginId, targetPath, disabled = false, busy = $bindable(false), onimport }: {
|
||||
pluginId?: number; targetPath?: string; disabled?: boolean; busy?: boolean; onimport?: (result: { pluginId: number; betaUrl: string; alreadyImported: boolean }) => void | Promise<void>;
|
||||
let {
|
||||
pluginId,
|
||||
targetPath,
|
||||
disabled = false,
|
||||
busy = $bindable(false),
|
||||
onimport
|
||||
}: {
|
||||
pluginId?: number;
|
||||
targetPath?: string;
|
||||
disabled?: boolean;
|
||||
busy?: boolean;
|
||||
onimport?: (result: {
|
||||
pluginId: number;
|
||||
betaUrl: string;
|
||||
alreadyImported: boolean;
|
||||
}) => void | Promise<void>;
|
||||
} = $props();
|
||||
let url = $state('');
|
||||
let name = $state('');
|
||||
@@ -18,55 +32,89 @@
|
||||
let update = $state<PluginUpdate | null>(null);
|
||||
let checking = $state(false);
|
||||
let error = $state('');
|
||||
let success = $state<{ pluginId: number; betaUrl: string; sha256: string; alreadyImported: boolean } | null>(null);
|
||||
let success = $state<{
|
||||
pluginId: number;
|
||||
betaUrl: string;
|
||||
sha256: string;
|
||||
alreadyImported: boolean;
|
||||
} | null>(null);
|
||||
let active = true;
|
||||
onDestroy(() => { active = false; });
|
||||
onDestroy(() => {
|
||||
active = false;
|
||||
});
|
||||
|
||||
function showRelease(value: GithubPluginRelease) {
|
||||
release = value;
|
||||
const preferred = update?.source?.asset_name ?? targetPath?.split('/').pop();
|
||||
assetId = String(value.assets.find(a => a.name === preferred)?.id ?? (value.assets.length === 1 ? value.assets[0].id : ''));
|
||||
assetId = String(
|
||||
value.assets.find((a) => a.name === preferred)?.id ??
|
||||
(value.assets.length === 1 ? value.assets[0].id : '')
|
||||
);
|
||||
if (!pluginId && !name) name = value.repository.split('/')[1];
|
||||
url = value.url;
|
||||
error = ''; success = null;
|
||||
error = '';
|
||||
success = null;
|
||||
}
|
||||
|
||||
async function checkUpdates() {
|
||||
if (!pluginId || checking) return;
|
||||
checking = true; error = '';
|
||||
checking = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await fetch(`/api/plugins/updates?pluginId=${pluginId}`);
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not check for updates.');
|
||||
update = body.updates[0];
|
||||
if (!url && update?.source) url = `https://github.com/${update.source.repository}`;
|
||||
} catch (e) { error = (e as Error).message; }
|
||||
finally { checking = false; }
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
checking = false;
|
||||
}
|
||||
}
|
||||
onMount(() => { void checkUpdates(); });
|
||||
onMount(() => {
|
||||
void checkUpdates();
|
||||
});
|
||||
|
||||
async function preview(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
if (busy || disabled) return;
|
||||
busy = true; error = ''; release = null; success = null;
|
||||
busy = true;
|
||||
error = '';
|
||||
release = null;
|
||||
success = null;
|
||||
try {
|
||||
const response = await fetch(`/api/plugins/github/release?url=${encodeURIComponent(url.trim())}`);
|
||||
const response = await fetch(
|
||||
`/api/plugins/github/release?url=${encodeURIComponent(url.trim())}`
|
||||
);
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Could not load release.');
|
||||
showRelease(body.release);
|
||||
} catch (e) { error = (e as Error).message; }
|
||||
finally { busy = false; }
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function importRelease(startBeta: boolean) {
|
||||
if (!release || !assetId || busy || disabled) return;
|
||||
const startPath = window.location.pathname;
|
||||
busy = true; error = ''; success = null;
|
||||
busy = true;
|
||||
error = '';
|
||||
success = null;
|
||||
try {
|
||||
const response = await fetch('/api/plugins/github/import', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: release.url, assetId: Number(assetId), pluginId,
|
||||
name: name.trim(), scopeType: game ? 'game' : null, scopeValue: game || null })
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: release.url,
|
||||
assetId: Number(assetId),
|
||||
pluginId,
|
||||
name: name.trim(),
|
||||
scopeType: game ? 'game' : null,
|
||||
scopeValue: game || null
|
||||
})
|
||||
});
|
||||
const body = await response.json();
|
||||
if (!response.ok) throw new Error(body.error ?? 'Import failed.');
|
||||
@@ -75,29 +123,57 @@
|
||||
await onimport?.(body);
|
||||
if (startBeta && window.location.pathname === startPath) await goto(body.betaUrl);
|
||||
else if (pluginId) await checkUpdates();
|
||||
} catch (e) { error = (e as Error).message; }
|
||||
finally { busy = false; }
|
||||
} catch (e) {
|
||||
error = (e as Error).message;
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card padding="lg">
|
||||
<section class="github" aria-label="GitHub plugin releases">
|
||||
<div class="heading">
|
||||
<div><h2>{pluginId ? 'GitHub releases' : 'Import from GitHub'}</h2><p>Choose a release and DLL, then test it with your fleet.</p></div>
|
||||
{#if pluginId}<Button size="sm" onclick={checkUpdates} loading={checking} disabled={busy}>Check for updates</Button>{/if}
|
||||
<div>
|
||||
<h2>{pluginId ? 'GitHub releases' : 'Import from GitHub'}</h2>
|
||||
<p>Choose a release and DLL, then test it with your fleet.</p>
|
||||
</div>
|
||||
{#if pluginId}<Button
|
||||
size="sm"
|
||||
onclick={checkUpdates}
|
||||
loading={checking}
|
||||
disabled={busy}>Check for updates</Button
|
||||
>{/if}
|
||||
</div>
|
||||
{#if checking}<p role="status">Checking GitHub…</p>{/if}
|
||||
{#if update?.source}
|
||||
<p>Following <a href={`https://github.com/${update.source.repository}`} target="_blank" rel="noreferrer">{update.source.repository}</a> · {update.source.asset_name}</p>
|
||||
<p>
|
||||
Following <a
|
||||
href={`https://github.com/${update.source.repository}`}
|
||||
target="_blank"
|
||||
rel="noreferrer">{update.source.repository}</a
|
||||
>
|
||||
· {update.source.asset_name}
|
||||
</p>
|
||||
{/if}
|
||||
{#if update?.status === 'available' && update.release}
|
||||
<div class="update">
|
||||
<Badge variant="info">New release: {update.release.tag}</Badge>
|
||||
<Button size="sm" disabled={busy || disabled} onclick={() => showRelease(update!.release!)}>Review update</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={busy || disabled}
|
||||
onclick={() => showRelease(update!.release!)}>Review update</Button
|
||||
>
|
||||
</div>
|
||||
{:else if update?.status === 'imported' && update.release}
|
||||
<div class="update"><span>{update.release.tag} is already imported and available for testing.</span>
|
||||
<Button size="sm" href={`/betas?pluginId=${pluginId}&versionId=${update.versionId}`} disabled={disabled}>Start beta</Button></div>
|
||||
<div class="update">
|
||||
<span>{update.release.tag} is already imported and available for testing.</span>
|
||||
<Button
|
||||
size="sm"
|
||||
href={`/betas?pluginId=${pluginId}&versionId=${update.versionId}`}
|
||||
{disabled}>Start beta</Button
|
||||
>
|
||||
</div>
|
||||
{:else if update?.status === 'current'}
|
||||
<p role="status">The latest stable release is promoted.</p>
|
||||
{:else if update?.status === 'ahead'}
|
||||
@@ -108,44 +184,105 @@
|
||||
<p>Import a release below to track updates for this plugin.</p>
|
||||
{/if}
|
||||
<form onsubmit={preview} class="lookup">
|
||||
<label>Repository or release URL
|
||||
<input type="url" bind:value={url} oninput={() => { release = null; success = null; }} required
|
||||
placeholder="https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.0" disabled={busy || disabled} />
|
||||
<label
|
||||
>Repository or release URL
|
||||
<input
|
||||
type="url"
|
||||
bind:value={url}
|
||||
oninput={() => {
|
||||
release = null;
|
||||
success = null;
|
||||
}}
|
||||
required
|
||||
placeholder="https://github.com/alicealys/t6-gsc-utils/releases/tag/v3.1.0"
|
||||
disabled={busy || disabled}
|
||||
/>
|
||||
</label>
|
||||
<Button type="submit" loading={busy && !release} disabled={busy || disabled || !url.trim()}>Preview release</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={busy && !release}
|
||||
disabled={busy || disabled || !url.trim()}>Preview release</Button
|
||||
>
|
||||
</form>
|
||||
{#if disabled}<p>Finish the running beta before importing a release.</p>{/if}
|
||||
{#if release}
|
||||
<div class="preview">
|
||||
<div class="heading"><h3>{release.name || release.tag}</h3><Badge variant={release.prerelease ? 'warning' : 'neutral'}>{release.prerelease ? 'Prerelease' : 'Release'} · {release.tag}</Badge></div>
|
||||
<p><a href={release.url} target="_blank" rel="noreferrer">View on GitHub ↗</a> · Published {$timeFormat.date(release.publishedAt)}</p>
|
||||
<details open><summary>Release notes</summary><pre>{release.notes || 'No release notes provided.'}</pre></details>
|
||||
<div class="heading">
|
||||
<h3>{release.name || release.tag}</h3>
|
||||
<Badge variant={release.prerelease ? 'warning' : 'neutral'}
|
||||
>{release.prerelease ? 'Prerelease' : 'Release'} · {release.tag}</Badge
|
||||
>
|
||||
</div>
|
||||
<p>
|
||||
<a href={release.url} target="_blank" rel="noreferrer">View on GitHub ↗</a> ·
|
||||
Published {$timeFormat.date(release.publishedAt)}
|
||||
</p>
|
||||
<details open>
|
||||
<summary>Release notes</summary>
|
||||
<pre>{release.notes || 'No release notes provided.'}</pre>
|
||||
</details>
|
||||
{#if release.assets.length === 0}
|
||||
<p>No importable DLL assets found. Choose another release or upload a DLL manually. Direct DLL assets up to 64 MB are supported.</p>
|
||||
<p>
|
||||
No importable DLL assets found. Choose another release or upload a DLL
|
||||
manually. Direct DLL assets up to 64 MB are supported.
|
||||
</p>
|
||||
{:else}
|
||||
<div class="fields">
|
||||
<label>DLL asset<select bind:value={assetId} disabled={busy || disabled}>
|
||||
<option value="" disabled>Choose a DLL…</option>
|
||||
{#each release.assets as asset}<option value={String(asset.id)}>{asset.name} · {(asset.size / 1024).toFixed(1)} KB</option>{/each}
|
||||
</select></label>
|
||||
<label
|
||||
>DLL asset<select bind:value={assetId} disabled={busy || disabled}>
|
||||
<option value="" disabled>Choose a DLL…</option>
|
||||
{#each release.assets as asset}<option value={String(asset.id)}
|
||||
>{asset.name} · {(asset.size / 1024).toFixed(1)} KB</option
|
||||
>{/each}
|
||||
</select></label
|
||||
>
|
||||
{#if !pluginId}
|
||||
<label>Plugin name<input bind:value={name} disabled={busy} placeholder="t6-gsc-utils" /></label>
|
||||
<label>Deploy scope<select bind:value={game} disabled={busy}><option value="">Unscoped — select servers in a beta</option><option value="t5">Black Ops I</option><option value="t6">Black Ops II</option></select></label>
|
||||
<label
|
||||
>Plugin name<input
|
||||
bind:value={name}
|
||||
disabled={busy}
|
||||
placeholder="t6-gsc-utils"
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Deploy scope<select bind:value={game} disabled={busy}
|
||||
><option value="">Unscoped — select servers in a beta</option
|
||||
><option value="t5">Black Ops I</option><option value="t6"
|
||||
>Black Ops II</option
|
||||
></select
|
||||
></label
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if targetPath}<p>New versions deploy to <code>{targetPath}</code>.</p>{/if}
|
||||
<div class="actions">
|
||||
<Button variant="primary" disabled={busy || disabled || !assetId || (!pluginId && !name.trim())} loading={busy} onclick={() => importRelease(true)}>Import and test</Button>
|
||||
<Button disabled={busy || disabled || !assetId || (!pluginId && !name.trim())} onclick={() => importRelease(false)}>Import only</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={busy || disabled || !assetId || (!pluginId && !name.trim())}
|
||||
loading={busy}
|
||||
onclick={() => importRelease(true)}>Import and test</Button
|
||||
>
|
||||
<Button
|
||||
disabled={busy || disabled || !assetId || (!pluginId && !name.trim())}
|
||||
onclick={() => importRelease(false)}>Import only</Button
|
||||
>
|
||||
</div>
|
||||
<p>Imports are saved for testing. Choose servers and a test plan before starting a beta.</p>
|
||||
<p>
|
||||
Imports are saved for testing. Choose servers and a test plan before
|
||||
starting a beta.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success}
|
||||
<div class="success" role="status">
|
||||
<strong>{success.alreadyImported ? 'This binary is already imported.' : 'Release imported.'}</strong>
|
||||
<a href={`/plugins/${success.pluginId}`}>View plugin</a> · <a href={success.betaUrl}>Start beta</a>
|
||||
<strong
|
||||
>{success.alreadyImported
|
||||
? 'This binary is already imported.'
|
||||
: 'Release imported.'}</strong
|
||||
>
|
||||
<a href={`/plugins/${success.pluginId}`}>View plugin</a> ·
|
||||
<a href={success.betaUrl}>Start beta</a>
|
||||
<span class="checksum">SHA-256: {success.sha256}</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -154,27 +291,121 @@
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
.github, .preview { display: grid; gap: var(--space-4); }
|
||||
.heading, .actions, .update { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
|
||||
.actions { justify-content: flex-start; }
|
||||
h2, h3, p { margin: 0; }
|
||||
h2 { font-size: var(--text-md); font-weight: var(--weight-semibold); }
|
||||
h3 { font-size: var(--text-md); }
|
||||
p { color: var(--text-secondary); font-size: var(--text-sm); line-height: 1.6; }
|
||||
.heading p { margin-top: var(--space-2); }
|
||||
.lookup { display: flex; align-items: flex-end; gap: var(--space-3); flex-wrap: wrap; }
|
||||
.lookup label { flex: 1 1 300px; }
|
||||
label { display: grid; gap: var(--space-2); font-size: var(--text-sm); }
|
||||
input, select { width: 100%; min-width: 0; padding: 10px 12px; background: var(--surface-sunken); border: 1px solid var(--border-strong); border-radius: var(--radius-md); color: var(--text-primary); font: inherit; }
|
||||
input:focus-visible, select:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
|
||||
.fields { display: grid; gap: var(--space-3); grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }
|
||||
.preview { padding-top: var(--space-4); border-top: 1px solid var(--border-subtle); }
|
||||
a { color: var(--accent-primary-hover); }
|
||||
pre { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 280px; overflow-y: auto; font: inherit; font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.6; }
|
||||
summary { cursor: pointer; font-size: var(--text-sm); }
|
||||
.error { color: var(--state-danger); }
|
||||
.success { display: block; padding: var(--space-3); background: var(--surface-sunken); border-radius: var(--radius-md); font-size: var(--text-sm); }
|
||||
.success strong, .checksum { display: block; margin-bottom: var(--space-2); }
|
||||
.checksum { overflow-wrap: anywhere; font: var(--text-xs) var(--font-mono); margin-top: var(--space-2); }
|
||||
input:not([type='checkbox']):not([type='file']), select { border-color: var(--border-control); }
|
||||
.github,
|
||||
.preview {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.heading,
|
||||
.actions,
|
||||
.update {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--text-md);
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.heading p {
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
.lookup {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-3);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.lookup label {
|
||||
flex: 1 1 300px;
|
||||
}
|
||||
label {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
background: var(--surface-sunken);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
}
|
||||
input:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid var(--accent-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.fields {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
|
||||
}
|
||||
.preview {
|
||||
padding-top: var(--space-4);
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
a {
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
font: inherit;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.error {
|
||||
color: var(--state-danger);
|
||||
}
|
||||
.success {
|
||||
display: block;
|
||||
padding: var(--space-3);
|
||||
background: var(--surface-sunken);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.success strong,
|
||||
.checksum {
|
||||
display: block;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.checksum {
|
||||
overflow-wrap: anywhere;
|
||||
font: var(--text-xs) var(--font-mono);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
input:not([type='checkbox']):not([type='file']),
|
||||
select {
|
||||
border-color: var(--border-control);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user