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 { Settings, Plus, Check, LogOut, Home } from '@lucide/svelte';
|
||||
import Avatar from './Avatar.svelte';
|
||||
import { abs, activeAccount, app, instances, removeAccount, selectInstance, switchAccount } from '../lib/store.svelte';
|
||||
|
||||
let menu = $state(false);
|
||||
const kindLabel = { panel: 'Account', microsoft: 'Microsoft', offline: 'Offline' };
|
||||
const initials = (name: string) => name.split(/\s+/).map((w) => w[0]).join('').slice(0, 2).toUpperCase();
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={(e) => menu && !(e.target as HTMLElement).closest('.account') && (menu = false)} />
|
||||
|
||||
<nav class="rail glass">
|
||||
<div class="list">
|
||||
{#each instances() as inst (inst.id)}
|
||||
<button
|
||||
class="inst"
|
||||
class:active={app.view === 'home' && app.selected === inst.id}
|
||||
class:running={app.running === inst.id}
|
||||
onclick={() => { selectInstance(inst.id); app.view = 'home'; }}
|
||||
aria-label={inst.name}
|
||||
>
|
||||
{#if inst.icon_url}<img src={abs(inst.icon_url)} alt="" />{:else}<span>{initials(inst.name)}</span>{/if}
|
||||
<em class="tip">{inst.name}</em>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
{#if app.view === 'settings'}
|
||||
<button class="ghost icon big" onclick={() => (app.view = 'home')} aria-label="Home"><Home size={20} /></button>
|
||||
{/if}
|
||||
<button class="ghost icon big" class:on={app.view === 'settings'} onclick={() => (app.view = 'settings')} aria-label="Settings">
|
||||
<Settings size={20} />
|
||||
{#if app.update}<span class="dot"></span>{/if}
|
||||
</button>
|
||||
<div class="account">
|
||||
<button class="me" onclick={() => (menu = !menu)} aria-label="Accounts">
|
||||
<Avatar account={activeAccount()} size={2.3} />
|
||||
</button>
|
||||
{#if menu}
|
||||
<div class="menu glass">
|
||||
<span class="tiny muted head">Accounts</span>
|
||||
{#each app.accounts as a (a.id)}
|
||||
<div class="acc" class:current={a.id === app.active}>
|
||||
<button class="pick" onclick={() => { switchAccount(a.id); menu = false; }}>
|
||||
<Avatar account={a} size={1.9} />
|
||||
<span class="who"><strong>{a.username}</strong><span class="tiny muted">{kindLabel[a.kind]}</span></span>
|
||||
{#if a.id === app.active}<Check size={16} />{/if}
|
||||
</button>
|
||||
<button class="ghost icon" title="Sign out" aria-label="Sign out {a.username}" onclick={() => removeAccount(a.id)}><LogOut size={14} /></button>
|
||||
</div>
|
||||
{/each}
|
||||
<button class="add" onclick={() => { app.addAccount = true; menu = false; }}><Plus size={16} /> Add account</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.rail { width: 4.8rem; margin: 0 0 0.8rem 0.8rem; display: flex; flex-direction: column; align-items: center; padding: 0.8rem 0; gap: 0.5rem; border-radius: calc(var(--radius) + 4px); position: relative; z-index: 20; flex-shrink: 0; }
|
||||
.list { display: flex; flex-direction: column; gap: 0.6rem; align-items: center; overflow-y: auto; overflow-x: visible; flex: 1; width: 100%; padding: 0.2rem 0; scrollbar-width: none; }
|
||||
.inst {
|
||||
width: 3.1rem; height: 3.1rem; padding: 0; border-radius: var(--radius); position: relative; flex-shrink: 0; overflow: visible;
|
||||
background: color-mix(in srgb, var(--surface) 80%, var(--text) 6%); border-color: var(--line); font-weight: 750; font-size: 0.95rem;
|
||||
transition: transform 0.2s var(--ease), box-shadow 0.2s, border-radius 0.2s;
|
||||
}
|
||||
.inst img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
|
||||
.inst:hover { transform: translateY(-1px); border-radius: calc(var(--radius) * 0.75); }
|
||||
.inst.active { background: linear-gradient(135deg, var(--accent), var(--accent-2)); border-color: transparent; box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent), 0 0.5rem 1.5rem -0.5rem var(--accent); color: white; }
|
||||
.inst.running::after { content: ''; position: absolute; right: -0.2rem; bottom: -0.2rem; width: 0.8rem; height: 0.8rem; border-radius: 50%; background: var(--success); border: 2px solid var(--surface); box-shadow: 0 0 0.6rem var(--success); }
|
||||
.tip { position: fixed; left: 6.4rem; transform: translateY(0); font-style: normal; font-size: 0.82rem; font-weight: 600; white-space: nowrap; padding: 0.35rem 0.7rem; border-radius: 0.5rem; background: var(--surface); border: 1px solid var(--line-strong); opacity: 0; pointer-events: none; transition: opacity 0.15s; box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.4); z-index: 50; color: var(--text); }
|
||||
.inst:hover .tip { opacity: 1; }
|
||||
.bottom { display: flex; flex-direction: column; align-items: center; gap: 0.35rem; }
|
||||
.big { width: 2.8rem; height: 2.8rem; position: relative; }
|
||||
.big.on { color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, transparent); }
|
||||
.dot { position: absolute; top: 0.55rem; right: 0.55rem; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--accent-2); box-shadow: 0 0 0.5rem var(--accent-2); }
|
||||
.account { position: relative; margin-top: 0.3rem; }
|
||||
.me { padding: 0.15rem; border-radius: 30%; background: transparent; border-color: transparent; }
|
||||
.me:hover { border-color: var(--accent); background: transparent; }
|
||||
.menu { position: absolute; left: calc(100% + 0.9rem); bottom: 0; width: 17rem; padding: 0.6rem; display: flex; flex-direction: column; gap: 0.25rem; background: color-mix(in srgb, var(--surface) 99%, var(--text) 1%); backdrop-filter: none; box-shadow: 0 1.5rem 4rem -1rem rgba(0, 0, 0, 0.7); animation: fade-up 0.18s var(--ease); z-index: 60; }
|
||||
.head { padding: 0.2rem 0.5rem 0.4rem; text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700; }
|
||||
.acc { display: flex; align-items: center; gap: 0.2rem; border-radius: var(--radius-sm); }
|
||||
.acc.current { background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
||||
.pick { flex: 1; justify-content: flex-start; background: transparent; border-color: transparent; padding: 0.45rem 0.5rem; gap: 0.65rem; min-width: 0; }
|
||||
.pick :global(svg) { color: var(--accent); }
|
||||
.who { display: flex; flex-direction: column; align-items: flex-start; flex: 1; min-width: 0; }
|
||||
.who strong { font-size: 0.9rem; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
||||
.add { margin-top: 0.3rem; border-style: dashed; background: transparent; }
|
||||
</style>
|
||||
Reference in new issue
Block a user