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:
Claude committed 2026-09-28 05:00:30 +00:00
1 parent af9147ec75
commit 50eb1a0cac
60 files changed
+9424 -47

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
<script lang="ts">
import { SlidersHorizontal, SquareTerminal, Package, MessageCircle, Globe, ShoppingBag, CirclePlay, Tv, Code, AtSign, Link } from '@lucide/svelte';
import PlayButton from '../components/PlayButton.svelte';
import ServerStatus from '../components/ServerStatus.svelte';
import NewsFeed from '../components/NewsFeed.svelte';
import Console from '../components/Console.svelte';
import { activeAccount, app, instances, selectedInstance } from '../lib/store.svelte';
import { loaderName, shortLoaderVersion } from '../lib/format';
import { openUrl } from '../lib/tauri';
const inst = $derived(selectedInstance());
const b = $derived(app.manifest!.branding);
// Lucide ships no brand logos, so each platform gets a recognisable generic icon.
const linkIcon: Record<string, any> = { discord: MessageCircle, website: Globe, store: ShoppingBag, youtube: CirclePlay, twitch: Tv, github: Code, twitter: AtSign };
const hour = new Date().getHours();
const greet = hour < 5 ? 'Late night session' : hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
</script>
<div class="home">
{#if inst}
{#key inst.id}
<section class="hero">
<p class="greet muted">{greet}, <strong>{activeAccount()?.username ?? 'player'}</strong></p>
<div class="chips">
<span class="chip">{inst.mc_version}</span>
<span class="chip">{loaderName[inst.loader]} {shortLoaderVersion(inst.mc_version, inst.loader_version)}</span>
{#if inst.file_count}<span class="chip"><Package size={12} /> {inst.file_count} mods & files</span>{/if}
</div>
<h1>{inst.name}</h1>
<p class="desc">{inst.description || b.tagline}</p>
</section>
<section class="actions">
<PlayButton {inst} />
<div class="row tools">
<button class="glass tool" onclick={() => (app.instanceSettings = inst.id)} title="Instance settings"><SlidersHorizontal size={17} /></button>
<button class="glass tool" class:on={app.consoleOpen} onclick={() => (app.consoleOpen = !app.consoleOpen)} title="Game console"><SquareTerminal size={17} /></button>
{#if inst.server && b.features.server_status}<ServerStatus server={inst.server} />{/if}
</div>
</section>
{/key}
{:else}
<section class="hero empty">
<h1>{b.name}</h1>
<p class="desc">{instances().length === 0 ? "Nothing to play yet — your server admin hasn't published any instances you can access." : ''}</p>
</section>
{/if}
<aside class="side">
{#if b.features.news && b.news.length}<NewsFeed news={b.news} />{:else}<div></div>{/if}
{#if b.links.length}
<div class="links">
{#each b.links as l}
{@const Icon = linkIcon[l.icon] ?? Link}
<button class="glass link" onclick={() => openUrl(l.url)} title={l.label}><Icon size={16} /> <span>{l.label}</span></button>
{/each}
</div>
{/if}
</aside>
{#if app.consoleOpen}<Console />{/if}
</div>
<style>
.home { position: relative; height: 100%; display: grid; grid-template-columns: 1fr minmax(17rem, 23rem); grid-template-rows: 1fr auto; gap: 1.5rem 2rem; padding: 1.5rem 1.8rem 1.8rem 2.2rem; }
.hero { grid-column: 1; grid-row: 1; align-self: center; display: flex; flex-direction: column; gap: 0.9rem; animation: fade-up 0.45s var(--ease); max-width: 40rem; }
.greet { font-size: 0.95rem; }
.greet strong { color: var(--text); font-weight: 600; }
.chips { display: flex; gap: 0.45rem; flex-wrap: wrap; }
h1 { font-size: clamp(2.8rem, 6.2vw, 4.8rem); line-height: 0.98; font-weight: 800; letter-spacing: -0.035em; text-shadow: 0 0.2rem 2rem color-mix(in srgb, var(--bg) 60%, transparent); }
.desc { font-size: 1.05rem; line-height: 1.55; color: color-mix(in srgb, var(--text) 72%, transparent); max-width: 32rem; }
.actions { grid-column: 1; grid-row: 2; display: flex; flex-direction: column; gap: 0.9rem; animation: fade-up 0.5s var(--ease) 0.05s both; }
.tools { flex-wrap: wrap; gap: 0.6rem; }
.tool { width: 3.1rem; height: 3.1rem; padding: 0; border-radius: var(--radius); color: color-mix(in srgb, var(--text) 80%, transparent); }
.tool.on { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.side { grid-column: 2; grid-row: 1 / 3; display: flex; flex-direction: column; justify-content: space-between; gap: 1rem; min-height: 0; animation: fade-up 0.5s var(--ease) 0.1s both; }
.links { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.link { padding: 0.55rem 0.8rem; font-size: 0.84rem; border-radius: var(--radius-sm); flex: 1 1 auto; }
.link:hover { border-color: var(--line-strong); background: color-mix(in srgb, var(--surface) 80%, transparent); }
@media (max-width: 1060px) { .home { grid-template-columns: 1fr 16rem; } .link span { display: none; } }
</style>