Files
SCOPENET-MC/launcher/src/components/PlayButton.svelte
T
Claude 1b1bb984bc Restyle to a clean, flat look and rename brand to SCOPENET
- Brand is now SCOPENET everywhere (defaults, UI, installer, docs)
- Launcher: solid backgrounds (no animated orbs/grain), flat Play button
  without gradient/shine/glow, quieter rail with a simple active
  indicator, smaller type scale, Inter as default font, glass off by
  default, restyled sliders
- Panel: same flat treatment; live preview mirrors the new launcher
- New flat scope-reticle logo and app icons
- Calmer default palette
- Fix: lock the shared cargo cache in the Docker build (amd64/arm64 raced)
- Fix: Windows-only clippy lint in sys.rs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
2026-09-28 05:30:19 +00:00

87 lines
5.5 KiB
Svelte

<script lang="ts">
import { Play, X, Square, LoaderCircle, TriangleAlert, RotateCcw } from '@lucide/svelte';
import { app, cancel, kill, play } from '../lib/store.svelte';
import { bytes } from '../lib/format';
import type { Instance } from '../lib/types';
let { inst }: { inst: Instance } = $props();
const l = $derived(app.launch);
const mine = $derived(app.launchingId === inst.id);
const busy = $derived(mine && l.state === 'preparing');
const running = $derived(app.running === inst.id);
const otherRunning = $derived(!!app.running && app.running !== inst.id);
const pct = $derived(l.total > 0 ? Math.min(100, (l.done / l.total) * 100) : 0);
const eta = $derived(l.speed > 0 && l.total > l.done ? Math.ceil((l.total - l.done) / l.speed) : 0);
const stageNames: Record<string, string> = { preparing: 'Preparing', java: 'Java', game: 'Game files', loader: 'Mod loader', files: 'Instance files', launching: 'Launching' };
const noAccount = $derived(!app.active);
</script>
<svelte:window onkeydown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter' && !busy && !running && !otherRunning && !noAccount && app.view === 'home') play(inst.id);
}} />
<div class="wrap">
{#if busy}
<div class="progress glass">
<div class="top">
<span class="stage">
{#if l.stage === 'launching' || !l.total}<LoaderCircle class="spin" size={15} />{/if}
{l.label || 'Preparing…'}
</span>
<button class="ghost icon sm" onclick={cancel} aria-label="Cancel" title="Cancel"><X size={15} /></button>
</div>
<div class="bar" class:indeterminate={!l.total}><span style:width="{pct}%"></span></div>
<div class="meta tiny muted">
<span>{l.stage ? stageNames[l.stage] : ''}{l.filesTotal ? ` · ${l.filesDone.toLocaleString()} / ${l.filesTotal.toLocaleString()} files` : ''}</span>
<span>{#if l.total}{bytes(l.done)} / {bytes(l.total)}{#if l.speed > 0} · {bytes(l.speed)}/s{#if eta} · {eta < 60 ? `${eta}s` : `${Math.ceil(eta / 60)}m`} left{/if}{/if}{/if}</span>
</div>
</div>
{:else if running}
<button class="play running" onclick={kill} title="Force-close Minecraft">
<span class="pulse"></span>
<span class="lbl">Playing</span>
<span class="stop"><Square size={13} fill="currentColor" /> Stop</span>
</button>
{:else}
<button class="play" onclick={() => play(inst.id)} disabled={otherRunning || noAccount} title={noAccount ? 'Sign in first' : otherRunning ? 'Another instance is running' : 'Ctrl + Enter'}>
<Play size={18} fill="currentColor" />
<span class="lbl">Play</span>
</button>
{/if}
{#if mine && l.state === 'error'}
<div class="error glass selectable">
<TriangleAlert size={17} />
<span>{l.message}</span>
<button class="sm" onclick={() => play(inst.id)}><RotateCcw size={13} /> Retry</button>
</div>
{/if}
</div>
<style>
.wrap { display: flex; flex-direction: column; align-items: flex-start; gap: 0.6rem; }
.play {
height: 3.3rem; min-width: 14rem; padding: 0 2rem; border: none; border-radius: var(--radius);
background: var(--accent); color: white; font-size: 1.05rem; font-weight: 600; letter-spacing: 0.02em; gap: 0.65rem;
}
.play:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 88%, white); }
.play:active:not(:disabled) { background: color-mix(in srgb, var(--accent) 85%, black); }
.play:disabled { background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--muted); }
.lbl { position: relative; }
.running { background: color-mix(in srgb, var(--success) 16%, var(--surface)); color: var(--text); border: 1px solid color-mix(in srgb, var(--success) 35%, transparent); padding: 0 0.6rem 0 1.4rem; }
.running:hover:not(:disabled) { background: color-mix(in srgb, var(--success) 22%, var(--surface)); }
.pulse { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--success); }
.stop { font-size: 0.8rem; font-weight: 520; letter-spacing: 0; display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.7rem; border-radius: var(--radius-sm); background: color-mix(in srgb, var(--text) 8%, transparent); color: color-mix(in srgb, var(--text) 80%, transparent); margin-left: 0.6rem; }
.progress { width: 26rem; max-width: 100%; padding: 0.85rem 1rem 0.8rem; display: flex; flex-direction: column; gap: 0.55rem; background: var(--surface); }
.top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.stage { display: flex; align-items: center; gap: 0.5rem; font-weight: 560; font-size: 0.9rem; }
.bar { height: 0.3rem; border-radius: 99rem; background: color-mix(in srgb, var(--text) 9%, transparent); overflow: hidden; position: relative; }
.bar span { display: block; height: 100%; border-radius: inherit; background: var(--accent); transition: width 0.2s linear; }
.bar.indeterminate span { width: 30% !important; animation: slide 1.3s ease-in-out infinite; }
@keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(340%); } }
.meta { display: flex; justify-content: space-between; gap: 1rem; font-variant-numeric: tabular-nums; }
.error { display: flex; align-items: center; gap: 0.7rem; padding: 0.65rem 0.8rem; max-width: 34rem; font-size: 0.85rem; background: color-mix(in srgb, var(--danger) 10%, var(--surface)); border-color: color-mix(in srgb, var(--danger) 30%, transparent); line-height: 1.4; }
.error :global(svg) { color: var(--danger); flex-shrink: 0; }
.error span { flex: 1; }
</style>