aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/ui/src/stores
diff options
context:
space:
mode:
Diffstat (limited to 'ui/src/stores')
-rw-r--r--ui/src/stores/game.svelte.ts4
-rw-r--r--ui/src/stores/logs.svelte.ts139
-rw-r--r--ui/src/stores/releases.svelte.ts36
-rw-r--r--ui/src/stores/settings.svelte.ts335
4 files changed, 513 insertions, 1 deletions
diff --git a/ui/src/stores/game.svelte.ts b/ui/src/stores/game.svelte.ts
index 0af3daf..28b2db5 100644
--- a/ui/src/stores/game.svelte.ts
+++ b/ui/src/stores/game.svelte.ts
@@ -7,6 +7,10 @@ export class GameState {
versions = $state<Version[]>([]);
selectedVersion = $state("");
+ get latestRelease() {
+ return this.versions.find((v) => v.type === "release");
+ }
+
async loadVersions() {
try {
this.versions = await invoke<Version[]>("get_versions");
diff --git a/ui/src/stores/logs.svelte.ts b/ui/src/stores/logs.svelte.ts
new file mode 100644
index 0000000..5491f70
--- /dev/null
+++ b/ui/src/stores/logs.svelte.ts
@@ -0,0 +1,139 @@
+import { listen } from "@tauri-apps/api/event";
+
+export interface LogEntry {
+ id: number;
+ timestamp: string;
+ level: "info" | "warn" | "error" | "debug" | "fatal";
+ source: string;
+ message: string;
+}
+
+// Parse Minecraft/Java log format: [HH:MM:SS] [Thread/LEVEL]: message
+// or: [HH:MM:SS] [Thread/LEVEL] [Source]: message
+const GAME_LOG_REGEX = /^\[[\d:]+\]\s*\[([^\]]+)\/(\w+)\](?:\s*\[([^\]]+)\])?:\s*(.*)$/;
+
+function parseGameLogLevel(levelStr: string): LogEntry["level"] {
+ const upper = levelStr.toUpperCase();
+ if (upper === "INFO") return "info";
+ if (upper === "WARN" || upper === "WARNING") return "warn";
+ if (upper === "ERROR" || upper === "SEVERE") return "error";
+ if (upper === "DEBUG" || upper === "TRACE" || upper === "FINE" || upper === "FINER" || upper === "FINEST") return "debug";
+ if (upper === "FATAL") return "fatal";
+ return "info";
+}
+
+export class LogsState {
+ logs = $state<LogEntry[]>([]);
+ private nextId = 0;
+ private maxLogs = 5000;
+
+ // Track all unique sources for filtering
+ sources = $state<Set<string>>(new Set(["Launcher"]));
+
+ constructor() {
+ this.addLog("info", "Launcher", "Logs initialized");
+ this.setupListeners();
+ }
+
+ addLog(level: LogEntry["level"], source: string, message: string) {
+ const now = new Date();
+ const timestamp = now.toLocaleTimeString() + "." + now.getMilliseconds().toString().padStart(3, "0");
+
+ this.logs.push({
+ id: this.nextId++,
+ timestamp,
+ level,
+ source,
+ message,
+ });
+
+ // Track source
+ if (!this.sources.has(source)) {
+ this.sources = new Set([...this.sources, source]);
+ }
+
+ if (this.logs.length > this.maxLogs) {
+ this.logs.shift();
+ }
+ }
+
+ // Parse game output and extract level/source
+ addGameLog(rawLine: string, isStderr: boolean) {
+ const match = rawLine.match(GAME_LOG_REGEX);
+
+ if (match) {
+ const [, thread, levelStr, extraSource, message] = match;
+ const level = parseGameLogLevel(levelStr);
+ // Use extraSource if available, otherwise use thread name as source hint
+ const source = extraSource || `Game/${thread.split("-")[0]}`;
+ this.addLog(level, source, message);
+ } else {
+ // Fallback: couldn't parse, use stderr as error indicator
+ const level = isStderr ? "error" : "info";
+ this.addLog(level, "Game", rawLine);
+ }
+ }
+
+ clear() {
+ this.logs = [];
+ this.sources = new Set(["Launcher"]);
+ this.addLog("info", "Launcher", "Logs cleared");
+ }
+
+ // Export with filter support
+ exportLogs(filteredLogs: LogEntry[]): string {
+ return filteredLogs
+ .map((l) => `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`)
+ .join("\n");
+ }
+
+ private async setupListeners() {
+ // General Launcher Logs
+ await listen<string>("launcher-log", (e) => {
+ this.addLog("info", "Launcher", e.payload);
+ });
+
+ // Game Stdout - parse log level
+ await listen<string>("game-stdout", (e) => {
+ this.addGameLog(e.payload, false);
+ });
+
+ // Game Stderr - parse log level, default to error
+ await listen<string>("game-stderr", (e) => {
+ this.addGameLog(e.payload, true);
+ });
+
+ // Download Events (Summarized)
+ await listen("download-start", (e) => {
+ this.addLog("info", "Downloader", `Starting batch download of ${e.payload} files...`);
+ });
+
+ await listen("download-complete", () => {
+ this.addLog("info", "Downloader", "All downloads completed.");
+ });
+
+ // Listen to file download progress to log finished files
+ await listen<any>("download-progress", (e) => {
+ const p = e.payload;
+ if (p.status === "Finished") {
+ if (p.file.endsWith(".jar")) {
+ this.addLog("info", "Downloader", `Downloaded ${p.file}`);
+ }
+ }
+ });
+
+ // Java Download
+ await listen<any>("java-download-progress", (e) => {
+ const p = e.payload;
+ if (p.status === "Downloading" && p.percentage === 0) {
+ this.addLog("info", "JavaInstaller", `Downloading Java: ${p.file_name}`);
+ } else if (p.status === "Completed") {
+ this.addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`);
+ } else if (p.status === "Error") {
+ this.addLog("error", "JavaInstaller", `Java download error`);
+ }
+ });
+ }
+}
+
+export const logsState = new LogsState();
diff --git a/ui/src/stores/releases.svelte.ts b/ui/src/stores/releases.svelte.ts
new file mode 100644
index 0000000..c858abb
--- /dev/null
+++ b/ui/src/stores/releases.svelte.ts
@@ -0,0 +1,36 @@
+import { invoke } from "@tauri-apps/api/core";
+
+export interface GithubRelease {
+ tag_name: string;
+ name: string;
+ published_at: string;
+ body: string;
+ html_url: string;
+}
+
+export class ReleasesState {
+ releases = $state<GithubRelease[]>([]);
+ isLoading = $state(false);
+ isLoaded = $state(false);
+ error = $state<string | null>(null);
+
+ async loadReleases() {
+ // If already loaded or currently loading, skip to prevent duplicate requests
+ if (this.isLoaded || this.isLoading) return;
+
+ this.isLoading = true;
+ this.error = null;
+
+ try {
+ this.releases = await invoke<GithubRelease[]>("get_github_releases");
+ this.isLoaded = true;
+ } catch (e) {
+ console.error("Failed to load releases:", e);
+ this.error = String(e);
+ } finally {
+ this.isLoading = false;
+ }
+ }
+}
+
+export const releasesState = new ReleasesState();
diff --git a/ui/src/stores/settings.svelte.ts b/ui/src/stores/settings.svelte.ts
index 397b9a6..b85e5fb 100644
--- a/ui/src/stores/settings.svelte.ts
+++ b/ui/src/stores/settings.svelte.ts
@@ -1,5 +1,15 @@
import { invoke } from "@tauri-apps/api/core";
-import type { LauncherConfig, JavaInstallation } from "../types";
+import { convertFileSrc } from "@tauri-apps/api/core";
+import { listen, type UnlistenFn } from "@tauri-apps/api/event";
+import type {
+ JavaCatalog,
+ JavaDownloadProgress,
+ JavaDownloadSource,
+ JavaInstallation,
+ JavaReleaseInfo,
+ LauncherConfig,
+ PendingJavaDownload,
+} from "../types";
import { uiState } from "./ui.svelte";
export class SettingsState {
@@ -10,14 +20,133 @@ export class SettingsState {
width: 854,
height: 480,
download_threads: 32,
+ enable_gpu_acceleration: false,
+ enable_visual_effects: true,
+ active_effect: "constellation",
+ theme: "dark",
+ custom_background_path: undefined,
+ log_upload_service: "paste.rs",
+ pastebin_api_key: undefined,
});
+
+ // Convert background path to proper asset URL
+ get backgroundUrl(): string | undefined {
+ if (this.settings.custom_background_path) {
+ return convertFileSrc(this.settings.custom_background_path);
+ }
+ return undefined;
+ }
javaInstallations = $state<JavaInstallation[]>([]);
isDetectingJava = $state(false);
+ // Java download modal state
+ showJavaDownloadModal = $state(false);
+ selectedDownloadSource = $state<JavaDownloadSource>("adoptium");
+
+ // Java catalog state
+ javaCatalog = $state<JavaCatalog | null>(null);
+ isLoadingCatalog = $state(false);
+ catalogError = $state("");
+
+ // Version selection state
+ selectedMajorVersion = $state<number | null>(null);
+ selectedImageType = $state<"jre" | "jdk">("jre");
+ showOnlyRecommended = $state(true);
+ searchQuery = $state("");
+
+ // Download progress state
+ isDownloadingJava = $state(false);
+ downloadProgress = $state<JavaDownloadProgress | null>(null);
+ javaDownloadStatus = $state("");
+
+ // Pending downloads
+ pendingDownloads = $state<PendingJavaDownload[]>([]);
+
+ // Event listener cleanup
+ private progressUnlisten: UnlistenFn | null = null;
+
+ // Computed: filtered releases based on selection
+ get filteredReleases(): JavaReleaseInfo[] {
+ if (!this.javaCatalog) return [];
+
+ let releases = this.javaCatalog.releases;
+
+ // Filter by major version if selected
+ if (this.selectedMajorVersion !== null) {
+ releases = releases.filter(r => r.major_version === this.selectedMajorVersion);
+ }
+
+ // Filter by image type
+ releases = releases.filter(r => r.image_type === this.selectedImageType);
+
+ // Filter by recommended (LTS) versions
+ if (this.showOnlyRecommended) {
+ releases = releases.filter(r => r.is_lts);
+ }
+
+ // Filter by search query
+ if (this.searchQuery.trim()) {
+ const query = this.searchQuery.toLowerCase();
+ releases = releases.filter(
+ r =>
+ r.release_name.toLowerCase().includes(query) ||
+ r.version.toLowerCase().includes(query) ||
+ r.major_version.toString().includes(query)
+ );
+ }
+
+ return releases;
+ }
+
+ // Computed: available major versions for display
+ get availableMajorVersions(): number[] {
+ if (!this.javaCatalog) return [];
+ let versions = [...this.javaCatalog.available_major_versions];
+
+ // Filter by LTS if showOnlyRecommended is enabled
+ if (this.showOnlyRecommended) {
+ versions = versions.filter(v => this.javaCatalog!.lts_versions.includes(v));
+ }
+
+ // Sort descending (newest first)
+ return versions.sort((a, b) => b - a);
+ }
+
+ // Get installation status for a release: 'installed' | 'download'
+ getInstallStatus(release: JavaReleaseInfo): 'installed' | 'download' {
+ // Find installed Java that matches the major version and image type (by path pattern)
+ const matchingInstallations = this.javaInstallations.filter(inst => {
+ // Check if this is a DropOut-managed Java (path contains temurin-XX-jre/jdk pattern)
+ const pathLower = inst.path.toLowerCase();
+ const pattern = `temurin-${release.major_version}-${release.image_type}`;
+ return pathLower.includes(pattern);
+ });
+
+ // If any matching installation exists, it's installed
+ return matchingInstallations.length > 0 ? 'installed' : 'download';
+ }
+
+ // Computed: selected release details
+ get selectedRelease(): JavaReleaseInfo | null {
+ if (!this.javaCatalog || this.selectedMajorVersion === null) return null;
+ return this.javaCatalog.releases.find(
+ r => r.major_version === this.selectedMajorVersion && r.image_type === this.selectedImageType
+ ) || null;
+ }
+
async loadSettings() {
try {
const result = await invoke<LauncherConfig>("get_settings");
this.settings = result;
+ // Force dark mode
+ if (this.settings.theme !== "dark") {
+ this.settings.theme = "dark";
+ this.saveSettings();
+ }
+ // Ensure custom_background_path is reactive
+ if (!this.settings.custom_background_path) {
+ this.settings.custom_background_path = undefined;
+ }
} catch (e) {
console.error("Failed to load settings:", e);
}
@@ -25,6 +154,11 @@ export class SettingsState {
async saveSettings() {
try {
+ // Ensure we clean up any invalid paths before saving
+ if (this.settings.custom_background_path === "") {
+ this.settings.custom_background_path = undefined;
+ }
+
await invoke("save_settings", { config: this.settings });
uiState.setStatus("Settings saved!");
} catch (e) {
@@ -53,6 +187,205 @@ export class SettingsState {
selectJava(path: string) {
this.settings.java_path = path;
}
+
+ async openJavaDownloadModal() {
+ this.showJavaDownloadModal = true;
+ this.javaDownloadStatus = "";
+ this.catalogError = "";
+ this.downloadProgress = null;
+
+ // Setup progress event listener
+ await this.setupProgressListener();
+
+ // Load catalog
+ await this.loadJavaCatalog(false);
+
+ // Check for pending downloads
+ await this.loadPendingDownloads();
+ }
+
+ async closeJavaDownloadModal() {
+ if (!this.isDownloadingJava) {
+ this.showJavaDownloadModal = false;
+ // Cleanup listener
+ if (this.progressUnlisten) {
+ this.progressUnlisten();
+ this.progressUnlisten = null;
+ }
+ }
+ }
+
+ private async setupProgressListener() {
+ if (this.progressUnlisten) {
+ this.progressUnlisten();
+ }
+
+ this.progressUnlisten = await listen<JavaDownloadProgress>(
+ "java-download-progress",
+ (event) => {
+ this.downloadProgress = event.payload;
+ this.javaDownloadStatus = event.payload.status;
+
+ if (event.payload.status === "Completed") {
+ this.isDownloadingJava = false;
+ setTimeout(async () => {
+ await this.detectJava();
+ uiState.setStatus(`Java installed successfully!`);
+ }, 500);
+ } else if (event.payload.status === "Error") {
+ this.isDownloadingJava = false;
+ }
+ }
+ );
+ }
+
+ async loadJavaCatalog(forceRefresh: boolean) {
+ this.isLoadingCatalog = true;
+ this.catalogError = "";
+
+ try {
+ const command = forceRefresh ? "refresh_java_catalog" : "fetch_java_catalog";
+ this.javaCatalog = await invoke<JavaCatalog>(command);
+
+ // Auto-select first LTS version
+ if (this.selectedMajorVersion === null && this.javaCatalog.lts_versions.length > 0) {
+ // Select most recent LTS (21 or highest)
+ const ltsVersions = [...this.javaCatalog.lts_versions].sort((a, b) => b - a);
+ this.selectedMajorVersion = ltsVersions[0];
+ }
+ } catch (e) {
+ console.error("Failed to load Java catalog:", e);
+ this.catalogError = `Failed to load Java catalog: ${e}`;
+ } finally {
+ this.isLoadingCatalog = false;
+ }
+ }
+
+ async refreshCatalog() {
+ await this.loadJavaCatalog(true);
+ uiState.setStatus("Java catalog refreshed");
+ }
+
+ async loadPendingDownloads() {
+ try {
+ this.pendingDownloads = await invoke<PendingJavaDownload[]>("get_pending_java_downloads");
+ } catch (e) {
+ console.error("Failed to load pending downloads:", e);
+ }
+ }
+
+ selectMajorVersion(version: number) {
+ this.selectedMajorVersion = version;
+ }
+
+ async downloadJava() {
+ if (!this.selectedRelease || !this.selectedRelease.is_available) {
+ uiState.setStatus("Selected Java version is not available for this platform");
+ return;
+ }
+
+ this.isDownloadingJava = true;
+ this.javaDownloadStatus = "Starting download...";
+ this.downloadProgress = null;
+
+ try {
+ const result: JavaInstallation = await invoke("download_adoptium_java", {
+ majorVersion: this.selectedMajorVersion,
+ imageType: this.selectedImageType,
+ customPath: null,
+ });
+
+ this.settings.java_path = result.path;
+ await this.detectJava();
+
+ setTimeout(() => {
+ this.showJavaDownloadModal = false;
+ uiState.setStatus(`Java ${this.selectedMajorVersion} is ready to use!`);
+ }, 1500);
+ } catch (e) {
+ console.error("Failed to download Java:", e);
+ this.javaDownloadStatus = `Download failed: ${e}`;
+ } finally {
+ this.isDownloadingJava = false;
+ }
+ }
+
+ async cancelDownload() {
+ try {
+ await invoke("cancel_java_download");
+ this.isDownloadingJava = false;
+ this.javaDownloadStatus = "Download cancelled";
+ this.downloadProgress = null;
+ await this.loadPendingDownloads();
+ } catch (e) {
+ console.error("Failed to cancel download:", e);
+ }
+ }
+
+ async resumeDownloads() {
+ if (this.pendingDownloads.length === 0) return;
+
+ this.isDownloadingJava = true;
+ this.javaDownloadStatus = "Resuming download...";
+
+ try {
+ const installed = await invoke<JavaInstallation[]>("resume_java_downloads");
+ if (installed.length > 0) {
+ this.settings.java_path = installed[0].path;
+ await this.detectJava();
+ uiState.setStatus(`Resumed and installed ${installed.length} Java version(s)`);
+ }
+ await this.loadPendingDownloads();
+ } catch (e) {
+ console.error("Failed to resume downloads:", e);
+ this.javaDownloadStatus = `Resume failed: ${e}`;
+ } finally {
+ this.isDownloadingJava = false;
+ }
+ }
+
+ // Format bytes to human readable
+ formatBytes(bytes: number): string {
+ if (bytes === 0) return "0 B";
+ const k = 1024;
+ const sizes = ["B", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
+ }
+
+ // Format seconds to human readable
+ formatTime(seconds: number): string {
+ if (seconds === 0 || !isFinite(seconds)) return "--";
+ if (seconds < 60) return `${Math.round(seconds)}s`;
+ if (seconds < 3600) {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.round(seconds % 60);
+ return `${mins}m ${secs}s`;
+ }
+ const hours = Math.floor(seconds / 3600);
+ const mins = Math.floor((seconds % 3600) / 60);
+ return `${hours}h ${mins}m`;
+ }
+
+ // Format date string
+ formatDate(dateStr: string | null): string {
+ if (!dateStr) return "--";
+ try {
+ const date = new Date(dateStr);
+ return date.toLocaleDateString("en-US", {
+ year: "2-digit",
+ month: "2-digit",
+ day: "2-digit",
+ });
+ } catch {
+ return "--";
+ }
+ }
+
+ // Legacy compatibility
+ get availableJavaVersions(): number[] {
+ return this.availableMajorVersions;
+ }
}
export const settingsState = new SettingsState();