aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui/src/components/BottomBar.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ui/src/components/BottomBar.svelte')
-rw-r--r--packages/ui/src/components/BottomBar.svelte250
1 files changed, 250 insertions, 0 deletions
diff --git a/packages/ui/src/components/BottomBar.svelte b/packages/ui/src/components/BottomBar.svelte
new file mode 100644
index 0000000..19cf35d
--- /dev/null
+++ b/packages/ui/src/components/BottomBar.svelte
@@ -0,0 +1,250 @@
+<script lang="ts">
+ import { invoke } from "@tauri-apps/api/core";
+ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
+ import { authState } from "../stores/auth.svelte";
+ import { gameState } from "../stores/game.svelte";
+ import { uiState } from "../stores/ui.svelte";
+ import { instancesState } from "../stores/instances.svelte";
+ import { Terminal, ChevronDown, Play, User, Check } from 'lucide-svelte';
+
+ interface InstalledVersion {
+ id: string;
+ type: string;
+ }
+
+ let isVersionDropdownOpen = $state(false);
+ let dropdownRef: HTMLDivElement;
+ let installedVersions = $state<InstalledVersion[]>([]);
+ let isLoadingVersions = $state(true);
+ let downloadCompleteUnlisten: UnlistenFn | null = null;
+ let versionDeletedUnlisten: UnlistenFn | null = null;
+
+ // Load installed versions on mount
+ $effect(() => {
+ loadInstalledVersions();
+ setupEventListeners();
+ return () => {
+ if (downloadCompleteUnlisten) {
+ downloadCompleteUnlisten();
+ }
+ if (versionDeletedUnlisten) {
+ versionDeletedUnlisten();
+ }
+ };
+ });
+
+ async function setupEventListeners() {
+ // Refresh list when a download completes
+ downloadCompleteUnlisten = await listen("download-complete", () => {
+ loadInstalledVersions();
+ });
+ // Refresh list when a version is deleted
+ versionDeletedUnlisten = await listen("version-deleted", () => {
+ loadInstalledVersions();
+ });
+ }
+
+ async function loadInstalledVersions() {
+ if (!instancesState.activeInstanceId) {
+ installedVersions = [];
+ isLoadingVersions = false;
+ return;
+ }
+ isLoadingVersions = true;
+ try {
+ installedVersions = await invoke<InstalledVersion[]>("list_installed_versions", {
+ instanceId: instancesState.activeInstanceId,
+ });
+ // If no version is selected but we have installed versions, select the first one
+ if (!gameState.selectedVersion && installedVersions.length > 0) {
+ gameState.selectedVersion = installedVersions[0].id;
+ }
+ } catch (e) {
+ console.error("Failed to load installed versions:", e);
+ } finally {
+ isLoadingVersions = false;
+ }
+ }
+
+ let versionOptions = $derived(
+ isLoadingVersions
+ ? [{ id: "loading", type: "loading", label: "Loading..." }]
+ : installedVersions.length === 0
+ ? [{ id: "empty", type: "empty", label: "No versions installed" }]
+ : installedVersions.map(v => ({
+ ...v,
+ label: `${v.id}${v.type !== 'release' ? ` (${v.type})` : ''}`
+ }))
+ );
+
+ function selectVersion(id: string) {
+ if (id !== "loading" && id !== "empty") {
+ gameState.selectedVersion = id;
+ isVersionDropdownOpen = false;
+ }
+ }
+
+ function handleClickOutside(e: MouseEvent) {
+ if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
+ isVersionDropdownOpen = false;
+ }
+ }
+
+ $effect(() => {
+ if (isVersionDropdownOpen) {
+ document.addEventListener('click', handleClickOutside);
+ return () => document.removeEventListener('click', handleClickOutside);
+ }
+ });
+
+ function getVersionTypeColor(type: string) {
+ switch (type) {
+ case 'fabric': return 'text-indigo-400';
+ case 'forge': return 'text-orange-400';
+ case 'snapshot': return 'text-amber-400';
+ case 'modpack': return 'text-purple-400';
+ default: return 'text-emerald-400';
+ }
+ }
+</script>
+
+<div
+ class="h-20 bg-white/80 dark:bg-[#09090b]/90 border-t dark:border-white/10 border-black/5 flex items-center px-8 justify-between z-20 backdrop-blur-md"
+>
+ <!-- Account Area -->
+ <div class="flex items-center gap-6">
+ <div
+ class="group flex items-center gap-4 cursor-pointer"
+ onclick={() => authState.openLoginModal()}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === "Enter" && authState.openLoginModal()}
+ >
+ <div
+ class="w-10 h-10 rounded-sm bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 flex items-center justify-center overflow-hidden transition-all group-hover:border-zinc-400 dark:group-hover:border-zinc-500"
+ >
+ {#if authState.currentAccount}
+ <img
+ src={`https://minotar.net/avatar/${authState.currentAccount.username}/48`}
+ alt={authState.currentAccount.username}
+ class="w-full h-full"
+ />
+ {:else}
+ <User size={20} class="text-zinc-400" />
+ {/if}
+ </div>
+ <div>
+ <div class="font-bold dark:text-white text-gray-900 text-sm group-hover:text-black dark:group-hover:text-zinc-200 transition-colors">
+ {authState.currentAccount ? authState.currentAccount.username : "Login Account"}
+ </div>
+ <div class="text-[10px] uppercase tracking-wider dark:text-zinc-500 text-gray-500 flex items-center gap-2">
+ {#if authState.currentAccount}
+ {#if authState.currentAccount.type === "Microsoft"}
+ {#if authState.currentAccount.expires_at && authState.currentAccount.expires_at * 1000 < Date.now()}
+ <span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>
+ <span class="text-red-400">Expired</span>
+ {:else}
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500"></span>
+ Online
+ {/if}
+ {:else}
+ <span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>
+ Offline
+ {/if}
+ {:else}
+ <span class="w-1.5 h-1.5 rounded-full bg-zinc-400"></span>
+ Guest
+ {/if}
+ </div>
+ </div>
+ </div>
+
+ <div class="h-8 w-px dark:bg-white/10 bg-black/10"></div>
+
+ <!-- Console Toggle -->
+ <button
+ class="text-xs font-mono dark:text-zinc-500 text-gray-500 dark:hover:text-white hover:text-black transition-colors flex items-center gap-2 px-2 py-1 rounded-sm hover:bg-black/5 dark:hover:bg-white/5"
+ onclick={() => uiState.toggleConsole()}
+ >
+ <Terminal size={14} />
+ {uiState.showConsole ? "HIDE LOGS" : "SHOW LOGS"}
+ </button>
+ </div>
+
+ <!-- Action Area -->
+ <div class="flex items-center gap-4">
+ <div class="flex flex-col items-end mr-2">
+ <!-- Custom Version Dropdown -->
+ <div class="relative" bind:this={dropdownRef}>
+ <button
+ type="button"
+ onclick={() => isVersionDropdownOpen = !isVersionDropdownOpen}
+ disabled={installedVersions.length === 0 && !isLoadingVersions}
+ class="flex items-center justify-between gap-2 w-56 px-4 py-2.5 text-left
+ dark:bg-zinc-900 bg-zinc-50 border dark:border-zinc-700 border-zinc-300 rounded-md
+ text-sm font-mono dark:text-white text-gray-900
+ dark:hover:border-zinc-600 hover:border-zinc-400
+ focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30
+ transition-colors cursor-pointer outline-none
+ disabled:opacity-50 disabled:cursor-not-allowed"
+ >
+ <span class="truncate">
+ {#if isLoadingVersions}
+ Loading...
+ {:else if installedVersions.length === 0}
+ No versions installed
+ {:else}
+ {gameState.selectedVersion || "Select version"}
+ {/if}
+ </span>
+ <ChevronDown
+ size={14}
+ class="shrink-0 dark:text-zinc-500 text-zinc-400 transition-transform duration-200 {isVersionDropdownOpen ? 'rotate-180' : ''}"
+ />
+ </button>
+
+ {#if isVersionDropdownOpen && installedVersions.length > 0}
+ <div
+ class="absolute z-50 w-full mt-1 py-1 dark:bg-zinc-900 bg-white border dark:border-zinc-700 border-zinc-300 rounded-md shadow-xl
+ max-h-72 overflow-y-auto animate-in fade-in slide-in-from-top-1 duration-150 bottom-full mb-1 right-0"
+ >
+ {#each versionOptions as version}
+ <button
+ type="button"
+ onclick={() => selectVersion(version.id)}
+ disabled={version.id === "loading" || version.id === "empty"}
+ class="w-full flex items-center justify-between px-3 py-2 text-sm font-mono text-left
+ transition-colors outline-none
+ {version.id === gameState.selectedVersion
+ ? 'bg-indigo-600 text-white'
+ : 'dark:text-zinc-300 text-gray-700 dark:hover:bg-zinc-800 hover:bg-zinc-100'}
+ {version.id === 'loading' || version.id === 'empty' ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}"
+ >
+ <span class="truncate flex items-center gap-2">
+ {version.id}
+ {#if version.type && version.type !== 'release' && version.type !== 'loading' && version.type !== 'empty'}
+ <span class="text-[10px] uppercase {version.id === gameState.selectedVersion ? 'text-white/70' : getVersionTypeColor(version.type)}">
+ {version.type}
+ </span>
+ {/if}
+ </span>
+ {#if version.id === gameState.selectedVersion}
+ <Check size={14} class="shrink-0 ml-2" />
+ {/if}
+ </button>
+ {/each}
+ </div>
+ {/if}
+ </div>
+ </div>
+
+ <button
+ onclick={() => gameState.startGame()}
+ disabled={installedVersions.length === 0 || !gameState.selectedVersion}
+ class="bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white h-14 px-10 rounded-sm transition-all duration-200 hover:scale-[1.02] active:scale-[0.98] shadow-lg shadow-emerald-500/20 flex items-center gap-3 font-bold text-lg tracking-widest uppercase"
+ >
+ <Play size={24} fill="currentColor" />
+ <span>Launch</span>
+ </button>
+ </div>
+</div>