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,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>
|
||||
Reference in new issue
Block a user