Add Tauri launcher with Svelte UI
- Accounts: panel login/sign-up, Microsoft device-code sign-in, offline play; tokens encrypted with a key kept in the OS credential store - Home: instance rail, live server status, news feed, social links, Play button with download progress, speed and ETA - Full settings: accounts, memory/resolution/after-launch behaviour, Java and GC presets, Minecraft keybind editor applied to options.txt, theme overrides, storage manager, self-update via GitHub releases - Game console, crash dialog with hints, per-instance overrides - Live admin branding (colours, fonts, backgrounds, custom CSS) - Online install tests for vanilla/Fabric/Quilt/Forge/NeoForge (CI) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
This commit is contained in:
60 files changed
+9424
-47
No files matched your search
@@ -0,0 +1,91 @@
|
||||
<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={22} 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.7rem; }
|
||||
.play {
|
||||
position: relative; overflow: hidden; height: 4.2rem; min-width: 19rem; padding: 0 2.6rem; border: none; border-radius: var(--radius);
|
||||
background: linear-gradient(120deg, var(--accent), var(--accent-2)); color: white; font-size: 1.35rem; font-weight: 800; letter-spacing: 0.12em;
|
||||
box-shadow: 0 1rem 2.6rem -0.9rem var(--accent), inset 0 1px 0 rgba(255, 255, 255, 0.25); gap: 0.8rem;
|
||||
transition: transform 0.2s var(--ease), box-shadow 0.25s, filter 0.2s;
|
||||
}
|
||||
.play::before { content: ''; position: absolute; inset: 0; background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, 0.28) 50%, transparent 70%); transform: translateX(-100%); transition: transform 0.7s var(--ease); }
|
||||
.play:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 1.4rem 3rem -0.9rem var(--accent), inset 0 1px 0 rgba(255, 255, 255, 0.3); filter: brightness(1.05); }
|
||||
.play:hover:not(:disabled)::before { transform: translateX(100%); }
|
||||
.play:active:not(:disabled) { transform: translateY(0) scale(0.985); }
|
||||
.play:disabled { filter: grayscale(0.7) brightness(0.7); }
|
||||
.lbl { position: relative; }
|
||||
.running { background: linear-gradient(120deg, color-mix(in srgb, var(--success) 70%, var(--bg)), color-mix(in srgb, var(--success) 45%, var(--accent-2))); box-shadow: 0 1rem 2.6rem -1rem var(--success); padding: 0 1.6rem 0 2rem; }
|
||||
.pulse { width: 0.7rem; height: 0.7rem; border-radius: 50%; background: white; box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); animation: ping 1.6s infinite; }
|
||||
@keyframes ping { 70% { box-shadow: 0 0 0 0.7rem rgba(255, 255, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); } }
|
||||
.stop { font-size: 0.78rem; letter-spacing: 0.04em; font-weight: 650; display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.35rem 0.7rem; border-radius: 99rem; background: rgba(0, 0, 0, 0.2); opacity: 0.85; }
|
||||
.running:hover .stop { background: rgba(0, 0, 0, 0.35); opacity: 1; }
|
||||
.progress { width: 28rem; max-width: 100%; padding: 0.95rem 1.1rem 0.85rem; display: flex; flex-direction: column; gap: 0.6rem; animation: fade-up 0.25s var(--ease); }
|
||||
.top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
|
||||
.stage { display: flex; align-items: center; gap: 0.5rem; font-weight: 620; font-size: 0.95rem; }
|
||||
.bar { height: 0.5rem; border-radius: 99rem; background: color-mix(in srgb, var(--text) 10%, transparent); overflow: hidden; position: relative; }
|
||||
.bar span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--accent-2)); transition: width 0.2s linear; box-shadow: 0 0 1rem var(--accent); }
|
||||
.bar.indeterminate span { width: 35% !important; animation: slide 1.2s ease-in-out infinite; }
|
||||
@keyframes slide { from { transform: translateX(-100%); } to { transform: translateX(300%); } }
|
||||
.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.7rem 0.9rem; max-width: 34rem; font-size: 0.87rem; border-color: color-mix(in srgb, var(--danger) 35%, transparent); background: color-mix(in srgb, var(--danger) 14%, var(--panel)); line-height: 1.4; }
|
||||
.error :global(svg) { color: var(--danger); flex-shrink: 0; }
|
||||
.error span { flex: 1; }
|
||||
</style>
|
||||
Reference in new issue
Block a user