diff options
Diffstat (limited to 'packages/ui-new/src')
69 files changed, 0 insertions, 10305 deletions
diff --git a/packages/ui-new/src/client.ts b/packages/ui-new/src/client.ts deleted file mode 100644 index 18d2377..0000000 --- a/packages/ui-new/src/client.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import type { - Account, - DeviceCodeResponse, - FabricGameVersion, - FabricLoaderEntry, - FabricLoaderVersion, - FileInfo, - ForgeVersion, - GithubRelease, - InstalledFabricVersion, - InstalledForgeVersion, - InstalledVersion, - Instance, - JavaCatalog, - JavaDownloadInfo, - JavaInstallation, - LauncherConfig, - Message, - MigrationResult, - ModelInfo, - PastebinResponse, - PendingJavaDownload, - Version, - VersionMetadata, -} from "@/types"; - -export function assistantChat(messages: Message[]): Promise<Message> { - return invoke<Message>("assistant_chat", { - messages, - }); -} - -export function assistantChatStream(messages: Message[]): Promise<string> { - return invoke<string>("assistant_chat_stream", { - messages, - }); -} - -export function assistantCheckHealth(): Promise<boolean> { - return invoke<boolean>("assistant_check_health"); -} - -export function cancelJavaDownload(): Promise<void> { - return invoke<void>("cancel_java_download"); -} - -export function checkVersionInstalled( - instanceId: string, - versionId: string, -): Promise<boolean> { - return invoke<boolean>("check_version_installed", { - instanceId, - versionId, - }); -} - -export function completeMicrosoftLogin(deviceCode: string): Promise<Account> { - return invoke<Account>("complete_microsoft_login", { - deviceCode, - }); -} - -export function createInstance(name: string): Promise<Instance> { - return invoke<Instance>("create_instance", { - name, - }); -} - -export function deleteInstance(instanceId: string): Promise<void> { - return invoke<void>("delete_instance", { - instanceId, - }); -} - -export function deleteInstanceFile(path: string): Promise<void> { - return invoke<void>("delete_instance_file", { - path, - }); -} - -export function deleteVersion( - instanceId: string, - versionId: string, -): Promise<void> { - return invoke<void>("delete_version", { - instanceId, - versionId, - }); -} - -export function detectAllJavaInstallations(): Promise<JavaInstallation[]> { - return invoke<JavaInstallation[]>("detect_all_java_installations"); -} - -export function detectJava(): Promise<JavaInstallation[]> { - return invoke<JavaInstallation[]>("detect_java"); -} - -export function downloadAdoptiumJava( - majorVersion: number, - imageType: string, - customPath: string | null, -): Promise<JavaInstallation> { - return invoke<JavaInstallation>("download_adoptium_java", { - majorVersion, - imageType, - customPath, - }); -} - -export function duplicateInstance( - instanceId: string, - newName: string, -): Promise<Instance> { - return invoke<Instance>("duplicate_instance", { - instanceId, - newName, - }); -} - -export function fetchAdoptiumJava( - majorVersion: number, - imageType: string, -): Promise<JavaDownloadInfo> { - return invoke<JavaDownloadInfo>("fetch_adoptium_java", { - majorVersion, - imageType, - }); -} - -export function fetchAvailableJavaVersions(): Promise<number[]> { - return invoke<number[]>("fetch_available_java_versions"); -} - -export function fetchJavaCatalog(): Promise<JavaCatalog> { - return invoke<JavaCatalog>("fetch_java_catalog"); -} - -export function getActiveAccount(): Promise<Account | null> { - return invoke<Account | null>("get_active_account"); -} - -export function getActiveInstance(): Promise<Instance | null> { - return invoke<Instance | null>("get_active_instance"); -} - -export function getConfigPath(): Promise<string> { - return invoke<string>("get_config_path"); -} - -export function getFabricGameVersions(): Promise<FabricGameVersion[]> { - return invoke<FabricGameVersion[]>("get_fabric_game_versions"); -} - -export function getFabricLoaderVersions(): Promise<FabricLoaderVersion[]> { - return invoke<FabricLoaderVersion[]>("get_fabric_loader_versions"); -} - -export function getFabricLoadersForVersion( - gameVersion: string, -): Promise<FabricLoaderEntry[]> { - return invoke<FabricLoaderEntry[]>("get_fabric_loaders_for_version", { - gameVersion, - }); -} - -export function getForgeGameVersions(): Promise<string[]> { - return invoke<string[]>("get_forge_game_versions"); -} - -export function getForgeVersionsForGame( - gameVersion: string, -): Promise<ForgeVersion[]> { - return invoke<ForgeVersion[]>("get_forge_versions_for_game", { - gameVersion, - }); -} - -export function getGithubReleases(): Promise<GithubRelease[]> { - return invoke<GithubRelease[]>("get_github_releases"); -} - -export function getInstance(instanceId: string): Promise<Instance> { - return invoke<Instance>("get_instance", { - instanceId, - }); -} - -export function getPendingJavaDownloads(): Promise<PendingJavaDownload[]> { - return invoke<PendingJavaDownload[]>("get_pending_java_downloads"); -} - -export function getRecommendedJava( - requiredMajorVersion: number | null, -): Promise<JavaInstallation | null> { - return invoke<JavaInstallation | null>("get_recommended_java", { - requiredMajorVersion, - }); -} - -export function getSettings(): Promise<LauncherConfig> { - return invoke<LauncherConfig>("get_settings"); -} - -export function getVersionJavaVersion( - instanceId: string, - versionId: string, -): Promise<number | null> { - return invoke<number | null>("get_version_java_version", { - instanceId, - versionId, - }); -} - -export function getVersionMetadata( - instanceId: string, - versionId: string, -): Promise<VersionMetadata> { - return invoke<VersionMetadata>("get_version_metadata", { - instanceId, - versionId, - }); -} - -export function getVersions(): Promise<Version[]> { - return invoke<Version[]>("get_versions"); -} - -export function getVersionsOfInstance(instanceId: string): Promise<Version[]> { - return invoke<Version[]>("get_versions_of_instance", { - instanceId, - }); -} - -export function installFabric( - instanceId: string, - gameVersion: string, - loaderVersion: string, -): Promise<InstalledFabricVersion> { - return invoke<InstalledFabricVersion>("install_fabric", { - instanceId, - gameVersion, - loaderVersion, - }); -} - -export function installForge( - instanceId: string, - gameVersion: string, - forgeVersion: string, -): Promise<InstalledForgeVersion> { - return invoke<InstalledForgeVersion>("install_forge", { - instanceId, - gameVersion, - forgeVersion, - }); -} - -export function installVersion( - instanceId: string, - versionId: string, -): Promise<void> { - return invoke<void>("install_version", { - instanceId, - versionId, - }); -} - -export function isFabricInstalled( - instanceId: string, - gameVersion: string, - loaderVersion: string, -): Promise<boolean> { - return invoke<boolean>("is_fabric_installed", { - instanceId, - gameVersion, - loaderVersion, - }); -} - -export function listInstalledFabricVersions( - instanceId: string, -): Promise<string[]> { - return invoke<string[]>("list_installed_fabric_versions", { - instanceId, - }); -} - -export function listInstalledVersions( - instanceId: string, -): Promise<InstalledVersion[]> { - return invoke<InstalledVersion[]>("list_installed_versions", { - instanceId, - }); -} - -export function listInstanceDirectory( - instanceId: string, - folder: string, -): Promise<FileInfo[]> { - return invoke<FileInfo[]>("list_instance_directory", { - instanceId, - folder, - }); -} - -export function listInstances(): Promise<Instance[]> { - return invoke<Instance[]>("list_instances"); -} - -export function listOllamaModels(endpoint: string): Promise<ModelInfo[]> { - return invoke<ModelInfo[]>("list_ollama_models", { - endpoint, - }); -} - -export function listOpenaiModels(): Promise<ModelInfo[]> { - return invoke<ModelInfo[]>("list_openai_models"); -} - -export function loginOffline(username: string): Promise<Account> { - return invoke<Account>("login_offline", { - username, - }); -} - -export function logout(): Promise<void> { - return invoke<void>("logout"); -} - -export function migrateSharedCaches(): Promise<MigrationResult> { - return invoke<MigrationResult>("migrate_shared_caches"); -} - -export function openFileExplorer(path: string): Promise<void> { - return invoke<void>("open_file_explorer", { - path, - }); -} - -export function readRawConfig(): Promise<string> { - return invoke<string>("read_raw_config"); -} - -export function refreshAccount(): Promise<Account> { - return invoke<Account>("refresh_account"); -} - -export function refreshJavaCatalog(): Promise<JavaCatalog> { - return invoke<JavaCatalog>("refresh_java_catalog"); -} - -export function resumeJavaDownloads(): Promise<JavaInstallation[]> { - return invoke<JavaInstallation[]>("resume_java_downloads"); -} - -export function saveRawConfig(content: string): Promise<void> { - return invoke<void>("save_raw_config", { - content, - }); -} - -export function saveSettings(config: LauncherConfig): Promise<void> { - return invoke<void>("save_settings", { - config, - }); -} - -export function setActiveInstance(instanceId: string): Promise<void> { - return invoke<void>("set_active_instance", { - instanceId, - }); -} - -export function startGame( - instanceId: string, - versionId: string, -): Promise<string> { - return invoke<string>("start_game", { - instanceId, - versionId, - }); -} - -export function startMicrosoftLogin(): Promise<DeviceCodeResponse> { - return invoke<DeviceCodeResponse>("start_microsoft_login"); -} - -export function updateInstance(instance: Instance): Promise<void> { - return invoke<void>("update_instance", { - instance, - }); -} - -export function uploadToPastebin(content: string): Promise<PastebinResponse> { - return invoke<PastebinResponse>("upload_to_pastebin", { - content, - }); -} diff --git a/packages/ui-new/src/components/bottom-bar.tsx b/packages/ui-new/src/components/bottom-bar.tsx deleted file mode 100644 index 32eb852..0000000 --- a/packages/ui-new/src/components/bottom-bar.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { Play, User } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { listInstalledVersions, startGame } from "@/client"; -import { cn } from "@/lib/utils"; -import { useAuthStore } from "@/models/auth"; -import { useInstancesStore } from "@/models/instances"; -import { useGameStore } from "@/stores/game-store"; -import { LoginModal } from "./login-modal"; -import { Button } from "./ui/button"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "./ui/select"; - -interface InstalledVersion { - id: string; - type: string; -} - -export function BottomBar() { - const authStore = useAuthStore(); - const gameStore = useGameStore(); - const instancesStore = useInstancesStore(); - - const [selectedVersion, setSelectedVersion] = useState<string | null>(null); - const [installedVersions, setInstalledVersions] = useState< - InstalledVersion[] - >([]); - const [isLoadingVersions, setIsLoadingVersions] = useState(true); - const [showLoginModal, setShowLoginModal] = useState(false); - - const loadInstalledVersions = useCallback(async () => { - if (!instancesStore.activeInstance) { - setInstalledVersions([]); - setIsLoadingVersions(false); - return; - } - - setIsLoadingVersions(true); - try { - const versions = await listInstalledVersions( - instancesStore.activeInstance.id, - ); - - const installed = versions || []; - setInstalledVersions(installed); - - // If no version is selected but we have installed versions, select the first one - if (!gameStore.selectedVersion && installed.length > 0) { - gameStore.setSelectedVersion(installed[0].id); - } - } catch (error) { - console.error("Failed to load installed versions:", error); - } finally { - setIsLoadingVersions(false); - } - }, [ - instancesStore.activeInstance, - gameStore.selectedVersion, - gameStore.setSelectedVersion, - ]); - - useEffect(() => { - loadInstalledVersions(); - - // Listen for backend events that should refresh installed versions. - let unlistenDownload: UnlistenFn | null = null; - let unlistenVersionDeleted: UnlistenFn | null = null; - - (async () => { - try { - unlistenDownload = await listen("download-complete", () => { - loadInstalledVersions(); - }); - } catch (err) { - // best-effort: do not break UI if listening fails - // eslint-disable-next-line no-console - console.warn("Failed to attach download-complete listener:", err); - } - - try { - unlistenVersionDeleted = await listen("version-deleted", () => { - loadInstalledVersions(); - }); - } catch (err) { - // eslint-disable-next-line no-console - console.warn("Failed to attach version-deleted listener:", err); - } - })(); - - return () => { - try { - if (unlistenDownload) unlistenDownload(); - } catch { - // ignore - } - try { - if (unlistenVersionDeleted) unlistenVersionDeleted(); - } catch { - // ignore - } - }; - }, [loadInstalledVersions]); - - const handleStartGame = async () => { - if (!selectedVersion) { - toast.info("Please select a version!"); - return; - } - - if (!instancesStore.activeInstance) { - toast.info("Please select an instance first!"); - return; - } - // await gameStore.startGame( - // authStore.currentAccount, - // authStore.openLoginModal, - // instancesStore.activeInstanceId, - // uiStore.setView, - // ); - await startGame(instancesStore.activeInstance?.id, selectedVersion); - }; - - const getVersionTypeColor = (type: string) => { - switch (type) { - case "release": - return "bg-emerald-500"; - case "snapshot": - return "bg-amber-500"; - case "old_beta": - return "bg-rose-500"; - case "old_alpha": - return "bg-violet-500"; - default: - return "bg-gray-500"; - } - }; - - const versionOptions = useMemo( - () => - installedVersions.map((v) => ({ - label: `${v.id}${v.type !== "release" ? ` (${v.type})` : ""}`, - value: v.id, - type: v.type, - })), - [installedVersions], - ); - - return ( - <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/30 via-transparent to-transparent p-4 z-10"> - <div className="max-w-7xl mx-auto"> - <div className="flex items-center justify-between bg-white/5 dark:bg-black/20 backdrop-blur-xl border border-white/10 dark:border-white/5 p-3 shadow-lg"> - <div className="flex items-center gap-4"> - <div className="flex flex-col"> - <span className="text-xs font-mono text-zinc-400 uppercase tracking-wider"> - Active Instance - </span> - <span className="text-sm font-medium text-white"> - {instancesStore.activeInstance?.name || "No instance selected"} - </span> - </div> - - <Select - items={versionOptions} - onValueChange={setSelectedVersion} - disabled={isLoadingVersions} - > - <SelectTrigger className="max-w-48"> - <SelectValue - placeholder={ - isLoadingVersions - ? "Loading versions..." - : "Please select a version" - } - /> - </SelectTrigger> - <SelectContent> - <SelectGroup> - {versionOptions.map((item) => ( - <SelectItem - key={item.value} - value={item.value} - className={getVersionTypeColor(item.type)} - > - {item.label} - </SelectItem> - ))} - </SelectGroup> - </SelectContent> - </Select> - </div> - - <div className="flex items-center gap-3"> - {authStore.account ? ( - <Button - className={cn( - "px-4 py-2 shadow-xl", - "bg-emerald-600! hover:bg-emerald-500!", - )} - size="lg" - onClick={handleStartGame} - > - <Play /> - Start - </Button> - ) : ( - <Button - className="px-4 py-2" - size="lg" - onClick={() => setShowLoginModal(true)} - > - <User /> Login - </Button> - )} - </div> - </div> - </div> - - <LoginModal - open={showLoginModal} - onOpenChange={() => setShowLoginModal(false)} - /> - </div> - ); -} diff --git a/packages/ui-new/src/components/config-editor.tsx b/packages/ui-new/src/components/config-editor.tsx deleted file mode 100644 index 129b8f7..0000000 --- a/packages/ui-new/src/components/config-editor.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import type React from "react"; -import { useEffect, useState } from "react"; -import { type ZodType, z } from "zod"; -import { useSettingsStore } from "@/models/settings"; -import type { LauncherConfig } from "@/types"; -import { Button } from "./ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "./ui/dialog"; -import { FieldError } from "./ui/field"; -import { Spinner } from "./ui/spinner"; -import { Textarea } from "./ui/textarea"; - -const launcherConfigSchema: ZodType<LauncherConfig> = z.object({ - minMemory: z.number(), - maxMemory: z.number(), - javaPath: z.string(), - width: z.number(), - height: z.number(), - downloadThreads: z.number(), - customBackgroundPath: z.string().nullable(), - enableGpuAcceleration: z.boolean(), - enableVisualEffects: z.boolean(), - activeEffect: z.string(), - theme: z.string(), - logUploadService: z.string(), - pastebinApiKey: z.string().nullable(), - assistant: z.any(), // TODO: AssistantConfig schema - useSharedCaches: z.boolean(), - keepLegacyPerInstanceStorage: z.boolean(), - featureFlags: z.any(), // TODO: FeatureFlags schema -}); - -export interface ConfigEditorProps - extends Omit<React.ComponentPropsWithoutRef<typeof Dialog>, "onOpenChange"> { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function ConfigEditor({ onOpenChange, ...props }: ConfigEditorProps) { - const settings = useSettingsStore(); - - const [errorMessage, setErrorMessage] = useState<string | null>(null); - const [rawConfigContent, setRawConfigContent] = useState( - JSON.stringify(settings.config, null, 2), - ); - const [isSaving, setIsSaving] = useState(false); - - useEffect(() => { - setRawConfigContent(JSON.stringify(settings.config, null, 2)); - }, [settings.config]); - - const handleSave = async () => { - setIsSaving(true); - setErrorMessage(null); - try { - const validatedConfig = launcherConfigSchema.parse( - JSON.parse(rawConfigContent), - ); - settings.config = validatedConfig; - await settings.save(); - onOpenChange?.(false); - } catch (error) { - setErrorMessage(error instanceof Error ? error.message : String(error)); - } finally { - setIsSaving(false); - } - }; - - return ( - <Dialog onOpenChange={onOpenChange} {...props}> - <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Edit Configuration</DialogTitle> - <DialogDescription> - Edit the raw JSON configuration file. - </DialogDescription> - </DialogHeader> - - <Textarea - value={rawConfigContent} - onChange={(e) => setRawConfigContent(e.target.value)} - className="font-mono text-sm h-100 resize-none" - spellCheck={false} - aria-invalid={!!errorMessage} - /> - - {errorMessage && <FieldError errors={[{ message: errorMessage }]} />} - - <DialogFooter> - <Button - variant="outline" - onClick={() => onOpenChange?.(false)} - disabled={isSaving} - > - Cancel - </Button> - <Button onClick={handleSave} disabled={isSaving}> - {isSaving && <Spinner />} - Save Changes - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/ui-new/src/components/download-monitor.tsx b/packages/ui-new/src/components/download-monitor.tsx deleted file mode 100644 index f3902d9..0000000 --- a/packages/ui-new/src/components/download-monitor.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { X } from "lucide-react"; -import { useState } from "react"; - -export function DownloadMonitor() { - const [isVisible, setIsVisible] = useState(true); - - if (!isVisible) return null; - - return ( - <div className="bg-zinc-900/80 backdrop-blur-md border border-zinc-700 rounded-lg shadow-2xl overflow-hidden"> - {/* Header */} - <div className="flex items-center justify-between px-4 py-3 bg-zinc-800/50 border-b border-zinc-700"> - <div className="flex items-center gap-2"> - <div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div> - <span className="text-sm font-medium text-white">Downloads</span> - </div> - <button - type="button" - onClick={() => setIsVisible(false)} - className="text-zinc-400 hover:text-white transition-colors p-1" - > - <X size={16} /> - </button> - </div> - - {/* Content */} - <div className="p-4"> - <div className="space-y-3"> - {/* Download Item */} - <div className="space-y-1"> - <div className="flex justify-between text-xs"> - <span className="text-zinc-300">Minecraft 1.20.4</span> - <span className="text-zinc-400">65%</span> - </div> - <div className="h-1.5 bg-zinc-800 rounded-full overflow-hidden"> - <div - className="h-full bg-emerald-500 rounded-full transition-all duration-300" - style={{ width: "65%" }} - ></div> - </div> - <div className="flex justify-between text-[10px] text-zinc-500"> - <span>142 MB / 218 MB</span> - <span>2.1 MB/s • 36s remaining</span> - </div> - </div> - - {/* Download Item */} - <div className="space-y-1"> - <div className="flex justify-between text-xs"> - <span className="text-zinc-300">Java 17</span> - <span className="text-zinc-400">100%</span> - </div> - <div className="h-1.5 bg-zinc-800 rounded-full overflow-hidden"> - <div className="h-full bg-emerald-500 rounded-full"></div> - </div> - <div className="text-[10px] text-emerald-400">Completed</div> - </div> - </div> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/components/game-console.tsx b/packages/ui-new/src/components/game-console.tsx deleted file mode 100644 index 6980c8c..0000000 --- a/packages/ui-new/src/components/game-console.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Copy, Download, Filter, Search, Trash2, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useLogsStore } from "@/stores/logs-store"; -import { useUIStore } from "@/stores/ui-store"; - -export function GameConsole() { - const uiStore = useUIStore(); - const logsStore = useLogsStore(); - - const [searchTerm, setSearchTerm] = useState(""); - const [selectedLevels, setSelectedLevels] = useState<Set<string>>( - new Set(["info", "warn", "error", "debug", "fatal"]), - ); - const [autoScroll, setAutoScroll] = useState(true); - const consoleEndRef = useRef<HTMLDivElement>(null); - const logsContainerRef = useRef<HTMLDivElement>(null); - - const levelColors: Record<string, string> = { - info: "text-blue-400", - warn: "text-amber-400", - error: "text-red-400", - debug: "text-purple-400", - fatal: "text-rose-400", - }; - - const levelBgColors: Record<string, string> = { - info: "bg-blue-400/10", - warn: "bg-amber-400/10", - error: "bg-red-400/10", - debug: "bg-purple-400/10", - fatal: "bg-rose-400/10", - }; - - // Filter logs based on search term and selected levels - const filteredLogs = logsStore.logs.filter((log) => { - const matchesSearch = - searchTerm === "" || - log.message.toLowerCase().includes(searchTerm.toLowerCase()) || - log.source.toLowerCase().includes(searchTerm.toLowerCase()); - - const matchesLevel = selectedLevels.has(log.level); - - return matchesSearch && matchesLevel; - }); - - // Auto-scroll to bottom when new logs arrive or autoScroll is enabled - useEffect(() => { - if (autoScroll && consoleEndRef.current && filteredLogs.length > 0) { - consoleEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [filteredLogs, autoScroll]); - - // Handle keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Ctrl/Cmd + K to focus search - if ((e.ctrlKey || e.metaKey) && e.key === "k") { - e.preventDefault(); - // Focus search input - const searchInput = document.querySelector( - 'input[type="text"]', - ) as HTMLInputElement; - if (searchInput) searchInput.focus(); - } - // Escape to close console - if (e.key === "Escape") { - uiStore.toggleConsole(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [uiStore.toggleConsole]); - - const toggleLevel = (level: string) => { - const newLevels = new Set(selectedLevels); - if (newLevels.has(level)) { - newLevels.delete(level); - } else { - newLevels.add(level); - } - setSelectedLevels(newLevels); - }; - - const handleCopyAll = () => { - const logsText = logsStore.exportLogs(filteredLogs); - navigator.clipboard.writeText(logsText); - }; - - const handleExport = () => { - const logsText = logsStore.exportLogs(filteredLogs); - const blob = new Blob([logsText], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `dropout_logs_${new Date().toISOString().replace(/[:.]/g, "-")}.txt`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const handleClear = () => { - logsStore.clear(); - }; - - return ( - <> - {/* Header */} - <div className="flex items-center justify-between p-4 border-b border-zinc-700 bg-[#252526]"> - <div className="flex items-center gap-3"> - <h2 className="text-lg font-bold text-white">Game Console</h2> - <div className="flex items-center gap-1"> - <span className="text-xs text-zinc-400">Logs:</span> - <span className="text-xs font-medium text-emerald-400"> - {filteredLogs.length} - </span> - <span className="text-xs text-zinc-400">/</span> - <span className="text-xs text-zinc-400"> - {logsStore.logs.length} - </span> - </div> - </div> - <button - type="button" - onClick={() => uiStore.toggleConsole()} - className="p-2 text-zinc-400 hover:text-white transition-colors" - > - <X size={20} /> - </button> - </div> - - {/* Toolbar */} - <div className="flex items-center gap-3 p-3 border-b border-zinc-700 bg-[#2D2D30]"> - {/* Search */} - <div className="relative flex-1"> - <Search - className="absolute left-3 top-1/2 transform -translate-y-1/2 text-zinc-500" - size={16} - /> - <input - type="text" - value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} - placeholder="Search logs..." - className="w-full pl-10 pr-4 py-2 bg-[#3E3E42] border border-zinc-600 rounded text-sm text-white placeholder:text-zinc-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" - /> - {searchTerm && ( - <button - type="button" - onClick={() => setSearchTerm("")} - className="absolute right-3 top-1/2 transform -translate-y-1/2 text-zinc-400 hover:text-white" - > - × - </button> - )} - </div> - - {/* Level Filters */} - <div className="flex items-center gap-1"> - {Object.entries(levelColors).map(([level, colorClass]) => ( - <button - type="button" - key={level} - onClick={() => toggleLevel(level)} - className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${ - selectedLevels.has(level) - ? `${levelBgColors[level]} ${colorClass}` - : "bg-[#3E3E42] text-zinc-400 hover:text-white" - }`} - > - {level.toUpperCase()} - </button> - ))} - </div> - - {/* Actions */} - <div className="flex items-center gap-1"> - <button - type="button" - onClick={handleCopyAll} - className="p-2 text-zinc-400 hover:text-white transition-colors" - title="Copy all logs" - > - <Copy size={16} /> - </button> - <button - type="button" - onClick={handleExport} - className="p-2 text-zinc-400 hover:text-white transition-colors" - title="Export logs" - > - <Download size={16} /> - </button> - <button - type="button" - onClick={handleClear} - className="p-2 text-zinc-400 hover:text-white transition-colors" - title="Clear logs" - > - <Trash2 size={16} /> - </button> - </div> - - {/* Auto-scroll Toggle */} - <div className="flex items-center gap-2 pl-2 border-l border-zinc-700"> - <label className="inline-flex items-center cursor-pointer"> - <input - type="checkbox" - checked={autoScroll} - onChange={(e) => setAutoScroll(e.target.checked)} - className="sr-only peer" - /> - <div className="relative w-9 h-5 bg-zinc-700 peer-focus:outline-none peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div> - <span className="ml-2 text-xs text-zinc-400">Auto-scroll</span> - </label> - </div> - </div> - - {/* Logs Container */} - <div - ref={logsContainerRef} - className="flex-1 overflow-y-auto font-mono text-sm bg-[#1E1E1E]" - style={{ fontFamily: "'Cascadia Code', 'Consolas', monospace" }} - > - {filteredLogs.length === 0 ? ( - <div className="flex items-center justify-center h-full"> - <div className="text-center text-zinc-500"> - <Filter className="mx-auto mb-2" size={24} /> - <p>No logs match the current filters</p> - </div> - </div> - ) : ( - <div className="p-4 space-y-1"> - {filteredLogs.map((log) => ( - <div - key={log.id} - className="group hover:bg-white/5 p-2 rounded transition-colors" - > - <div className="flex items-start gap-3"> - <div - className={`px-2 py-0.5 rounded text-xs font-medium ${levelBgColors[log.level]} ${levelColors[log.level]}`} - > - {log.level.toUpperCase()} - </div> - <div className="text-zinc-400 text-xs shrink-0"> - {log.timestamp} - </div> - <div className="text-amber-300 text-xs shrink-0"> - [{log.source}] - </div> - <div className="text-gray-300 flex-1">{log.message}</div> - </div> - </div> - ))} - <div ref={consoleEndRef} /> - </div> - )} - </div> - - {/* Footer */} - <div className="flex items-center justify-between p-3 border-t border-zinc-700 bg-[#252526] text-xs text-zinc-400"> - <div className="flex items-center gap-4"> - <div> - <span>Total: </span> - <span className="text-white">{logsStore.logs.length}</span> - <span> | Filtered: </span> - <span className="text-emerald-400">{filteredLogs.length}</span> - </div> - <div className="flex items-center gap-2"> - <kbd className="px-1.5 py-0.5 bg-[#3E3E42] rounded text-xs"> - Ctrl+K - </kbd> - <span>to search</span> - </div> - </div> - <div> - <span>Updated: </span> - <span> - {new Date().toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - })} - </span> - </div> - </div> - </> - ); -} diff --git a/packages/ui-new/src/components/instance-creation-modal.tsx b/packages/ui-new/src/components/instance-creation-modal.tsx deleted file mode 100644 index 8a2b1b4..0000000 --- a/packages/ui-new/src/components/instance-creation-modal.tsx +++ /dev/null @@ -1,552 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { Loader2, Search } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useInstancesStore } from "@/models/instances"; -import { useGameStore } from "@/stores/game-store"; -import type { Version } from "@/types/bindings/manifest"; -import type { FabricLoaderEntry } from "../types/bindings/fabric"; -import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge"; -import type { Instance } from "../types/bindings/instance"; - -interface Props { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function InstanceCreationModal({ open, onOpenChange }: Props) { - const gameStore = useGameStore(); - const instancesStore = useInstancesStore(); - - // Steps: 1 = name, 2 = version, 3 = mod loader - const [step, setStep] = useState<number>(1); - - // Step 1 - const [instanceName, setInstanceName] = useState<string>(""); - - // Step 2 - const [versionSearch, setVersionSearch] = useState<string>(""); - const [versionFilter, setVersionFilter] = useState< - "all" | "release" | "snapshot" - >("release"); - const [selectedVersionUI, setSelectedVersionUI] = useState<Version | null>( - null, - ); - - // Step 3 - const [modLoaderType, setModLoaderType] = useState< - "vanilla" | "fabric" | "forge" - >("vanilla"); - const [fabricLoaders, setFabricLoaders] = useState<FabricLoaderEntry[]>([]); - const [forgeVersions, setForgeVersions] = useState<ForgeVersionEntry[]>([]); - const [selectedFabricLoader, setSelectedFabricLoader] = useState<string>(""); - const [selectedForgeLoader, setSelectedForgeLoader] = useState<string>(""); - const [loadingLoaders, setLoadingLoaders] = useState(false); - - const loadModLoaders = useCallback(async () => { - if (!selectedVersionUI) return; - setLoadingLoaders(true); - setFabricLoaders([]); - setForgeVersions([]); - try { - if (modLoaderType === "fabric") { - const loaders = await invoke<FabricLoaderEntry[]>( - "get_fabric_loaders_for_version", - { - gameVersion: selectedVersionUI.id, - }, - ); - setFabricLoaders(loaders || []); - if (loaders && loaders.length > 0) { - setSelectedFabricLoader(loaders[0].loader.version); - } else { - setSelectedFabricLoader(""); - } - } else if (modLoaderType === "forge") { - const versions = await invoke<ForgeVersionEntry[]>( - "get_forge_versions_for_game", - { - gameVersion: selectedVersionUI.id, - }, - ); - setForgeVersions(versions || []); - if (versions && versions.length > 0) { - // Binding `ForgeVersion` uses `version` (not `id`) — use `.version` here. - setSelectedForgeLoader(versions[0].version); - } else { - setSelectedForgeLoader(""); - } - } - } catch (e) { - console.error("Failed to load mod loaders:", e); - toast.error("Failed to fetch mod loader versions"); - } finally { - setLoadingLoaders(false); - } - }, [modLoaderType, selectedVersionUI]); - - // When entering step 3 and a base version exists, fetch loaders if needed - useEffect(() => { - if (step === 3 && modLoaderType !== "vanilla" && selectedVersionUI) { - loadModLoaders(); - } - }, [step, modLoaderType, selectedVersionUI, loadModLoaders]); - - // Creating state - const [creating, setCreating] = useState(false); - const [errorMessage, setErrorMessage] = useState<string>(""); - - // Derived filtered versions - const filteredVersions = useMemo(() => { - const all = gameStore.versions || []; - let list = all.slice(); - if (versionFilter !== "all") { - list = list.filter((v) => v.type === versionFilter); - } - if (versionSearch.trim()) { - const q = versionSearch.trim().toLowerCase().replace(/。/g, "."); - list = list.filter((v) => v.id.toLowerCase().includes(q)); - } - return list; - }, [gameStore.versions, versionFilter, versionSearch]); - - // Reset when opened/closed - useEffect(() => { - if (open) { - // ensure versions are loaded - gameStore.loadVersions(); - setStep(1); - setInstanceName(""); - setVersionSearch(""); - setVersionFilter("release"); - setSelectedVersionUI(null); - setModLoaderType("vanilla"); - setFabricLoaders([]); - setForgeVersions([]); - setSelectedFabricLoader(""); - setSelectedForgeLoader(""); - setErrorMessage(""); - setCreating(false); - } - }, [open, gameStore.loadVersions]); - - function validateStep1(): boolean { - if (!instanceName.trim()) { - setErrorMessage("Please enter an instance name"); - return false; - } - setErrorMessage(""); - return true; - } - - function validateStep2(): boolean { - if (!selectedVersionUI) { - setErrorMessage("Please select a Minecraft version"); - return false; - } - setErrorMessage(""); - return true; - } - - async function handleNext() { - setErrorMessage(""); - if (step === 1) { - if (!validateStep1()) return; - setStep(2); - } else if (step === 2) { - if (!validateStep2()) return; - setStep(3); - } - } - - function handleBack() { - setErrorMessage(""); - setStep((s) => Math.max(1, s - 1)); - } - - async function handleCreate() { - if (!validateStep1() || !validateStep2()) return; - setCreating(true); - setErrorMessage(""); - - try { - // Step 1: create instance - const instance = await invoke<Instance>("create_instance", { - name: instanceName.trim(), - }); - - // If selectedVersion provided, install it - if (selectedVersionUI) { - try { - await invoke("install_version", { - instanceId: instance.id, - versionId: selectedVersionUI.id, - }); - } catch (err) { - console.error("Failed to install base version:", err); - // continue - instance created but version install failed - toast.error( - `Failed to install version ${selectedVersionUI.id}: ${String(err)}`, - ); - } - } - - // If mod loader selected, install it - if (modLoaderType === "fabric" && selectedFabricLoader) { - try { - await invoke("install_fabric", { - instanceId: instance.id, - gameVersion: selectedVersionUI?.id ?? "", - loaderVersion: selectedFabricLoader, - }); - } catch (err) { - console.error("Failed to install Fabric:", err); - toast.error(`Failed to install Fabric: ${String(err)}`); - } - } else if (modLoaderType === "forge" && selectedForgeLoader) { - try { - await invoke("install_forge", { - instanceId: instance.id, - gameVersion: selectedVersionUI?.id ?? "", - installerVersion: selectedForgeLoader, - }); - } catch (err) { - console.error("Failed to install Forge:", err); - toast.error(`Failed to install Forge: ${String(err)}`); - } - } - - // Refresh instances list - await instancesStore.refresh(); - - toast.success("Instance created successfully"); - onOpenChange(false); - } catch (e) { - console.error("Failed to create instance:", e); - setErrorMessage(String(e)); - toast.error(`Failed to create instance: ${e}`); - } finally { - setCreating(false); - } - } - - // UI pieces - const StepIndicator = () => ( - <div className="flex gap-2 w-full"> - <div - className={`flex-1 h-1 rounded ${step >= 1 ? "bg-indigo-500" : "bg-zinc-700"}`} - /> - <div - className={`flex-1 h-1 rounded ${step >= 2 ? "bg-indigo-500" : "bg-zinc-700"}`} - /> - <div - className={`flex-1 h-1 rounded ${step >= 3 ? "bg-indigo-500" : "bg-zinc-700"}`} - /> - </div> - ); - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="w-full max-w-3xl max-h-[90vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Create New Instance</DialogTitle> - <DialogDescription> - Multi-step wizard — create an instance and optionally install a - version or mod loader. - </DialogDescription> - </DialogHeader> - - <div className="px-6"> - <div className="pt-4 pb-6"> - <StepIndicator /> - </div> - - {/* Step 1 - Name */} - {step === 1 && ( - <div className="space-y-4"> - <div> - <label - htmlFor="instance-name" - className="block text-sm font-medium mb-2" - > - Instance Name - </label> - <Input - id="instance-name" - placeholder="My Minecraft Instance" - value={instanceName} - onChange={(e) => setInstanceName(e.target.value)} - disabled={creating} - /> - </div> - <p className="text-xs text-muted-foreground"> - Give your instance a memorable name. - </p> - </div> - )} - - {/* Step 2 - Version selection */} - {step === 2 && ( - <div className="space-y-4"> - <div className="flex gap-3"> - <div className="relative flex-1"> - <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" /> - <Input - value={versionSearch} - onChange={(e) => setVersionSearch(e.target.value)} - placeholder="Search versions..." - className="pl-9" - /> - </div> - - <div className="flex gap-2"> - <Button - type="button" - variant={versionFilter === "all" ? "default" : "outline"} - onClick={() => setVersionFilter("all")} - > - All - </Button> - <Button - type="button" - variant={ - versionFilter === "release" ? "default" : "outline" - } - onClick={() => setVersionFilter("release")} - > - Release - </Button> - <Button - type="button" - variant={ - versionFilter === "snapshot" ? "default" : "outline" - } - onClick={() => setVersionFilter("snapshot")} - > - Snapshot - </Button> - </div> - </div> - - <ScrollArea className="max-h-[36vh]"> - <div className="space-y-2 py-2"> - {gameStore.versions.length === 0 ? ( - <div className="flex items-center justify-center py-8 text-muted-foreground"> - <Loader2 className="animate-spin mr-2" /> - Loading versions... - </div> - ) : filteredVersions.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground"> - No matching versions found - </div> - ) : ( - filteredVersions.map((v) => { - const isSelected = selectedVersionUI?.id === v.id; - return ( - <button - key={v.id} - type="button" - onClick={() => setSelectedVersionUI(v)} - className={`w-full text-left p-3 rounded-lg border transition-colors ${ - isSelected - ? "bg-indigo-50 dark:bg-indigo-600/20 border-indigo-200" - : "bg-white/40 dark:bg-white/5 border-black/5 dark:border-white/5 hover:bg-white/60" - }`} - > - <div className="flex items-center justify-between"> - <div> - <div className="font-mono font-bold">{v.id}</div> - <div className="text-xs text-muted-foreground mt-1"> - {v.type}{" "} - {v.releaseTime - ? ` • ${new Date(v.releaseTime).toLocaleDateString()}` - : ""} - </div> - </div> - {v.javaVersion && ( - <div className="text-sm"> - Java {v.javaVersion} - </div> - )} - </div> - </button> - ); - }) - )} - </div> - </ScrollArea> - </div> - )} - - {/* Step 3 - Mod loader */} - {step === 3 && ( - <div className="space-y-4"> - <div> - <div className="text-sm font-medium mb-2">Mod Loader Type</div> - <div className="flex gap-3"> - <Button - type="button" - variant={ - modLoaderType === "vanilla" ? "default" : "outline" - } - onClick={() => setModLoaderType("vanilla")} - > - Vanilla - </Button> - <Button - type="button" - variant={modLoaderType === "fabric" ? "default" : "outline"} - onClick={() => setModLoaderType("fabric")} - > - Fabric - </Button> - <Button - type="button" - variant={modLoaderType === "forge" ? "default" : "outline"} - onClick={() => setModLoaderType("forge")} - > - Forge - </Button> - </div> - </div> - - {modLoaderType === "fabric" && ( - <div> - {loadingLoaders ? ( - <div className="flex items-center gap-2"> - <Loader2 className="animate-spin" /> - Loading Fabric versions... - </div> - ) : fabricLoaders.length > 0 ? ( - <div className="space-y-2"> - <select - value={selectedFabricLoader} - onChange={(e) => - setSelectedFabricLoader(e.target.value) - } - className="w-full px-3 py-2 rounded border bg-transparent" - > - {fabricLoaders.map((f) => ( - <option - key={f.loader.version} - value={f.loader.version} - > - {f.loader.version}{" "} - {f.loader.stable ? "(Stable)" : "(Beta)"} - </option> - ))} - </select> - </div> - ) : ( - <p className="text-sm text-muted-foreground"> - No Fabric loaders available for this version - </p> - )} - </div> - )} - - {modLoaderType === "forge" && ( - <div> - {loadingLoaders ? ( - <div className="flex items-center gap-2"> - <Loader2 className="animate-spin" /> - Loading Forge versions... - </div> - ) : forgeVersions.length > 0 ? ( - <div className="space-y-2"> - <select - value={selectedForgeLoader} - onChange={(e) => setSelectedForgeLoader(e.target.value)} - className="w-full px-3 py-2 rounded border bg-transparent" - > - {forgeVersions.map((f) => ( - // binding ForgeVersion uses `version` as the identifier - <option key={f.version} value={f.version}> - {f.version} - </option> - ))} - </select> - </div> - ) : ( - <p className="text-sm text-muted-foreground"> - No Forge versions available for this version - </p> - )} - </div> - )} - </div> - )} - - {errorMessage && ( - <div className="text-sm text-red-400 mt-3">{errorMessage}</div> - )} - </div> - - <DialogFooter> - <div className="w-full flex justify-between items-center"> - <div> - <Button - type="button" - variant="ghost" - onClick={() => { - // cancel - onOpenChange(false); - }} - disabled={creating} - > - Cancel - </Button> - </div> - - <div className="flex gap-2"> - {step > 1 && ( - <Button - type="button" - variant="outline" - onClick={handleBack} - disabled={creating} - > - Back - </Button> - )} - - {step < 3 ? ( - <Button type="button" onClick={handleNext} disabled={creating}> - Next - </Button> - ) : ( - <Button - type="button" - onClick={handleCreate} - disabled={creating} - > - {creating ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Creating... - </> - ) : ( - "Create" - )} - </Button> - )} - </div> - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} - -export default InstanceCreationModal; diff --git a/packages/ui-new/src/components/instance-editor-modal.tsx b/packages/ui-new/src/components/instance-editor-modal.tsx deleted file mode 100644 index f880c20..0000000 --- a/packages/ui-new/src/components/instance-editor-modal.tsx +++ /dev/null @@ -1,548 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { Folder, Loader2, Save, Trash2, X } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; - -import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/models/instances"; -import { useSettingsStore } from "@/models/settings"; -import type { FileInfo } from "../types/bindings/core"; -import type { Instance } from "../types/bindings/instance"; - -type Props = { - open: boolean; - instance: Instance | null; - onOpenChange: (open: boolean) => void; -}; - -export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { - const instancesStore = useInstancesStore(); - const { config } = useSettingsStore(); - - const [activeTab, setActiveTab] = useState< - "info" | "version" | "files" | "settings" - >("info"); - const [saving, setSaving] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); - - // Info tab fields - const [editName, setEditName] = useState(""); - const [editNotes, setEditNotes] = useState(""); - - // Files tab state - const [selectedFileFolder, setSelectedFileFolder] = useState< - "mods" | "resourcepacks" | "shaderpacks" | "saves" | "screenshots" - >("mods"); - const [fileList, setFileList] = useState<FileInfo[]>([]); - const [loadingFiles, setLoadingFiles] = useState(false); - const [deletingPath, setDeletingPath] = useState<string | null>(null); - - // Version tab state (placeholder - the Svelte implementation used a ModLoaderSelector component) - // React versions-view/instance-creation handle mod loader installs; here we show basic current info. - - // Settings tab fields - const [editMemoryMin, setEditMemoryMin] = useState<number>(0); - const [editMemoryMax, setEditMemoryMax] = useState<number>(0); - const [editJavaArgs, setEditJavaArgs] = useState<string>(""); - - // initialize when open & instance changes - useEffect(() => { - if (open && instance) { - setActiveTab("info"); - setSaving(false); - setErrorMessage(""); - setEditName(instance.name || ""); - setEditNotes(instance.notes ?? ""); - setEditMemoryMin( - (instance.memoryOverride && toNumber(instance.memoryOverride.min)) ?? - config?.minMemory ?? - 512, - ); - setEditMemoryMax( - (instance.memoryOverride && toNumber(instance.memoryOverride.max)) ?? - config?.maxMemory ?? - 2048, - ); - setEditJavaArgs(instance.jvmArgsOverride ?? ""); - setFileList([]); - setSelectedFileFolder("mods"); - } - }, [open, instance, config?.minMemory, config?.maxMemory]); - - // load files when switching to files tab - const loadFileList = useCallback( - async ( - folder: - | "mods" - | "resourcepacks" - | "shaderpacks" - | "saves" - | "screenshots", - ) => { - if (!instance) return; - setLoadingFiles(true); - try { - const files = await invoke<FileInfo[]>("list_instance_directory", { - instanceId: instance.id, - folder, - }); - setFileList(files || []); - } catch (err) { - console.error("Failed to load files:", err); - toast.error("Failed to load files: " + String(err)); - setFileList([]); - } finally { - setLoadingFiles(false); - } - }, - [instance], - ); - - useEffect(() => { - if (open && instance && activeTab === "files") { - // explicitly pass the selected folder so loadFileList doesn't rely on stale closures - loadFileList(selectedFileFolder); - } - }, [activeTab, open, instance, selectedFileFolder, loadFileList]); - - async function changeFolder( - folder: "mods" | "resourcepacks" | "shaderpacks" | "saves" | "screenshots", - ) { - setSelectedFileFolder(folder); - // reload the list for the newly selected folder - if (open && instance) await loadFileList(folder); - } - - async function deleteFile(filePath: string) { - if ( - !confirm( - `Are you sure you want to delete "${filePath.split("/").pop()}"?`, - ) - ) { - return; - } - setDeletingPath(filePath); - try { - await invoke("delete_instance_file", { path: filePath }); - // refresh the currently selected folder - await loadFileList(selectedFileFolder); - toast.success("Deleted"); - } catch (err) { - console.error("Failed to delete file:", err); - toast.error("Failed to delete file: " + String(err)); - } finally { - setDeletingPath(null); - } - } - - async function openInExplorer(filePath: string) { - try { - await invoke("open_file_explorer", { path: filePath }); - } catch (err) { - console.error("Failed to open in explorer:", err); - toast.error("Failed to open file explorer: " + String(err)); - } - } - - async function saveChanges() { - if (!instance) return; - if (!editName.trim()) { - setErrorMessage("Instance name cannot be empty"); - return; - } - setSaving(true); - setErrorMessage(""); - try { - // Build updated instance shape compatible with backend - const updatedInstance: Instance = { - ...instance, - name: editName.trim(), - // some bindings may use camelCase; set optional string fields to null when empty - notes: editNotes.trim() ? editNotes.trim() : null, - memoryOverride: { - min: editMemoryMin, - max: editMemoryMax, - }, - jvmArgsOverride: editJavaArgs.trim() ? editJavaArgs.trim() : null, - }; - - await instancesStore.update(updatedInstance as Instance); - toast.success("Instance saved"); - onOpenChange(false); - } catch (err) { - console.error("Failed to save instance:", err); - setErrorMessage(String(err)); - toast.error("Failed to save instance: " + String(err)); - } finally { - setSaving(false); - } - } - - function formatFileSize(bytesBig: FileInfo["size"]): string { - const bytes = Number(bytesBig ?? 0); - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`; - } - - function formatDate( - tsBig?: - | FileInfo["modified"] - | Instance["createdAt"] - | Instance["lastPlayed"], - ) { - if (tsBig === undefined || tsBig === null) return ""; - const n = toNumber(tsBig); - // tsrs bindings often use seconds for createdAt/lastPlayed; if value looks like seconds use *1000 - const maybeMs = n > 1e12 ? n : n * 1000; - return new Date(maybeMs).toLocaleDateString(); - } - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="w-full max-w-4xl max-h-[90vh] overflow-hidden"> - <DialogHeader> - <div className="flex items-center justify-between gap-4"> - <div> - <DialogTitle>Edit Instance</DialogTitle> - <DialogDescription>{instance?.name ?? ""}</DialogDescription> - </div> - <div className="flex items-center gap-2"> - <button - type="button" - onClick={() => onOpenChange(false)} - disabled={saving} - className="p-2 rounded hover:bg-zinc-800 text-zinc-400" - aria-label="Close" - > - <X /> - </button> - </div> - </div> - </DialogHeader> - - {/* Tab Navigation */} - <div className="flex gap-1 px-6 pt-2 border-b border-zinc-700"> - {[ - { id: "info", label: "Info" }, - { id: "version", label: "Version" }, - { id: "files", label: "Files" }, - { id: "settings", label: "Settings" }, - ].map((tab) => ( - <button - type="button" - key={tab.id} - onClick={() => - setActiveTab( - tab.id as "info" | "version" | "files" | "settings", - ) - } - className={`px-4 py-2 text-sm font-medium transition-colors rounded-t-lg ${ - activeTab === tab.id - ? "bg-indigo-600 text-white" - : "bg-zinc-800 text-zinc-400 hover:text-white" - }`} - > - {tab.label} - </button> - ))} - </div> - - {/* Content */} - <div className="p-6 overflow-y-auto max-h-[60vh]"> - {activeTab === "info" && ( - <div className="space-y-4"> - <div> - <label - htmlFor="instance-name-edit" - className="block text-sm font-medium mb-2" - > - Instance Name - </label> - <Input - id="instance-name-edit" - value={editName} - onChange={(e) => setEditName(e.target.value)} - disabled={saving} - /> - </div> - - <div> - <label - htmlFor="instance-notes-edit" - className="block text-sm font-medium mb-2" - > - Notes - </label> - <Textarea - id="instance-notes-edit" - value={editNotes} - onChange={(e) => setEditNotes(e.target.value)} - rows={4} - disabled={saving} - /> - </div> - - <div className="grid grid-cols-2 gap-4 text-sm"> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Created</p> - <p className="text-white font-medium"> - {instance?.createdAt ? formatDate(instance.createdAt) : "-"} - </p> - </div> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Last Played</p> - <p className="text-white font-medium"> - {instance?.lastPlayed - ? formatDate(instance.lastPlayed) - : "Never"} - </p> - </div> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Game Directory</p> - <p - className="text-white font-medium text-xs truncate" - title={instance?.gameDir ?? ""} - > - {instance?.gameDir - ? String(instance.gameDir).split("/").pop() - : ""} - </p> - </div> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Current Version</p> - <p className="text-white font-medium"> - {instance?.versionId ?? "None"} - </p> - </div> - </div> - </div> - )} - - {activeTab === "version" && ( - <div className="space-y-4"> - {instance?.versionId ? ( - <div className="p-4 bg-indigo-500/10 border border-indigo-500/30 rounded-lg"> - <p className="text-sm text-indigo-400"> - Currently playing:{" "} - <span className="font-medium">{instance.versionId}</span> - {instance.modLoader && ( - <> - {" "} - with{" "} - <span className="capitalize">{instance.modLoader}</span> - {instance.modLoaderVersion - ? ` ${instance.modLoaderVersion}` - : ""} - </> - )} - </p> - </div> - ) : ( - <div className="text-sm text-zinc-400"> - No version selected for this instance - </div> - )} - - <div> - <p className="text-sm font-medium mb-2"> - Change Version / Mod Loader - </p> - <p className="text-xs text-zinc-400"> - Use the Versions page to install new game versions or mod - loaders, then set them here. - </p> - </div> - </div> - )} - - {activeTab === "files" && ( - <div className="space-y-4"> - <div className="flex gap-2 flex-wrap"> - {( - [ - "mods", - "resourcepacks", - "shaderpacks", - "saves", - "screenshots", - ] as const - ).map((folder) => ( - <button - type="button" - key={folder} - onClick={() => changeFolder(folder)} - className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${ - selectedFileFolder === folder - ? "bg-indigo-600 text-white" - : "bg-zinc-800 text-zinc-400 hover:text-white" - }`} - > - {folder} - </button> - ))} - </div> - - {loadingFiles ? ( - <div className="flex items-center gap-2 text-zinc-400 py-8 justify-center"> - <Loader2 className="animate-spin" /> - Loading files... - </div> - ) : fileList.length === 0 ? ( - <div className="text-center py-8 text-zinc-500"> - No files in this folder - </div> - ) : ( - <div className="space-y-2"> - {fileList.map((file) => ( - <div - key={file.path} - className="flex items-center justify-between p-3 bg-zinc-800 rounded-lg hover:bg-zinc-700 transition-colors" - > - <div className="flex-1 min-w-0"> - <p className="font-medium text-white truncate"> - {file.name} - </p> - <p className="text-xs text-zinc-400"> - {file.isDirectory - ? "Folder" - : formatFileSize(file.size)}{" "} - • {formatDate(file.modified)} - </p> - </div> - <div className="flex gap-2 ml-4"> - <button - type="button" - onClick={() => openInExplorer(file.path)} - title="Open in explorer" - className="p-2 rounded-lg hover:bg-zinc-600 text-zinc-400 hover:text-white transition-colors" - > - <Folder /> - </button> - <button - type="button" - onClick={() => deleteFile(file.path)} - disabled={deletingPath === file.path} - title="Delete" - className="p-2 rounded-lg hover:bg-red-600/20 text-red-400 hover:text-red-300 transition-colors disabled:opacity-50" - > - {deletingPath === file.path ? ( - <Loader2 className="animate-spin" /> - ) : ( - <Trash2 /> - )} - </button> - </div> - </div> - ))} - </div> - )} - </div> - )} - - {activeTab === "settings" && ( - <div className="space-y-4"> - <div> - <label - htmlFor="min-memory-edit" - className="block text-sm font-medium mb-2" - > - Minimum Memory (MB) - </label> - <Input - id="min-memory-edit" - type="number" - value={String(editMemoryMin)} - onChange={(e) => setEditMemoryMin(Number(e.target.value))} - disabled={saving} - /> - <p className="text-xs text-zinc-400 mt-1"> - Default: {config?.minMemory} MB - </p> - </div> - - <div> - <label - htmlFor="max-memory-edit" - className="block text-sm font-medium mb-2" - > - Maximum Memory (MB) - </label> - <Input - id="max-memory-edit" - type="number" - value={String(editMemoryMax)} - onChange={(e) => setEditMemoryMax(Number(e.target.value))} - disabled={saving} - /> - <p className="text-xs text-zinc-400 mt-1"> - Default: {config?.maxMemory} MB - </p> - </div> - - <div> - <label - htmlFor="jvm-args-edit" - className="block text-sm font-medium mb-2" - > - JVM Arguments (Advanced) - </label> - <Textarea - id="jvm-args-edit" - value={editJavaArgs} - onChange={(e) => setEditJavaArgs(e.target.value)} - rows={4} - disabled={saving} - /> - </div> - </div> - )} - </div> - - {errorMessage && ( - <div className="px-6 text-sm text-red-400">{errorMessage}</div> - )} - - <DialogFooter> - <div className="flex items-center justify-between w-full"> - <div /> - <div className="flex gap-2"> - <Button - variant="outline" - onClick={() => { - onOpenChange(false); - }} - > - Cancel - </Button> - <Button onClick={saveChanges} disabled={saving}> - {saving ? ( - <Loader2 className="animate-spin mr-2" /> - ) : ( - <Save className="mr-2" /> - )} - Save - </Button> - </div> - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} - -export default InstanceEditorModal; diff --git a/packages/ui-new/src/components/login-modal.tsx b/packages/ui-new/src/components/login-modal.tsx deleted file mode 100644 index 49596da..0000000 --- a/packages/ui-new/src/components/login-modal.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { Mail, User } from "lucide-react"; -import { useCallback, useState } from "react"; -import { toast } from "sonner"; -import { useAuthStore } from "@/models/auth"; -import { Button } from "./ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "./ui/dialog"; -import { - Field, - FieldDescription, - FieldError, - FieldGroup, - FieldLabel, -} from "./ui/field"; -import { Input } from "./ui/input"; - -export interface LoginModalProps - extends Omit<React.ComponentPropsWithoutRef<typeof Dialog>, "onOpenChange"> { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function LoginModal({ onOpenChange, ...props }: LoginModalProps) { - const authStore = useAuthStore(); - - const [offlineUsername, setOfflineUsername] = useState<string>(""); - const [errorMessage, setErrorMessage] = useState<string>(""); - const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false); - - const handleMicrosoftLogin = useCallback(async () => { - setIsLoggingIn(true); - authStore.setLoginMode("microsoft"); - try { - await authStore.loginOnline(() => onOpenChange?.(false)); - } catch (error) { - const err = error as Error; - console.error("Failed to login with Microsoft:", err); - setErrorMessage(err.message); - } finally { - setIsLoggingIn(false); - } - }, [authStore.loginOnline, authStore.setLoginMode, onOpenChange]); - - const handleOfflineLogin = useCallback(async () => { - setIsLoggingIn(true); - try { - await authStore.loginOffline(offlineUsername); - toast.success("Logged in offline successfully"); - onOpenChange?.(false); - } catch (error) { - const err = error as Error; - console.error("Failed to login offline:", err); - setErrorMessage(err.message); - } finally { - setIsLoggingIn(false); - } - }, [authStore, offlineUsername, onOpenChange]); - - return ( - <Dialog onOpenChange={onOpenChange} {...props}> - <DialogContent className="md:max-w-md"> - <DialogHeader> - <DialogTitle>Login</DialogTitle> - <DialogDescription> - Login to your Minecraft account or play offline - </DialogDescription> - </DialogHeader> - <div className="p-4 w-full overflow-hidden"> - {!authStore.loginMode && ( - <div className="flex flex-col space-y-4"> - <Button size="lg" onClick={handleMicrosoftLogin}> - <Mail /> - Login with Microsoft - </Button> - <Button - variant="secondary" - onClick={() => authStore.setLoginMode("offline")} - size="lg" - > - <User /> - Login Offline - </Button> - </div> - )} - {authStore.loginMode === "microsoft" && ( - <div className="flex flex-col space-y-4"> - <button - type="button" - className="text-4xl font-bold text-center bg-accent p-4 cursor-pointer" - onClick={() => { - if (authStore.deviceCode?.userCode) { - navigator.clipboard?.writeText( - authStore.deviceCode?.userCode, - ); - toast.success("Copied to clipboard"); - } - }} - > - {authStore.deviceCode?.userCode} - </button> - <span className="text-muted-foreground w-full overflow-hidden text-ellipsis"> - To sign in, use a web browser to open the page{" "} - <a href={authStore.deviceCode?.verificationUri}> - {authStore.deviceCode?.verificationUri} - </a>{" "} - and enter the code{" "} - <code - className="font-semibold cursor-pointer" - onClick={() => { - if (authStore.deviceCode?.userCode) { - navigator.clipboard?.writeText( - authStore.deviceCode?.userCode, - ); - } - }} - onKeyDown={() => { - if (authStore.deviceCode?.userCode) { - navigator.clipboard?.writeText( - authStore.deviceCode?.userCode, - ); - } - }} - > - {authStore.deviceCode?.userCode} - </code>{" "} - to authenticate, this code will be expired in{" "} - {authStore.deviceCode?.expiresIn} seconds. - </span> - <FieldError>{errorMessage}</FieldError> - </div> - )} - {authStore.loginMode === "offline" && ( - <FieldGroup> - <Field> - <FieldLabel>Username</FieldLabel> - <FieldDescription> - Enter a username to play offline - </FieldDescription> - <Input - value={offlineUsername} - onChange={(e) => { - setOfflineUsername(e.target.value); - setErrorMessage(""); - }} - aria-invalid={!!errorMessage} - /> - <FieldError>{errorMessage}</FieldError> - </Field> - </FieldGroup> - )} - </div> - <DialogFooter> - <div className="flex flex-col justify-center items-center"> - <span className="text-xs text-muted-foreground "> - {authStore.statusMessage} - </span> - </div> - <Button - variant="outline" - onClick={() => { - if (authStore.loginMode) { - if (authStore.loginMode === "microsoft") { - authStore.cancelLoginOnline(); - } - authStore.setLoginMode(null); - } else { - onOpenChange?.(false); - } - }} - > - Cancel - </Button> - {authStore.loginMode === "offline" && ( - <Button onClick={handleOfflineLogin} disabled={isLoggingIn}> - Login - </Button> - )} - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/ui-new/src/components/particle-background.tsx b/packages/ui-new/src/components/particle-background.tsx deleted file mode 100644 index 2e0b15a..0000000 --- a/packages/ui-new/src/components/particle-background.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useRef } from "react"; -import { SaturnEffect } from "../lib/effects/SaturnEffect"; - -export function ParticleBackground() { - const canvasRef = useRef<HTMLCanvasElement | null>(null); - const effectRef = useRef<SaturnEffect | null>(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - // Instantiate SaturnEffect and attach to canvas - let effect: SaturnEffect | null = null; - try { - effect = new SaturnEffect(canvas); - effectRef.current = effect; - } catch (err) { - // If effect fails, silently degrade (keep background blank) - // eslint-disable-next-line no-console - console.warn("SaturnEffect initialization failed:", err); - } - - const resizeHandler = () => { - if (effectRef.current) { - try { - effectRef.current.resize(window.innerWidth, window.innerHeight); - } catch { - // ignore - } - } - }; - - window.addEventListener("resize", resizeHandler); - - // Expose getter for HomeView interactions (getSaturnEffect) - // HomeView will call window.getSaturnEffect()?.handleMouseDown/Move/Up - ( - window as unknown as { getSaturnEffect?: () => SaturnEffect | null } - ).getSaturnEffect = () => effectRef.current; - - return () => { - window.removeEventListener("resize", resizeHandler); - if (effectRef.current) { - try { - effectRef.current.destroy(); - } catch { - // ignore - } - } - effectRef.current = null; - ( - window as unknown as { getSaturnEffect?: () => SaturnEffect | null } - ).getSaturnEffect = undefined; - }; - }, []); - - return ( - <canvas - ref={canvasRef} - className="absolute inset-0 z-0 pointer-events-none" - /> - ); -} diff --git a/packages/ui-new/src/components/sidebar.tsx b/packages/ui-new/src/components/sidebar.tsx deleted file mode 100644 index 0147b0a..0000000 --- a/packages/ui-new/src/components/sidebar.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { Folder, Home, LogOutIcon, Settings } from "lucide-react"; -import { useLocation, useNavigate } from "react-router"; -import { cn } from "@/lib/utils"; -import { useAuthStore } from "@/models/auth"; -import { Button } from "./ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, -} from "./ui/dropdown-menu"; -import { UserAvatar } from "./user-avatar"; - -interface NavItemProps { - Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; - label: string; - to: string; -} - -function NavItem({ Icon, label, to }: NavItemProps) { - const navigate = useNavigate(); - const location = useLocation(); - const isActive = location.pathname === to; - - const handleClick = () => { - navigate(to); - }; - - return ( - <Button - variant="ghost" - className={cn( - "w-fit lg:w-full justify-center lg:justify-start", - isActive && "relative bg-accent", - )} - size="lg" - onClick={handleClick} - > - <Icon className="size-5" strokeWidth={isActive ? 2.5 : 2} /> - <span className="hidden lg:block text-sm relative z-10">{label}</span> - {isActive && ( - <div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-black dark:bg-white rounded-r-full hidden lg:block"></div> - )} - </Button> - ); -} - -export function Sidebar() { - const authStore = useAuthStore(); - - return ( - <aside - className={cn( - "flex flex-col items-center lg:items-start", - "bg-sidebar transition-all duration-300", - "w-20 lg:w-64 shrink-0 py-6 h-full", - )} - > - {/* Logo Area */} - <div className="h-16 w-full flex items-center justify-center lg:justify-start lg:px-6 mb-6"> - {/* Icon Logo (Small) */} - <div className="lg:hidden text-black dark:text-white"> - <svg - width="32" - height="32" - viewBox="0 0 100 100" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <title>Logo</title> - <path - d="M25 25 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M25 75 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M50 50 L75 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <circle cx="25" cy="25" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="50" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="75" r="8" fill="currentColor" stroke="none" /> - <circle cx="50" cy="50" r="10" fill="currentColor" stroke="none" /> - <circle cx="75" cy="50" r="8" fill="currentColor" stroke="none" /> - </svg> - </div> - {/* Full Logo (Large) */} - <div className="hidden lg:flex items-center gap-3 font-bold text-xl tracking-tighter dark:text-white text-black"> - <svg - width="42" - height="42" - viewBox="0 0 100 100" - fill="none" - xmlns="http://www.w3.org/2000/svg" - className="shrink-0" - > - <title>Logo</title> - <path - d="M25 25 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M25 75 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M50 50 L75 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - - <circle cx="25" cy="25" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="50" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="75" r="8" fill="currentColor" stroke="none" /> - - <circle - cx="50" - cy="25" - r="7" - stroke="currentColor" - strokeWidth="2" - strokeDasharray="4 2" - fill="none" - className="opacity-30" - /> - <circle - cx="50" - cy="75" - r="7" - stroke="currentColor" - strokeWidth="2" - strokeDasharray="4 2" - fill="none" - className="opacity-30" - /> - <circle cx="50" cy="50" r="10" fill="currentColor" stroke="none" /> - <circle cx="75" cy="50" r="8" fill="currentColor" stroke="none" /> - </svg> - - <span>DROPOUT</span> - </div> - </div> - - <nav className="w-full flex flex-col space-y-1 px-3 items-center"> - <NavItem Icon={Home} label="Overview" to="/" /> - <NavItem Icon={Folder} label="Instances" to="/instances" /> - <NavItem Icon={Settings} label="Settings" to="/settings" /> - </nav> - - <div className="flex-1 flex flex-col justify-end"> - <DropdownMenu> - <DropdownMenuTrigger render={<UserAvatar />}> - Open - </DropdownMenuTrigger> - <DropdownMenuContent align="end" side="right" sideOffset={20}> - <DropdownMenuGroup> - <DropdownMenuItem - variant="destructive" - onClick={authStore.logout} - > - <LogOutIcon /> - Logout - </DropdownMenuItem> - </DropdownMenuGroup> - </DropdownMenuContent> - </DropdownMenu> - </div> - </aside> - ); -} diff --git a/packages/ui-new/src/components/ui/avatar.tsx b/packages/ui-new/src/components/ui/avatar.tsx deleted file mode 100644 index 9fd72a2..0000000 --- a/packages/ui-new/src/components/ui/avatar.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Avatar({ - className, - size = "default", - ...props -}: AvatarPrimitive.Root.Props & { - size?: "default" | "sm" | "lg"; -}) { - return ( - <AvatarPrimitive.Root - data-slot="avatar" - data-size={size} - className={cn( - "size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten", - className, - )} - {...props} - /> - ); -} - -function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { - return ( - <AvatarPrimitive.Image - data-slot="avatar-image" - className={cn( - "rounded-full aspect-square size-full object-cover", - className, - )} - {...props} - /> - ); -} - -function AvatarFallback({ - className, - ...props -}: AvatarPrimitive.Fallback.Props) { - return ( - <AvatarPrimitive.Fallback - data-slot="avatar-fallback" - className={cn( - "bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs", - className, - )} - {...props} - /> - ); -} - -function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { - return ( - <span - data-slot="avatar-badge" - className={cn( - "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", - "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", - className, - )} - {...props} - /> - ); -} - -function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="avatar-group" - className={cn( - "*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2", - className, - )} - {...props} - /> - ); -} - -function AvatarGroupCount({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( - <div - data-slot="avatar-group-count" - className={cn( - "bg-muted text-muted-foreground size-8 rounded-full text-xs group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", - className, - )} - {...props} - /> - ); -} - -export { - Avatar, - AvatarImage, - AvatarFallback, - AvatarGroup, - AvatarGroupCount, - AvatarBadge, -}; diff --git a/packages/ui-new/src/components/ui/badge.tsx b/packages/ui-new/src/components/ui/badge.tsx deleted file mode 100644 index 425ab9e..0000000 --- a/packages/ui-new/src/components/ui/badge.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - secondary: - "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", - destructive: - "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", - outline: - "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", - ghost: - "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", - link: "text-primary underline-offset-4 hover:underline", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function Badge({ - className, - variant = "default", - render, - ...props -}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) { - return useRender({ - defaultTagName: "span", - props: mergeProps<"span">( - { - className: cn(badgeVariants({ variant }), className), - }, - props, - ), - render, - state: { - slot: "badge", - variant, - }, - }); -} - -export { Badge, badgeVariants }; diff --git a/packages/ui-new/src/components/ui/button.tsx b/packages/ui-new/src/components/ui/button.tsx deleted file mode 100644 index 7dee494..0000000 --- a/packages/ui-new/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", - destructive: - "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", - icon: "size-8", - "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-none", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) { - return ( - <ButtonPrimitive - data-slot="button" - className={cn(buttonVariants({ variant, size, className }))} - {...props} - /> - ); -} - -export { Button, buttonVariants }; diff --git a/packages/ui-new/src/components/ui/card.tsx b/packages/ui-new/src/components/ui/card.tsx deleted file mode 100644 index b7084a0..0000000 --- a/packages/ui-new/src/components/ui/card.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Card({ - className, - size = "default", - ...props -}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { - return ( - <div - data-slot="card" - data-size={size} - className={cn( - "ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-none py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col", - className, - )} - {...props} - /> - ); -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-header" - className={cn( - "gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", - className, - )} - {...props} - /> - ); -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-title" - className={cn( - "text-sm font-medium group-data-[size=sm]/card:text-sm", - className, - )} - {...props} - /> - ); -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-description" - className={cn("text-muted-foreground text-xs/relaxed", className)} - {...props} - /> - ); -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-action" - className={cn( - "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className, - )} - {...props} - /> - ); -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-content" - className={cn("px-4 group-data-[size=sm]/card:px-3", className)} - {...props} - /> - ); -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-footer" - className={cn( - "rounded-none border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", - className, - )} - {...props} - /> - ); -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -}; diff --git a/packages/ui-new/src/components/ui/checkbox.tsx b/packages/ui-new/src/components/ui/checkbox.tsx deleted file mode 100644 index 9f22cea..0000000 --- a/packages/ui-new/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; -import { CheckIcon } from "lucide-react"; -import { cn } from "@/lib/utils"; - -function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { - return ( - <CheckboxPrimitive.Root - data-slot="checkbox" - className={cn( - "border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-none border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-1 aria-invalid:ring-1 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - > - <CheckboxPrimitive.Indicator - data-slot="checkbox-indicator" - className="[&>svg]:size-3.5 grid place-content-center text-current transition-none" - > - <CheckIcon /> - </CheckboxPrimitive.Indicator> - </CheckboxPrimitive.Root> - ); -} - -export { Checkbox }; diff --git a/packages/ui-new/src/components/ui/dialog.tsx b/packages/ui-new/src/components/ui/dialog.tsx deleted file mode 100644 index 033b47c..0000000 --- a/packages/ui-new/src/components/ui/dialog.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; - -import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; -import { XIcon } from "lucide-react"; -import type * as React from "react"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; - -function Dialog({ ...props }: DialogPrimitive.Root.Props) { - return <DialogPrimitive.Root data-slot="dialog" {...props} />; -} - -function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { - return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; -} - -function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { - return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; -} - -function DialogClose({ ...props }: DialogPrimitive.Close.Props) { - return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; -} - -function DialogOverlay({ - className, - ...props -}: DialogPrimitive.Backdrop.Props) { - return ( - <DialogPrimitive.Backdrop - data-slot="dialog-overlay" - className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", - className, - )} - {...props} - /> - ); -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: DialogPrimitive.Popup.Props & { - showCloseButton?: boolean; -}) { - return ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Popup - data-slot="dialog-content" - className={cn( - "bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-none p-4 text-xs/relaxed ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none", - className, - )} - {...props} - > - {children} - {showCloseButton && ( - <DialogPrimitive.Close - data-slot="dialog-close" - render={ - <Button - variant="ghost" - className="absolute top-2 right-2" - size="icon-sm" - /> - } - > - <XIcon /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - )} - </DialogPrimitive.Popup> - </DialogPortal> - ); -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="dialog-header" - className={cn("gap-1 text-left flex flex-col", className)} - {...props} - /> - ); -} - -function DialogFooter({ - className, - showCloseButton = false, - children, - ...props -}: React.ComponentProps<"div"> & { - showCloseButton?: boolean; -}) { - return ( - <div - data-slot="dialog-footer" - className={cn( - "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", - className, - )} - {...props} - > - {children} - {showCloseButton && ( - <DialogPrimitive.Close render={<Button variant="outline" />}> - Close - </DialogPrimitive.Close> - )} - </div> - ); -} - -function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { - return ( - <DialogPrimitive.Title - data-slot="dialog-title" - className={cn("text-sm font-medium", className)} - {...props} - /> - ); -} - -function DialogDescription({ - className, - ...props -}: DialogPrimitive.Description.Props) { - return ( - <DialogPrimitive.Description - data-slot="dialog-description" - className={cn( - "text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed *:[a]:underline *:[a]:underline-offset-3", - className, - )} - {...props} - /> - ); -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -}; diff --git a/packages/ui-new/src/components/ui/dropdown-menu.tsx b/packages/ui-new/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index ee97374..0000000 --- a/packages/ui-new/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { Menu as MenuPrimitive } from "@base-ui/react/menu"; -import { CheckIcon, ChevronRightIcon } from "lucide-react"; -import type * as React from "react"; -import { cn } from "@/lib/utils"; - -function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { - return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />; -} - -function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { - return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />; -} - -function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { - return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />; -} - -function DropdownMenuContent({ - align = "start", - alignOffset = 0, - side = "bottom", - sideOffset = 4, - className, - ...props -}: MenuPrimitive.Popup.Props & - Pick< - MenuPrimitive.Positioner.Props, - "align" | "alignOffset" | "side" | "sideOffset" - >) { - return ( - <MenuPrimitive.Portal> - <MenuPrimitive.Positioner - className="isolate z-50 outline-none" - align={align} - alignOffset={alignOffset} - side={side} - sideOffset={sideOffset} - > - <MenuPrimitive.Popup - data-slot="dropdown-menu-content" - className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-none shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden", - className, - )} - {...props} - /> - </MenuPrimitive.Positioner> - </MenuPrimitive.Portal> - ); -} - -function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { - return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />; -} - -function DropdownMenuLabel({ - className, - inset, - ...props -}: MenuPrimitive.GroupLabel.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.GroupLabel - data-slot="dropdown-menu-label" - data-inset={inset} - className={cn( - "text-muted-foreground px-2 py-2 text-xs data-inset:pl-7", - className, - )} - {...props} - /> - ); -} - -function DropdownMenuItem({ - className, - inset, - variant = "default", - ...props -}: MenuPrimitive.Item.Props & { - inset?: boolean; - variant?: "default" | "destructive"; -}) { - return ( - <MenuPrimitive.Item - data-slot="dropdown-menu-item" - data-inset={inset} - data-variant={variant} - className={cn( - "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - /> - ); -} - -function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { - return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />; -} - -function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: MenuPrimitive.SubmenuTrigger.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.SubmenuTrigger - data-slot="dropdown-menu-sub-trigger" - data-inset={inset} - className={cn( - "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 data-popup-open:bg-accent data-popup-open:text-accent-foreground flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - {children} - <ChevronRightIcon className="ml-auto" /> - </MenuPrimitive.SubmenuTrigger> - ); -} - -function DropdownMenuSubContent({ - align = "start", - alignOffset = -3, - side = "right", - sideOffset = 0, - className, - ...props -}: React.ComponentProps<typeof DropdownMenuContent>) { - return ( - <DropdownMenuContent - data-slot="dropdown-menu-sub-content" - className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-24 rounded-none shadow-lg ring-1 duration-100 w-auto", - className, - )} - align={align} - alignOffset={alignOffset} - side={side} - sideOffset={sideOffset} - {...props} - /> - ); -} - -function DropdownMenuCheckboxItem({ - className, - children, - checked, - inset, - ...props -}: MenuPrimitive.CheckboxItem.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.CheckboxItem - data-slot="dropdown-menu-checkbox-item" - data-inset={inset} - className={cn( - "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - checked={checked} - {...props} - > - <span - className="absolute right-2 flex items-center justify-center pointer-events-none" - data-slot="dropdown-menu-checkbox-item-indicator" - > - <MenuPrimitive.CheckboxItemIndicator> - <CheckIcon /> - </MenuPrimitive.CheckboxItemIndicator> - </span> - {children} - </MenuPrimitive.CheckboxItem> - ); -} - -function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { - return ( - <MenuPrimitive.RadioGroup - data-slot="dropdown-menu-radio-group" - {...props} - /> - ); -} - -function DropdownMenuRadioItem({ - className, - children, - inset, - ...props -}: MenuPrimitive.RadioItem.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.RadioItem - data-slot="dropdown-menu-radio-item" - data-inset={inset} - className={cn( - "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - <span - className="absolute right-2 flex items-center justify-center pointer-events-none" - data-slot="dropdown-menu-radio-item-indicator" - > - <MenuPrimitive.RadioItemIndicator> - <CheckIcon /> - </MenuPrimitive.RadioItemIndicator> - </span> - {children} - </MenuPrimitive.RadioItem> - ); -} - -function DropdownMenuSeparator({ - className, - ...props -}: MenuPrimitive.Separator.Props) { - return ( - <MenuPrimitive.Separator - data-slot="dropdown-menu-separator" - className={cn("bg-border -mx-1 h-px", className)} - {...props} - /> - ); -} - -function DropdownMenuShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { - return ( - <span - data-slot="dropdown-menu-shortcut" - className={cn( - "text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", - className, - )} - {...props} - /> - ); -} - -export { - DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuSubContent, -}; diff --git a/packages/ui-new/src/components/ui/field.tsx b/packages/ui-new/src/components/ui/field.tsx deleted file mode 100644 index ab9fb71..0000000 --- a/packages/ui-new/src/components/ui/field.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import { useMemo } from "react"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; - -function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { - return ( - <fieldset - data-slot="field-set" - className={cn( - "gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", - className, - )} - {...props} - /> - ); -} - -function FieldLegend({ - className, - variant = "legend", - ...props -}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { - return ( - <legend - data-slot="field-legend" - data-variant={variant} - className={cn( - "mb-2.5 font-medium data-[variant=label]:text-xs data-[variant=legend]:text-sm", - className, - )} - {...props} - /> - ); -} - -function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="field-group" - className={cn( - "gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col", - className, - )} - {...props} - /> - ); -} - -const fieldVariants = cva( - "data-[invalid=true]:text-destructive gap-2 group/field flex w-full", - { - variants: { - orientation: { - vertical: "flex-col *:w-full [&>.sr-only]:w-auto", - horizontal: - "flex-row items-center *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", - responsive: - "flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", - }, - }, - defaultVariants: { - orientation: "vertical", - }, - }, -); - -function Field({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { - return ( - <div - data-slot="field" - data-orientation={orientation} - className={cn(fieldVariants({ orientation }), className)} - {...props} - /> - ); -} - -function FieldContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="field-content" - className={cn( - "gap-0.5 group/field-content flex flex-1 flex-col leading-snug", - className, - )} - {...props} - /> - ); -} - -function FieldLabel({ - className, - ...props -}: React.ComponentProps<typeof Label>) { - return ( - <Label - data-slot="field-label" - className={cn( - "has-data-checked:bg-primary/5 has-data-checked:border-primary/30 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 gap-2 group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-none has-[>[data-slot=field]]:border *:data-[slot=field]:p-2 group/field-label peer/field-label flex w-fit leading-snug", - "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", - className, - )} - {...props} - /> - ); -} - -function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="field-label" - className={cn( - "gap-2 text-xs/relaxed group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug", - className, - )} - {...props} - /> - ); -} - -function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { - return ( - <p - data-slot="field-description" - className={cn( - "text-muted-foreground text-left text-xs/relaxed [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance", - "last:mt-0 nth-last-2:-mt-1", - "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", - className, - )} - {...props} - /> - ); -} - -function FieldSeparator({ - children, - className, - ...props -}: React.ComponentProps<"div"> & { - children?: React.ReactNode; -}) { - return ( - <div - data-slot="field-separator" - data-content={!!children} - className={cn( - "-my-2 h-5 text-xs group-data-[variant=outline]/field-group:-mb-2 relative", - className, - )} - {...props} - > - <Separator className="absolute inset-0 top-1/2" /> - {children && ( - <span - className="text-muted-foreground px-2 bg-background relative mx-auto block w-fit" - data-slot="field-separator-content" - > - {children} - </span> - )} - </div> - ); -} - -function FieldError({ - className, - children, - errors, - ...props -}: React.ComponentProps<"div"> & { - errors?: Array<{ message?: string } | undefined>; -}) { - const content = useMemo(() => { - if (children) { - return children; - } - - if (!errors?.length) { - return null; - } - - const uniqueErrors = [ - ...new Map(errors.map((error) => [error?.message, error])).values(), - ]; - - if (uniqueErrors?.length === 1) { - return uniqueErrors[0]?.message; - } - - return ( - <ul className="ml-4 flex list-disc flex-col gap-1"> - {uniqueErrors.map( - (error, index) => - error?.message && ( - <li key={`${error.message.slice(6)}-${index}`}> - {error.message} - </li> - ), - )} - </ul> - ); - }, [children, errors]); - - if (!content) { - return null; - } - - return ( - <div - role="alert" - data-slot="field-error" - className={cn("text-destructive text-xs font-normal", className)} - {...props} - > - {content} - </div> - ); -} - -export { - Field, - FieldLabel, - FieldDescription, - FieldError, - FieldGroup, - FieldLegend, - FieldSeparator, - FieldSet, - FieldContent, - FieldTitle, -}; diff --git a/packages/ui-new/src/components/ui/input.tsx b/packages/ui-new/src/components/ui/input.tsx deleted file mode 100644 index bb0390a..0000000 --- a/packages/ui-new/src/components/ui/input.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Input as InputPrimitive } from "@base-ui/react/input"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - <InputPrimitive - type={type} - data-slot="input" - className={cn( - "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-none border bg-transparent px-2.5 py-1 text-xs transition-colors file:h-6 file:text-xs file:font-medium focus-visible:ring-1 aria-invalid:ring-1 md:text-xs file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - ); -} - -export { Input }; diff --git a/packages/ui-new/src/components/ui/label.tsx b/packages/ui-new/src/components/ui/label.tsx deleted file mode 100644 index 9a998c7..0000000 --- a/packages/ui-new/src/components/ui/label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Label({ className, ...props }: React.ComponentProps<"label">) { - return ( - // biome-ignore lint/a11y/noLabelWithoutControl: shadcn component - <label - data-slot="label" - className={cn( - "gap-2 text-xs leading-none group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed", - className, - )} - {...props} - /> - ); -} - -export { Label }; diff --git a/packages/ui-new/src/components/ui/scroll-area.tsx b/packages/ui-new/src/components/ui/scroll-area.tsx deleted file mode 100644 index 4a68eb2..0000000 --- a/packages/ui-new/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; -import { cn } from "@/lib/utils"; - -function ScrollArea({ - className, - children, - ...props -}: ScrollAreaPrimitive.Root.Props) { - return ( - <ScrollAreaPrimitive.Root - data-slot="scroll-area" - className={cn("relative", className)} - {...props} - > - <ScrollAreaPrimitive.Viewport - data-slot="scroll-area-viewport" - className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" - > - {children} - </ScrollAreaPrimitive.Viewport> - <ScrollBar /> - <ScrollAreaPrimitive.Corner /> - </ScrollAreaPrimitive.Root> - ); -} - -function ScrollBar({ - className, - orientation = "vertical", - ...props -}: ScrollAreaPrimitive.Scrollbar.Props) { - return ( - <ScrollAreaPrimitive.Scrollbar - data-slot="scroll-area-scrollbar" - data-orientation={orientation} - orientation={orientation} - className={cn( - "data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none", - className, - )} - {...props} - > - <ScrollAreaPrimitive.Thumb - data-slot="scroll-area-thumb" - className="rounded-none bg-border relative flex-1" - /> - </ScrollAreaPrimitive.Scrollbar> - ); -} - -export { ScrollArea, ScrollBar }; diff --git a/packages/ui-new/src/components/ui/select.tsx b/packages/ui-new/src/components/ui/select.tsx deleted file mode 100644 index 210adba..0000000 --- a/packages/ui-new/src/components/ui/select.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { Select as SelectPrimitive } from "@base-ui/react/select"; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import type * as React from "react"; -import { cn } from "@/lib/utils"; - -const Select = SelectPrimitive.Root; - -function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { - return ( - <SelectPrimitive.Group - data-slot="select-group" - className={cn("scroll-my-1", className)} - {...props} - /> - ); -} - -function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { - return ( - <SelectPrimitive.Value - data-slot="select-value" - className={cn("flex flex-1 text-left", className)} - {...props} - /> - ); -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: SelectPrimitive.Trigger.Props & { - size?: "sm" | "default"; -}) { - return ( - <SelectPrimitive.Trigger - data-slot="select-trigger" - data-size={size} - className={cn( - "border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-none border bg-transparent py-2 pr-2 pl-2.5 text-xs transition-colors select-none focus-visible:ring-1 aria-invalid:ring-1 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - {children} - <SelectPrimitive.Icon - render={ - <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" /> - } - /> - </SelectPrimitive.Trigger> - ); -} - -function SelectContent({ - className, - children, - side = "bottom", - sideOffset = 4, - align = "center", - alignOffset = 0, - alignItemWithTrigger = true, - ...props -}: SelectPrimitive.Popup.Props & - Pick< - SelectPrimitive.Positioner.Props, - "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" - >) { - return ( - <SelectPrimitive.Portal> - <SelectPrimitive.Positioner - side={side} - sideOffset={sideOffset} - align={align} - alignOffset={alignOffset} - alignItemWithTrigger={alignItemWithTrigger} - className="isolate z-50" - > - <SelectPrimitive.Popup - data-slot="select-content" - data-align-trigger={alignItemWithTrigger} - className={cn( - "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-none shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", - className, - )} - {...props} - > - <SelectScrollUpButton /> - <SelectPrimitive.List>{children}</SelectPrimitive.List> - <SelectScrollDownButton /> - </SelectPrimitive.Popup> - </SelectPrimitive.Positioner> - </SelectPrimitive.Portal> - ); -} - -function SelectLabel({ - className, - ...props -}: SelectPrimitive.GroupLabel.Props) { - return ( - <SelectPrimitive.GroupLabel - data-slot="select-label" - className={cn("text-muted-foreground px-2 py-2 text-xs", className)} - {...props} - /> - ); -} - -function SelectItem({ - className, - children, - ...props -}: SelectPrimitive.Item.Props) { - return ( - <SelectPrimitive.Item - data-slot="select-item" - className={cn( - "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - <SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap"> - {children} - </SelectPrimitive.ItemText> - <SelectPrimitive.ItemIndicator - render={ - <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> - } - > - <CheckIcon className="pointer-events-none" /> - </SelectPrimitive.ItemIndicator> - </SelectPrimitive.Item> - ); -} - -function SelectSeparator({ - className, - ...props -}: SelectPrimitive.Separator.Props) { - return ( - <SelectPrimitive.Separator - data-slot="select-separator" - className={cn("bg-border -mx-1 h-px pointer-events-none", className)} - {...props} - /> - ); -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { - return ( - <SelectPrimitive.ScrollUpArrow - data-slot="select-scroll-up-button" - className={cn( - "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", - className, - )} - {...props} - > - <ChevronUpIcon /> - </SelectPrimitive.ScrollUpArrow> - ); -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { - return ( - <SelectPrimitive.ScrollDownArrow - data-slot="select-scroll-down-button" - className={cn( - "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", - className, - )} - {...props} - > - <ChevronDownIcon /> - </SelectPrimitive.ScrollDownArrow> - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/packages/ui-new/src/components/ui/separator.tsx b/packages/ui-new/src/components/ui/separator.tsx deleted file mode 100644 index e91a862..0000000 --- a/packages/ui-new/src/components/ui/separator.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"; - -import { cn } from "@/lib/utils"; - -function Separator({ - className, - orientation = "horizontal", - ...props -}: SeparatorPrimitive.Props) { - return ( - <SeparatorPrimitive - data-slot="separator" - orientation={orientation} - className={cn( - "bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", - className, - )} - {...props} - /> - ); -} - -export { Separator }; diff --git a/packages/ui-new/src/components/ui/sonner.tsx b/packages/ui-new/src/components/ui/sonner.tsx deleted file mode 100644 index d6e293d..0000000 --- a/packages/ui-new/src/components/ui/sonner.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - CircleCheckIcon, - InfoIcon, - Loader2Icon, - OctagonXIcon, - TriangleAlertIcon, -} from "lucide-react"; -import { useTheme } from "next-themes"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); - - return ( - <Sonner - theme={theme as ToasterProps["theme"]} - className="toaster group" - icons={{ - success: <CircleCheckIcon className="size-4" />, - info: <InfoIcon className="size-4" />, - warning: <TriangleAlertIcon className="size-4" />, - error: <OctagonXIcon className="size-4" />, - loading: <Loader2Icon className="size-4 animate-spin" />, - }} - style={ - { - "--normal-bg": "var(--popover)", - "--normal-text": "var(--popover-foreground)", - "--normal-border": "var(--border)", - "--border-radius": "var(--radius)", - } as React.CSSProperties - } - toastOptions={{ - classNames: { - toast: "cn-toast", - }, - }} - {...props} - /> - ); -}; - -export { Toaster }; diff --git a/packages/ui-new/src/components/ui/spinner.tsx b/packages/ui-new/src/components/ui/spinner.tsx deleted file mode 100644 index 91f6a63..0000000 --- a/packages/ui-new/src/components/ui/spinner.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { cn } from "@/lib/utils" -import { Loader2Icon } from "lucide-react" - -function Spinner({ className, ...props }: React.ComponentProps<"svg">) { - return ( - <Loader2Icon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} /> - ) -} - -export { Spinner } diff --git a/packages/ui-new/src/components/ui/switch.tsx b/packages/ui-new/src/components/ui/switch.tsx deleted file mode 100644 index fef14e3..0000000 --- a/packages/ui-new/src/components/ui/switch.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; - -import { cn } from "@/lib/utils"; - -function Switch({ - className, - size = "default", - ...props -}: SwitchPrimitive.Root.Props & { - size?: "sm" | "default"; -}) { - return ( - <SwitchPrimitive.Root - data-slot="switch" - data-size={size} - className={cn( - "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-1 aria-invalid:ring-1 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50", - className, - )} - {...props} - > - <SwitchPrimitive.Thumb - data-slot="switch-thumb" - className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform" - /> - </SwitchPrimitive.Root> - ); -} - -export { Switch }; diff --git a/packages/ui-new/src/components/ui/tabs.tsx b/packages/ui-new/src/components/ui/tabs.tsx deleted file mode 100644 index c66893f..0000000 --- a/packages/ui-new/src/components/ui/tabs.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -function Tabs({ - className, - orientation = "horizontal", - ...props -}: TabsPrimitive.Root.Props) { - return ( - <TabsPrimitive.Root - data-slot="tabs" - data-orientation={orientation} - className={cn( - "gap-2 group/tabs flex data-horizontal:flex-col", - className, - )} - {...props} - /> - ); -} - -const tabsListVariants = cva( - "rounded-none p-0.75 group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col", - { - variants: { - variant: { - default: "bg-muted", - line: "gap-1 bg-transparent", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function TabsList({ - className, - variant = "default", - ...props -}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) { - return ( - <TabsPrimitive.List - data-slot="tabs-list" - data-variant={variant} - className={cn(tabsListVariants({ variant }), className)} - {...props} - /> - ); -} - -function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { - return ( - <TabsPrimitive.Tab - data-slot="tabs-trigger" - className={cn( - "gap-1.5 rounded-none border border-transparent px-1.5 py-0.5 text-xs font-medium group-data-vertical/tabs:py-[calc(--spacing(1.25))] [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", - "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground", - "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:-bottom-1.25 group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", - className, - )} - {...props} - /> - ); -} - -function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) { - return ( - <TabsPrimitive.Panel - data-slot="tabs-content" - className={cn("text-xs/relaxed flex-1 outline-none", className)} - {...props} - /> - ); -} - -export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; diff --git a/packages/ui-new/src/components/ui/textarea.tsx b/packages/ui-new/src/components/ui/textarea.tsx deleted file mode 100644 index 3c3e5d0..0000000 --- a/packages/ui-new/src/components/ui/textarea.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( - <textarea - data-slot="textarea" - className={cn( - "border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 rounded-none border bg-transparent px-2.5 py-2 text-xs transition-colors focus-visible:ring-1 aria-invalid:ring-1 md:text-xs placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - ); -} - -export { Textarea }; diff --git a/packages/ui-new/src/components/user-avatar.tsx b/packages/ui-new/src/components/user-avatar.tsx deleted file mode 100644 index bbdb84c..0000000 --- a/packages/ui-new/src/components/user-avatar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useAuthStore } from "@/models/auth"; -import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "./ui/avatar"; - -export function UserAvatar({ - className, - ...props -}: React.ComponentProps<typeof Avatar>) { - const authStore = useAuthStore(); - - if (!authStore.account) { - return null; - } - - return ( - <Avatar {...props}> - <AvatarImage - src={`https://minotar.net/helm/${authStore.account.username}/100.png`} - /> - <AvatarFallback>{authStore.account.username.slice(0, 2)}</AvatarFallback> - <AvatarBadge /> - </Avatar> - ); -} diff --git a/packages/ui-new/src/index.css b/packages/ui-new/src/index.css deleted file mode 100644 index 8803e5e..0000000 --- a/packages/ui-new/src/index.css +++ /dev/null @@ -1,126 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; -@import "shadcn/tailwind.css"; - -@custom-variant dark (&:is(.dark *)); - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --radius-2xl: calc(var(--radius) + 8px); - --radius-3xl: calc(var(--radius) + 12px); - --radius-4xl: calc(var(--radius) + 16px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -:root { - --radius: 0.625rem; - --background: #f3f4f6; /* bg-gray-100 */ - --foreground: #18181b; /* zinc-900 */ - --card: #ffffff; - --card-foreground: #18181b; - --popover: #ffffff; - --popover-foreground: #18181b; - --primary: #4f46e5; /* indigo-600 */ - --primary-foreground: #ffffff; - --secondary: #f4f4f5; /* zinc-100 */ - --secondary-foreground: #18181b; - --muted: #f4f4f5; /* zinc-100 */ - --muted-foreground: #71717a; /* zinc-500 */ - --accent: #f4f4f5; /* zinc-100 */ - --accent-foreground: #18181b; - --destructive: #ef4444; /* red-500 */ - --destructive-foreground: #ffffff; - --border: #e4e4e7; /* zinc-200 */ - --input: #ffffff; - --ring: #6366f1; /* indigo-500 */ - --chart-1: #059669; /* emerald-600 */ - --chart-2: #0d9488; /* teal-600 */ - --chart-3: #4f46e5; /* indigo-600 */ - --chart-4: #7c3aed; /* violet-600 */ - --chart-5: #dc2626; /* red-600 */ - --sidebar: #ffffff; - --sidebar-foreground: #18181b; - --sidebar-primary: #4f46e5; /* indigo-600 */ - --sidebar-primary-foreground: #ffffff; - --sidebar-accent: #f4f4f5; /* zinc-100 */ - --sidebar-accent-foreground: #18181b; - --sidebar-border: #e4e4e7; /* zinc-200 */ - --sidebar-ring: #6366f1; /* indigo-500 */ -} - -.dark { - --background: #09090b; - --foreground: #fafafa; /* zinc-50 */ - --card: #18181b; /* zinc-900 */ - --card-foreground: #fafafa; - --popover: #18181b; - --popover-foreground: #fafafa; - --primary: #6366f1; /* indigo-500 */ - --primary-foreground: #ffffff; - --secondary: #27272a; /* zinc-800 */ - --secondary-foreground: #fafafa; - --muted: #27272a; /* zinc-800 */ - --muted-foreground: #a1a1aa; /* zinc-400 */ - --accent: #27272a; /* zinc-800 */ - --accent-foreground: #fafafa; - --destructive: #f87171; /* red-400 */ - --destructive-foreground: #ffffff; - --border: #3f3f46; /* zinc-700 */ - --input: rgba(255, 255, 255, 0.15); - --ring: #6366f1; /* indigo-500 */ - --chart-1: #10b981; /* emerald-500 */ - --chart-2: #06b6d4; /* cyan-500 */ - --chart-3: #6366f1; /* indigo-500 */ - --chart-4: #8b5cf6; /* violet-500 */ - --chart-5: #f87171; /* red-400 */ - --sidebar: #09090b; - --sidebar-foreground: #fafafa; - --sidebar-primary: #6366f1; /* indigo-500 */ - --sidebar-primary-foreground: #ffffff; - --sidebar-accent: #27272a; /* zinc-800 */ - --sidebar-accent-foreground: #fafafa; - --sidebar-border: #3f3f46; /* zinc-700 */ - --sidebar-ring: #6366f1; /* indigo-500 */ -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/packages/ui-new/src/lib/effects/SaturnEffect.ts b/packages/ui-new/src/lib/effects/SaturnEffect.ts deleted file mode 100644 index 497a340..0000000 --- a/packages/ui-new/src/lib/effects/SaturnEffect.ts +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Ported SaturnEffect for the React UI (ui-new). - * Adapted from the original Svelte implementation but written as a standalone - * TypeScript class that manages a 2D canvas particle effect resembling a - * rotating "Saturn" with rings. Designed to be instantiated and controlled - * from a React component (e.g. ParticleBackground). - * - * Usage: - * const effect = new SaturnEffect(canvasElement); - * effect.handleMouseDown(clientX); - * effect.handleMouseMove(clientX); - * effect.handleMouseUp(); - * // on resize: - * effect.resize(width, height); - * // on unmount: - * effect.destroy(); - */ - -export class SaturnEffect { - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - private width = 0; - private height = 0; - - // Particle storage - private xyz: Float32Array | null = null; // interleaved x,y,z - private types: Uint8Array | null = null; // 0 = planet, 1 = ring - private count = 0; - - // Animation - private animationId = 0; - private angle = 0; - private scaleFactor = 1; - - // Interaction - private isDragging = false; - private lastMouseX = 0; - private lastMouseTime = 0; - private mouseVelocities: number[] = []; - - // Speed control - private readonly baseSpeed = 0.005; - private currentSpeed = 0.005; - private rotationDirection = 1; - private readonly speedDecayRate = 0.992; - private readonly minSpeedMultiplier = 1; - private readonly maxSpeedMultiplier = 50; - private isStopped = false; - - constructor(canvas: HTMLCanvasElement) { - this.canvas = canvas; - const ctx = canvas.getContext("2d", { alpha: true, desynchronized: false }); - if (!ctx) { - throw new Error("Failed to get 2D context for SaturnEffect"); - } - this.ctx = ctx; - - // Initialize size & particles - this.resize(window.innerWidth, window.innerHeight); - this.initParticles(); - - this.animate = this.animate.bind(this); - this.animate(); - } - - // External interaction handlers (accept clientX) - handleMouseDown(clientX: number) { - this.isDragging = true; - this.lastMouseX = clientX; - this.lastMouseTime = performance.now(); - this.mouseVelocities = []; - } - - handleMouseMove(clientX: number) { - if (!this.isDragging) return; - const now = performance.now(); - const dt = now - this.lastMouseTime; - if (dt > 0) { - const dx = clientX - this.lastMouseX; - const velocity = dx / dt; - this.mouseVelocities.push(velocity); - if (this.mouseVelocities.length > 5) this.mouseVelocities.shift(); - // Rotate directly while dragging for immediate feedback - this.angle += dx * 0.002; - } - this.lastMouseX = clientX; - this.lastMouseTime = now; - } - - handleMouseUp() { - if (this.isDragging && this.mouseVelocities.length > 0) { - this.applyFlingVelocity(); - } - this.isDragging = false; - } - - handleTouchStart(clientX: number) { - this.handleMouseDown(clientX); - } - - handleTouchMove(clientX: number) { - this.handleMouseMove(clientX); - } - - handleTouchEnd() { - this.handleMouseUp(); - } - - // Resize canvas & scale (call on window resize) - resize(width: number, height: number) { - const dpr = window.devicePixelRatio || 1; - this.width = width; - this.height = height; - - // Update canvas pixel size and CSS size - this.canvas.width = Math.max(1, Math.floor(width * dpr)); - this.canvas.height = Math.max(1, Math.floor(height * dpr)); - this.canvas.style.width = `${width}px`; - this.canvas.style.height = `${height}px`; - - // Reset transform and scale for devicePixelRatio - this.ctx.setTransform(1, 0, 0, 1, 0, 0); // reset - this.ctx.scale(dpr, dpr); - - const minDim = Math.min(width, height); - this.scaleFactor = Math.max(1, minDim * 0.45); - } - - // Initialize particle arrays with reduced counts to keep performance reasonable - private initParticles() { - // Tuned particle counts for reasonable performance across platforms - const planetCount = 1000; - const ringCount = 2500; - this.count = planetCount + ringCount; - - this.xyz = new Float32Array(this.count * 3); - this.types = new Uint8Array(this.count); - - let idx = 0; - - // Planet points - for (let i = 0; i < planetCount; i++, idx++) { - const theta = Math.random() * Math.PI * 2; - const phi = Math.acos(Math.random() * 2 - 1); - const r = 1.0; - - this.xyz[idx * 3] = r * Math.sin(phi) * Math.cos(theta); - this.xyz[idx * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); - this.xyz[idx * 3 + 2] = r * Math.cos(phi); - - this.types[idx] = 0; - } - - // Ring points - const ringInner = 1.4; - const ringOuter = 2.3; - for (let i = 0; i < ringCount; i++, idx++) { - const angle = Math.random() * Math.PI * 2; - const dist = Math.sqrt( - Math.random() * (ringOuter * ringOuter - ringInner * ringInner) + - ringInner * ringInner, - ); - - this.xyz[idx * 3] = dist * Math.cos(angle); - this.xyz[idx * 3 + 1] = (Math.random() - 0.5) * 0.05; - this.xyz[idx * 3 + 2] = dist * Math.sin(angle); - - this.types[idx] = 1; - } - } - - // Map fling/velocity samples to a rotation speed and direction - private applyFlingVelocity() { - if (this.mouseVelocities.length === 0) return; - const avg = - this.mouseVelocities.reduce((a, b) => a + b, 0) / - this.mouseVelocities.length; - const flingThreshold = 0.3; - const stopThreshold = 0.1; - - if (Math.abs(avg) > flingThreshold) { - this.isStopped = false; - const newDir = avg > 0 ? 1 : -1; - if (newDir !== this.rotationDirection) this.rotationDirection = newDir; - const multiplier = Math.min( - this.maxSpeedMultiplier, - this.minSpeedMultiplier + Math.abs(avg) * 10, - ); - this.currentSpeed = this.baseSpeed * multiplier; - } else if (Math.abs(avg) < stopThreshold) { - this.isStopped = true; - this.currentSpeed = 0; - } - } - - // Main render loop - private animate() { - // Clear with full alpha to allow layering over background - this.ctx.clearRect(0, 0, this.width, this.height); - - // Standard composition - this.ctx.globalCompositeOperation = "source-over"; - - // Update rotation speed (decay) - if (!this.isDragging && !this.isStopped) { - if (this.currentSpeed > this.baseSpeed) { - this.currentSpeed = - this.baseSpeed + - (this.currentSpeed - this.baseSpeed) * this.speedDecayRate; - if (this.currentSpeed - this.baseSpeed < 0.00001) { - this.currentSpeed = this.baseSpeed; - } - } - this.angle += this.currentSpeed * this.rotationDirection; - } - - // Center positions - const cx = this.width * 0.6; - const cy = this.height * 0.5; - - // Pre-calc rotations - const rotationY = this.angle; - const rotationX = 0.4; - const rotationZ = 0.15; - - const sinY = Math.sin(rotationY); - const cosY = Math.cos(rotationY); - const sinX = Math.sin(rotationX); - const cosX = Math.cos(rotationX); - const sinZ = Math.sin(rotationZ); - const cosZ = Math.cos(rotationZ); - - const fov = 1500; - const scaleFactor = this.scaleFactor; - - if (!this.xyz || !this.types) { - this.animationId = requestAnimationFrame(this.animate); - return; - } - - // Loop particles - for (let i = 0; i < this.count; i++) { - const x = this.xyz[i * 3]; - const y = this.xyz[i * 3 + 1]; - const z = this.xyz[i * 3 + 2]; - - // Scale to screen - const px = x * scaleFactor; - const py = y * scaleFactor; - const pz = z * scaleFactor; - - // Rotate Y then X then Z - const x1 = px * cosY - pz * sinY; - const z1 = pz * cosY + px * sinY; - const y2 = py * cosX - z1 * sinX; - const z2 = z1 * cosX + py * sinX; - const x3 = x1 * cosZ - y2 * sinZ; - const y3 = y2 * cosZ + x1 * sinZ; - const z3 = z2; - - const scale = fov / (fov + z3); - - if (z3 > -fov) { - const x2d = cx + x3 * scale; - const y2d = cy + y3 * scale; - - const type = this.types[i]; - const sizeBase = type === 0 ? 2.4 : 1.5; - const size = sizeBase * scale; - - let alpha = scale * scale * scale; - if (alpha > 1) alpha = 1; - if (alpha < 0.15) continue; - - if (type === 0) { - // Planet: warm-ish - this.ctx.fillStyle = `rgba(255, 240, 220, ${alpha})`; - } else { - // Ring: cool-ish - this.ctx.fillStyle = `rgba(220, 240, 255, ${alpha})`; - } - - // Render as small rectangles (faster than arc) - this.ctx.fillRect(x2d, y2d, size, size); - } - } - - this.animationId = requestAnimationFrame(this.animate); - } - - // Stop animations and release resources - destroy() { - if (this.animationId) { - cancelAnimationFrame(this.animationId); - this.animationId = 0; - } - // Intentionally do not null out arrays to allow reuse if desired. - } -} diff --git a/packages/ui-new/src/lib/tsrs-utils.ts b/packages/ui-new/src/lib/tsrs-utils.ts deleted file mode 100644 index f48f851..0000000 --- a/packages/ui-new/src/lib/tsrs-utils.ts +++ /dev/null @@ -1,67 +0,0 @@ -export type Maybe<T> = T | null | undefined; - -export function toNumber( - value: Maybe<number | bigint | string>, - fallback = 0, -): number { - if (value === null || value === undefined) return fallback; - - if (typeof value === "number") { - if (Number.isFinite(value)) return value; - return fallback; - } - - if (typeof value === "bigint") { - // safe conversion for typical values (timestamps, sizes). Might overflow for huge bigint. - return Number(value); - } - - if (typeof value === "string") { - const n = Number(value); - return Number.isFinite(n) ? n : fallback; - } - - return fallback; -} - -/** - * Like `toNumber` but ensures non-negative result (clamps at 0). - */ -export function toNonNegativeNumber( - value: Maybe<number | bigint | string>, - fallback = 0, -): number { - const n = toNumber(value, fallback); - return n < 0 ? 0 : n; -} - -export function toDate( - value: Maybe<number | bigint | string>, - opts?: { isSeconds?: boolean }, -): Date | null { - if (value === null || value === undefined) return null; - - const isSeconds = opts?.isSeconds ?? true; - - // accept bigint, number, numeric string - const n = toNumber(value, NaN); - if (Number.isNaN(n)) return null; - - const ms = isSeconds ? Math.floor(n) * 1000 : Math.floor(n); - return new Date(ms); -} - -/** - * Convert a binding boolean-ish value (0/1, "true"/"false", boolean) to boolean. - */ -export function toBoolean(value: unknown, fallback = false): boolean { - if (value === null || value === undefined) return fallback; - if (typeof value === "boolean") return value; - if (typeof value === "number") return value !== 0; - if (typeof value === "string") { - const s = value.toLowerCase().trim(); - if (s === "true" || s === "1") return true; - if (s === "false" || s === "0") return false; - } - return fallback; -} diff --git a/packages/ui-new/src/lib/utils.ts b/packages/ui-new/src/lib/utils.ts deleted file mode 100644 index 365058c..0000000 --- a/packages/ui-new/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/ui-new/src/main.tsx b/packages/ui-new/src/main.tsx deleted file mode 100644 index a3157bd..0000000 --- a/packages/ui-new/src/main.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import "./index.css"; -import { createHashRouter, RouterProvider } from "react-router"; -import { Toaster } from "./components/ui/sonner"; -import { HomeView } from "./pages/home-view"; -import { IndexPage } from "./pages/index"; -import { InstancesView } from "./pages/instances-view"; -import { SettingsPage } from "./pages/settings"; - -const router = createHashRouter([ - { - path: "/", - element: <IndexPage />, - children: [ - { - index: true, - element: <HomeView />, - }, - { - path: "instances", - element: <InstancesView />, - }, - { - path: "settings", - element: <SettingsPage />, - }, - ], - }, -]); - -const root = createRoot(document.getElementById("root") as HTMLElement); -root.render( - <StrictMode> - <RouterProvider router={router} /> - <Toaster /> - </StrictMode>, -); diff --git a/packages/ui-new/src/models/auth.ts b/packages/ui-new/src/models/auth.ts deleted file mode 100644 index 10b2a0d..0000000 --- a/packages/ui-new/src/models/auth.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { open } from "@tauri-apps/plugin-shell"; -import { Mutex } from "es-toolkit"; -import { toString as stringify } from "es-toolkit/compat"; -import { toast } from "sonner"; -import { create } from "zustand"; -import { - completeMicrosoftLogin, - getActiveAccount, - loginOffline, - logout, - startMicrosoftLogin, -} from "@/client"; -import type { Account, DeviceCodeResponse } from "@/types"; - -export interface AuthState { - account: Account | null; - loginMode: Account["type"] | null; - deviceCode: DeviceCodeResponse | null; - _pollingInterval: number | null; - _mutex: Mutex; - statusMessage: string | null; - _progressUnlisten: UnlistenFn | null; - - init: () => Promise<void>; - setLoginMode: (mode: Account["type"] | null) => void; - loginOnline: (onSuccess?: () => void | Promise<void>) => Promise<void>; - _pollLoginStatus: ( - deviceCode: string, - onSuccess?: () => void | Promise<void>, - ) => Promise<void>; - cancelLoginOnline: () => Promise<void>; - loginOffline: (username: string) => Promise<void>; - logout: () => Promise<void>; -} - -export const useAuthStore = create<AuthState>((set, get) => ({ - account: null, - loginMode: null, - deviceCode: null, - _pollingInterval: null, - statusMessage: null, - _progressUnlisten: null, - _mutex: new Mutex(), - - init: async () => { - try { - const account = await getActiveAccount(); - set({ account }); - } catch (error) { - console.error("Failed to initialize auth store:", error); - } - }, - setLoginMode: (mode) => set({ loginMode: mode }), - loginOnline: async (onSuccess) => { - const { _pollLoginStatus } = get(); - - set({ statusMessage: "Waiting for authorization..." }); - - try { - const unlisten = await listen("auth-progress", (event) => { - const message = event.payload; - console.log(message); - set({ statusMessage: stringify(message), _progressUnlisten: unlisten }); - }); - } catch (error) { - console.warn("Failed to attch auth-progress listener:", error); - toast.warning("Failed to attch auth-progress listener"); - } - - const deviceCode = await startMicrosoftLogin(); - navigator.clipboard?.writeText(deviceCode.userCode).catch((err) => { - console.error("Failed to copy to clipboard:", err); - }); - open(deviceCode.verificationUri).catch((err) => { - console.error("Failed to open browser:", err); - }); - const ms = Number(deviceCode.interval) * 1000; - const interval = setInterval(() => { - _pollLoginStatus(deviceCode.deviceCode, onSuccess); - }, ms); - set({ _pollingInterval: interval, deviceCode }); - }, - _pollLoginStatus: async (deviceCode, onSuccess) => { - const { _pollingInterval, _mutex: mutex, _progressUnlisten } = get(); - if (mutex.isLocked) return; - mutex.acquire(); - try { - const account = await completeMicrosoftLogin(deviceCode); - clearInterval(_pollingInterval ?? undefined); - _progressUnlisten?.(); - onSuccess?.(); - set({ account, loginMode: "microsoft" }); - } catch (error) { - if (error === "authorization_pending") { - console.log("Authorization pending..."); - } else { - console.error("Failed to poll login status:", error); - toast.error("Failed to poll login status"); - } - } finally { - mutex.release(); - } - }, - cancelLoginOnline: async () => { - const { account, logout, _pollingInterval, _progressUnlisten } = get(); - clearInterval(_pollingInterval ?? undefined); - _progressUnlisten?.(); - if (account) { - await logout(); - } - set({ - loginMode: null, - _pollingInterval: null, - statusMessage: null, - _progressUnlisten: null, - }); - }, - loginOffline: async (username: string) => { - const trimmedUsername = username.trim(); - if (trimmedUsername.length === 0) { - throw new Error("Username cannot be empty"); - } - - try { - const account = await loginOffline(trimmedUsername); - set({ account, loginMode: "offline" }); - } catch (error) { - console.error("Failed to login offline:", error); - toast.error("Failed to login offline"); - } - }, - logout: async () => { - try { - await logout(); - set({ account: null }); - } catch (error) { - console.error("Failed to logout:", error); - toast.error("Failed to logout"); - } - }, -})); diff --git a/packages/ui-new/src/models/instances.ts b/packages/ui-new/src/models/instances.ts deleted file mode 100644 index f434c7c..0000000 --- a/packages/ui-new/src/models/instances.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { toast } from "sonner"; -import { create } from "zustand"; -import { - createInstance, - deleteInstance, - duplicateInstance, - getActiveInstance, - getInstance, - listInstances, - setActiveInstance, - updateInstance, -} from "@/client"; -import type { Instance } from "@/types"; - -interface InstancesState { - // State - instances: Instance[]; - activeInstance: Instance | null; - - // Actions - refresh: () => Promise<void>; - create: (name: string) => Promise<Instance | null>; - delete: (id: string) => Promise<void>; - update: (instance: Instance) => Promise<void>; - setActiveInstance: (instance: Instance) => Promise<void>; - duplicate: (id: string, newName: string) => Promise<Instance | null>; - getInstance: (id: string) => Promise<Instance | null>; -} - -export const useInstancesStore = create<InstancesState>((set, get) => ({ - // Initial state - instances: [], - activeInstance: null, - - // Actions - refresh: async () => { - const { setActiveInstance } = get(); - try { - const instances = await listInstances(); - const active = await getActiveInstance(); - - if (!active && instances.length > 0) { - // If no active instance but instances exist, set the first one as active - await setActiveInstance(instances[0]); - } - - set({ instances }); - } catch (e) { - console.error("Failed to load instances:", e); - toast.error("Error loading instances"); - } - }, - - create: async (name) => { - const { refresh } = get(); - try { - const instance = await createInstance(name); - await refresh(); - toast.success(`Instance "${name}" created successfully`); - return instance; - } catch (e) { - console.error("Failed to create instance:", e); - toast.error("Error creating instance"); - return null; - } - }, - - delete: async (id) => { - const { refresh, instances, activeInstance, setActiveInstance } = get(); - try { - await deleteInstance(id); - await refresh(); - - // If deleted instance was active, set another as active - if (activeInstance?.id === id) { - if (instances.length > 0) { - await setActiveInstance(instances[0]); - } else { - set({ activeInstance: null }); - } - } - - toast.success("Instance deleted successfully"); - } catch (e) { - console.error("Failed to delete instance:", e); - toast.error("Error deleting instance"); - } - }, - - update: async (instance) => { - const { refresh } = get(); - try { - await updateInstance(instance); - await refresh(); - toast.success("Instance updated successfully"); - } catch (e) { - console.error("Failed to update instance:", e); - toast.error("Error updating instance"); - } - }, - - setActiveInstance: async (instance) => { - try { - await setActiveInstance(instance.id); - set({ activeInstance: instance }); - toast.success("Active instance changed"); - } catch (e) { - console.error("Failed to set active instance:", e); - toast.error("Error setting active instance"); - } - }, - - duplicate: async (id, newName) => { - const { refresh } = get(); - try { - const instance = await duplicateInstance(id, newName); - await refresh(); - toast.success(`Instance duplicated as "${newName}"`); - return instance; - } catch (e) { - console.error("Failed to duplicate instance:", e); - toast.error("Error duplicating instance"); - return null; - } - }, - - getInstance: async (id) => { - try { - return await getInstance(id); - } catch (e) { - console.error("Failed to get instance:", e); - return null; - } - }, -})); diff --git a/packages/ui-new/src/models/settings.ts b/packages/ui-new/src/models/settings.ts deleted file mode 100644 index 9f4119c..0000000 --- a/packages/ui-new/src/models/settings.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { toast } from "sonner"; -import { create } from "zustand/react"; -import { getConfigPath, getSettings, saveSettings } from "@/client"; -import type { LauncherConfig } from "@/types"; - -export interface SettingsState { - config: LauncherConfig | null; - configPath: string | null; - - /* Theme getter */ - get theme(): string; - /* Apply theme to the document */ - applyTheme: (theme?: string) => void; - - /* Refresh settings from the backend */ - refresh: () => Promise<void>; - /* Save settings to the backend */ - save: () => Promise<void>; - /* Update settings in the backend */ - update: (config: LauncherConfig) => Promise<void>; - /* Merge settings with the current config without saving */ - merge: (config: Partial<LauncherConfig>) => void; -} - -export const useSettingsStore = create<SettingsState>((set, get) => ({ - config: null, - configPath: null, - - get theme() { - const { config } = get(); - return config?.theme || "dark"; - }, - applyTheme: (theme?: string) => { - const { config } = get(); - if (!config) return; - if (!theme) theme = config.theme; - let themeValue = theme; - if (theme === "system") { - themeValue = window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; - } - document.documentElement.classList.remove("light", "dark"); - document.documentElement.setAttribute("data-theme", themeValue); - document.documentElement.classList.add(themeValue); - set({ config: { ...config, theme } }); - }, - - refresh: async () => { - const { applyTheme } = get(); - try { - const settings = await getSettings(); - const path = await getConfigPath(); - set({ config: settings, configPath: path }); - applyTheme(settings.theme); - } catch (error) { - console.error("Failed to load settings:", error); - toast.error("Failed to load settings"); - } - }, - save: async () => { - const { config } = get(); - if (!config) return; - await saveSettings(config); - }, - update: async (config) => { - await saveSettings(config); - set({ config }); - }, - merge: (config) => { - const { config: currentConfig } = get(); - if (!currentConfig) throw new Error("Settings not loaded"); - set({ config: { ...currentConfig, ...config } }); - }, -})); diff --git a/packages/ui-new/src/pages/assistant-view.tsx.bk b/packages/ui-new/src/pages/assistant-view.tsx.bk deleted file mode 100644 index 56f827b..0000000 --- a/packages/ui-new/src/pages/assistant-view.tsx.bk +++ /dev/null @@ -1,485 +0,0 @@ -import { - AlertTriangle, - Bot, - Brain, - ChevronDown, - Loader2, - RefreshCw, - Send, - Settings, - Trash2, -} from "lucide-react"; -import { marked } from "marked"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; -import { Textarea } from "@/components/ui/textarea"; -import { toNumber } from "@/lib/tsrs-utils"; -import { type Message, useAssistantStore } from "../stores/assistant-store"; -import { useSettingsStore } from "../stores/settings-store"; -import { useUiStore } from "../stores/ui-store"; - -interface ParsedMessage { - thinking: string | null; - content: string; - isThinking: boolean; -} - -function parseMessageContent(content: string): ParsedMessage { - if (!content) return { thinking: null, content: "", isThinking: false }; - - // Support both <thinking> and <think> (DeepSeek uses <think>) - let startTag = "<thinking>"; - let endTag = "</thinking>"; - let startIndex = content.indexOf(startTag); - - if (startIndex === -1) { - startTag = "<think>"; - endTag = "</think>"; - startIndex = content.indexOf(startTag); - } - - // Also check for encoded tags if they weren't decoded properly - if (startIndex === -1) { - startTag = "\u003cthink\u003e"; - endTag = "\u003c/think\u003e"; - startIndex = content.indexOf(startTag); - } - - if (startIndex !== -1) { - const endIndex = content.indexOf(endTag, startIndex); - - if (endIndex !== -1) { - // Completed thinking block - const before = content.substring(0, startIndex); - const thinking = content - .substring(startIndex + startTag.length, endIndex) - .trim(); - const after = content.substring(endIndex + endTag.length); - - return { - thinking, - content: (before + after).trim(), - isThinking: false, - }; - } else { - // Incomplete thinking block (still streaming) - const before = content.substring(0, startIndex); - const thinking = content.substring(startIndex + startTag.length).trim(); - - return { - thinking, - content: before.trim(), - isThinking: true, - }; - } - } - - return { thinking: null, content, isThinking: false }; -} - -function renderMarkdown(content: string): string { - if (!content) return ""; - try { - return marked(content, { breaks: true, gfm: true }) as string; - } catch { - return content; - } -} - -export function AssistantView() { - const { - messages, - isProcessing, - isProviderHealthy, - streamingContent, - init, - checkHealth, - sendMessage, - clearHistory, - } = useAssistantStore(); - const { settings } = useSettingsStore(); - const { setView } = useUiStore(); - - const [input, setInput] = useState(""); - const messagesEndRef = useRef<HTMLDivElement>(null); - const messagesContainerRef = useRef<HTMLDivElement>(null); - - const provider = settings.assistant.llmProvider; - const endpoint = - provider === "ollama" - ? settings.assistant.ollamaEndpoint - : settings.assistant.openaiEndpoint; - const model = - provider === "ollama" - ? settings.assistant.ollamaModel - : settings.assistant.openaiModel; - - const getProviderName = (): string => { - if (provider === "ollama") { - return `Ollama (${model})`; - } else if (provider === "openai") { - return `OpenAI (${model})`; - } - return provider; - }; - - const getProviderHelpText = (): string => { - if (provider === "ollama") { - return `Please ensure Ollama is installed and running at ${endpoint}.`; - } else if (provider === "openai") { - return "Please check your OpenAI API key in Settings > AI Assistant."; - } - return ""; - }; - - const scrollToBottom = useCallback(() => { - if (messagesContainerRef.current) { - setTimeout(() => { - if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = - messagesContainerRef.current.scrollHeight; - } - }, 0); - } - }, []); - - useEffect(() => { - init(); - }, [init]); - - useEffect(() => { - if (messages.length > 0 || isProcessing) { - scrollToBottom(); - } - }, [messages.length, isProcessing, scrollToBottom]); - - const handleSubmit = async () => { - if (!input.trim() || isProcessing) return; - const text = input; - setInput(""); - await sendMessage(text, settings.assistant.enabled, provider, endpoint); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }; - - const renderMessage = (message: Message, index: number) => { - const isUser = message.role === "user"; - const parsed = parseMessageContent(message.content); - - return ( - <div - key={index} - className={`flex ${isUser ? "justify-end" : "justify-start"} mb-4`} - > - <div - className={`max-w-[80%] rounded-2xl px-4 py-3 ${ - isUser - ? "bg-indigo-500 text-white rounded-br-none" - : "bg-zinc-800 text-zinc-100 rounded-bl-none" - }`} - > - {!isUser && parsed.thinking && ( - <div className="mb-3 max-w-full overflow-hidden"> - <details className="group" open={parsed.isThinking}> - <summary className="list-none cursor-pointer flex items-center gap-2 text-zinc-500 hover:text-zinc-300 transition-colors text-xs font-medium select-none bg-black/20 p-2 rounded-lg border border-white/5 w-fit mb-2 outline-none"> - <Brain className="h-3 w-3" /> - <span>Thinking Process</span> - <ChevronDown className="h-3 w-3 transition-transform duration-200 group-open:rotate-180" /> - </summary> - <div className="pl-3 border-l-2 border-zinc-700 text-zinc-500 text-xs italic leading-relaxed whitespace-pre-wrap font-mono max-h-96 overflow-y-auto custom-scrollbar bg-black/10 p-2 rounded-r-md"> - {parsed.thinking} - {parsed.isThinking && ( - <span className="inline-block w-1.5 h-3 bg-zinc-500 ml-1 animate-pulse align-middle" /> - )} - </div> - </details> - </div> - )} - <div - className="prose prose-invert max-w-none" - dangerouslySetInnerHTML={{ - __html: renderMarkdown(parsed.content), - }} - /> - {!isUser && message.stats && ( - <div className="mt-2 pt-2 border-t border-zinc-700/50"> - <div className="text-xs text-zinc-400"> - {message.stats.evalCount} tokens ·{" "} - {Math.round(toNumber(message.stats.totalDuration) / 1000000)} - ms - </div> - </div> - )} - </div> - </div> - ); - }; - - return ( - <div className="h-full w-full flex flex-col gap-4 p-4 lg:p-8"> - <div className="flex items-center justify-between mb-2"> - <div className="flex items-center gap-3"> - <div className="p-2 bg-indigo-500/20 rounded-lg text-indigo-400"> - <Bot size={24} /> - </div> - <div> - <h2 className="text-2xl font-bold">Game Assistant</h2> - <p className="text-zinc-400 text-sm"> - Powered by {getProviderName()} - </p> - </div> - </div> - - <div className="flex items-center gap-2"> - {!settings.assistant.enabled ? ( - <Badge - variant="outline" - className="bg-zinc-500/10 text-zinc-400 border-zinc-500/20" - > - <AlertTriangle className="h-3 w-3 mr-1" /> - Disabled - </Badge> - ) : !isProviderHealthy ? ( - <Badge - variant="outline" - className="bg-red-500/10 text-red-400 border-red-500/20" - > - <AlertTriangle className="h-3 w-3 mr-1" /> - Offline - </Badge> - ) : ( - <Badge - variant="outline" - className="bg-emerald-500/10 text-emerald-400 border-emerald-500/20" - > - <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse mr-1" /> - Online - </Badge> - )} - - <Button - variant="ghost" - size="icon" - onClick={checkHealth} - title="Check Connection" - disabled={isProcessing} - > - <RefreshCw - className={`h-4 w-4 ${isProcessing ? "animate-spin" : ""}`} - /> - </Button> - - <Button - variant="ghost" - size="icon" - onClick={clearHistory} - title="Clear History" - disabled={isProcessing} - > - <Trash2 className="h-4 w-4" /> - </Button> - - <Button - variant="ghost" - size="icon" - onClick={() => setView("settings")} - title="Settings" - > - <Settings className="h-4 w-4" /> - </Button> - </div> - </div> - - {/* Chat Area */} - <div className="flex-1 bg-black/20 border border-white/5 rounded-xl overflow-hidden flex flex-col relative"> - {/* Warning when assistant is disabled */} - {!settings.assistant.enabled && ( - <div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-10"> - <Card className="bg-yellow-500/10 border-yellow-500/20"> - <CardContent className="p-3 flex items-center gap-2"> - <AlertTriangle className="h-4 w-4 text-yellow-500" /> - <span className="text-yellow-500 text-sm font-medium"> - Assistant is disabled. Enable it in Settings > AI - Assistant. - </span> - </CardContent> - </Card> - </div> - )} - - {/* Provider offline warning */} - {settings.assistant.enabled && !isProviderHealthy && ( - <div className="absolute top-4 left-1/2 transform -translate-x-1/2 z-10"> - <Card className="bg-red-500/10 border-red-500/20"> - <CardContent className="p-3 flex items-center gap-2"> - <AlertTriangle className="h-4 w-4 text-red-500" /> - <div className="flex flex-col"> - <span className="text-red-500 text-sm font-medium"> - Assistant is offline - </span> - <span className="text-red-400 text-xs"> - {getProviderHelpText()} - </span> - </div> - </CardContent> - </Card> - </div> - )} - - {/* Messages Container */} - <ScrollArea className="flex-1 p-4 lg:p-6" ref={messagesContainerRef}> - {messages.length === 0 ? ( - <div className="flex flex-col items-center justify-center h-full text-zinc-400 gap-4 mt-8"> - <div className="p-4 bg-zinc-800/50 rounded-full"> - <Bot className="h-12 w-12" /> - </div> - <h3 className="text-xl font-medium">How can I help you today?</h3> - <p className="text-center max-w-md text-sm"> - I can analyze your game logs, diagnose crashes, or explain mod - features. - {!settings.assistant.enabled && ( - <span className="block mt-2 text-yellow-500"> - Assistant is disabled. Enable it in{" "} - <button - type="button" - onClick={() => setView("settings")} - className="text-indigo-400 hover:underline" - > - Settings > AI Assistant - </button> - . - </span> - )} - </p> - <div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-2 max-w-lg"> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput("How do I fix Minecraft crashing on launch?") - } - disabled={isProcessing} - > - <div className="text-sm"> - How do I fix Minecraft crashing on launch? - </div> - </Button> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput("What's the best way to improve FPS?") - } - disabled={isProcessing} - > - <div className="text-sm"> - What's the best way to improve FPS? - </div> - </Button> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput( - "Can you help me install Fabric for Minecraft 1.20.4?", - ) - } - disabled={isProcessing} - > - <div className="text-sm"> - Can you help me install Fabric for 1.20.4? - </div> - </Button> - <Button - variant="outline" - className="text-left h-auto py-3" - onClick={() => - setInput("What mods do you recommend for performance?") - } - disabled={isProcessing} - > - <div className="text-sm"> - What mods do you recommend for performance? - </div> - </Button> - </div> - </div> - ) : ( - <> - {messages.map((message, index) => renderMessage(message, index))} - {isProcessing && streamingContent && ( - <div className="flex justify-start mb-4"> - <div className="max-w-[80%] bg-zinc-800 text-zinc-100 rounded-2xl rounded-bl-none px-4 py-3"> - <div - className="prose prose-invert max-w-none" - dangerouslySetInnerHTML={{ - __html: renderMarkdown(streamingContent), - }} - /> - <div className="flex items-center gap-1 mt-2 text-xs text-zinc-400"> - <Loader2 className="h-3 w-3 animate-spin" /> - <span>Assistant is typing...</span> - </div> - </div> - </div> - )} - </> - )} - <div ref={messagesEndRef} /> - </ScrollArea> - - <Separator /> - - {/* Input Area */} - <div className="p-3 lg:p-4"> - <div className="flex gap-2"> - <Textarea - placeholder={ - settings.assistant.enabled - ? "Ask about your game..." - : "Assistant is disabled. Enable it in Settings to use." - } - value={input} - onChange={(e) => setInput(e.target.value)} - onKeyDown={handleKeyDown} - className="min-h-11 max-h-50 resize-none border-zinc-700 bg-zinc-900/50 focus:bg-zinc-900/80" - disabled={!settings.assistant.enabled || isProcessing} - /> - <Button - onClick={handleSubmit} - disabled={ - !settings.assistant.enabled || !input.trim() || isProcessing - } - className="px-6 bg-indigo-600 hover:bg-indigo-700 text-white" - > - {isProcessing ? ( - <Loader2 className="h-4 w-4 animate-spin" /> - ) : ( - <Send className="h-4 w-4" /> - )} - </Button> - </div> - <div className="mt-2 flex items-center justify-between"> - <div className="text-xs text-zinc-500"> - {settings.assistant.enabled - ? "Press Enter to send, Shift+Enter for new line" - : "Enable the assistant in Settings to use"} - </div> - <div className="text-xs text-zinc-500"> - Model: {model} • Provider: {provider} - </div> - </div> - </div> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/pages/home-view.tsx b/packages/ui-new/src/pages/home-view.tsx deleted file mode 100644 index 4f80cb0..0000000 --- a/packages/ui-new/src/pages/home-view.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { useEffect, useState } from "react"; -import { BottomBar } from "@/components/bottom-bar"; -import type { SaturnEffect } from "@/lib/effects/SaturnEffect"; -import { useGameStore } from "../stores/game-store"; -import { useReleasesStore } from "../stores/releases-store"; - -export function HomeView() { - const gameStore = useGameStore(); - const releasesStore = useReleasesStore(); - const [mouseX, setMouseX] = useState(0); - const [mouseY, setMouseY] = useState(0); - - useEffect(() => { - releasesStore.loadReleases(); - }, [releasesStore.loadReleases]); - - const handleMouseMove = (e: React.MouseEvent) => { - const x = (e.clientX / window.innerWidth) * 2 - 1; - const y = (e.clientY / window.innerHeight) * 2 - 1; - setMouseX(x); - setMouseY(y); - - // Forward mouse move to SaturnEffect (if available) for parallax/rotation interactions - try { - const saturn = ( - window as unknown as { - getSaturnEffect?: () => SaturnEffect; - } - ).getSaturnEffect?.(); - if (saturn?.handleMouseMove) { - saturn.handleMouseMove(e.clientX); - } - } catch { - /* best-effort, ignore errors from effect */ - } - }; - - const handleSaturnMouseDown = (e: React.MouseEvent) => { - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleMouseDown) { - saturn.handleMouseDown(e.clientX); - } - } catch { - /* ignore */ - } - }; - - const handleSaturnMouseUp = () => { - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleMouseUp) { - saturn.handleMouseUp(); - } - } catch { - /* ignore */ - } - }; - - const handleSaturnMouseLeave = () => { - // Treat leaving the area as mouse-up for the effect - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleMouseUp) { - saturn.handleMouseUp(); - } - } catch { - /* ignore */ - } - }; - - const handleSaturnTouchStart = (e: React.TouchEvent) => { - if (e.touches && e.touches.length === 1) { - try { - const clientX = e.touches[0].clientX; - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleTouchStart) { - saturn.handleTouchStart(clientX); - } - } catch { - /* ignore */ - } - } - }; - - const handleSaturnTouchMove = (e: React.TouchEvent) => { - if (e.touches && e.touches.length === 1) { - try { - const clientX = e.touches[0].clientX; - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleTouchMove) { - saturn.handleTouchMove(clientX); - } - } catch { - /* ignore */ - } - } - }; - - const handleSaturnTouchEnd = () => { - try { - const saturn = (window as any).getSaturnEffect?.(); - if (saturn?.handleTouchEnd) { - saturn.handleTouchEnd(); - } - } catch { - /* ignore */ - } - }; - - return ( - <div - className="relative z-10 h-full overflow-y-auto custom-scrollbar scroll-smooth" - style={{ - overflow: releasesStore.isLoading ? "hidden" : "auto", - }} - > - {/* Hero Section (Full Height) - Interactive area */} - <div - role="tab" - className="min-h-full flex flex-col justify-end p-12 pb-32 cursor-grab active:cursor-grabbing select-none" - onMouseDown={handleSaturnMouseDown} - onMouseMove={handleMouseMove} - onMouseUp={handleSaturnMouseUp} - onMouseLeave={handleSaturnMouseLeave} - onTouchStart={handleSaturnTouchStart} - onTouchMove={handleSaturnTouchMove} - onTouchEnd={handleSaturnTouchEnd} - tabIndex={0} - > - {/* 3D Floating Hero Text */} - <div - className="transition-transform duration-200 ease-out origin-bottom-left" - style={{ - transform: `perspective(1000px) rotateX(${mouseY * -1}deg) rotateY(${mouseX * 1}deg)`, - }} - > - <div className="flex items-center gap-3 mb-6"> - <div className="h-px w-12 bg-white/50"></div> - <span className="text-xs font-mono font-bold tracking-[0.2em] text-white/50 uppercase"> - Launcher Active - </span> - </div> - - <h1 className="text-8xl font-black tracking-tighter text-white mb-6 leading-none"> - MINECRAFT - </h1> - - <div className="flex items-center gap-4"> - <div className="bg-white/10 backdrop-blur-md border border-white/10 px-3 py-1 rounded-sm text-xs font-bold uppercase tracking-widest text-white shadow-sm"> - Java Edition - </div> - <div className="h-4 w-px bg-white/20"></div> - <div className="text-sm text-zinc-400"> - Latest Release{" "} - <span className="text-white font-medium"> - {gameStore.latestRelease?.id || "..."} - </span> - </div> - </div> - </div> - - {/* Action Area */} - <div className="mt-8 flex gap-4"> - <div className="text-zinc-500 text-sm font-mono"> - > Ready to launch session. - </div> - </div> - - <BottomBar /> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/pages/index.tsx b/packages/ui-new/src/pages/index.tsx deleted file mode 100644 index 54cfc1e..0000000 --- a/packages/ui-new/src/pages/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { useEffect } from "react"; -import { Outlet, useLocation } from "react-router"; -import { ParticleBackground } from "@/components/particle-background"; -import { Sidebar } from "@/components/sidebar"; -import { useAuthStore } from "@/models/auth"; -import { useSettingsStore } from "@/models/settings"; - -export function IndexPage() { - const authStore = useAuthStore(); - const settingsStore = useSettingsStore(); - - const location = useLocation(); - - useEffect(() => { - authStore.init(); - settingsStore.refresh(); - }, [authStore.init, settingsStore.refresh]); - - return ( - <div className="relative h-screen w-full overflow-hidden bg-background font-sans"> - <div className="absolute inset-0 z-0 bg-gray-100 dark:bg-[#09090b] overflow-hidden"> - {settingsStore.config?.customBackgroundPath && ( - <> - <img - src={settingsStore.config?.customBackgroundPath} - alt="Background" - className="absolute inset-0 w-full h-full object-cover transition-transform duration-[20s] ease-linear" - onError={(e) => - console.error("Failed to load main background:", e) - } - /> - {/* Dimming Overlay for readability */} - <div className="absolute inset-0 bg-black/50" /> - </> - )} - - {!settingsStore.config?.customBackgroundPath && ( - <> - {settingsStore.theme === "dark" ? ( - <div className="absolute inset-0 opacity-60 bg-linear-to-br from-emerald-900 via-zinc-900 to-indigo-950"></div> - ) : ( - <div className="absolute inset-0 opacity-100 bg-linear-to-br from-emerald-100 via-gray-100 to-indigo-100"></div> - )} - - {location.pathname === "/" && <ParticleBackground />} - - <div className="absolute inset-0 bg-linear-to-t from-zinc-900 via-transparent to-black/50 dark:from-zinc-900 dark:to-black/50"></div> - </> - )} - - {/* Subtle Grid Overlay */} - <div - className="absolute inset-0 z-0 dark:opacity-10 opacity-30 pointer-events-none" - style={{ - backgroundImage: `linear-gradient(${ - settingsStore.config?.theme === "dark" ? "#ffffff" : "#000000" - } 1px, transparent 1px), linear-gradient(90deg, ${ - settingsStore.config?.theme === "dark" ? "#ffffff" : "#000000" - } 1px, transparent 1px)`, - backgroundSize: "40px 40px", - maskImage: - "radial-gradient(circle at 50% 50%, black 30%, transparent 70%)", - }} - /> - </div> - - <div className="size-full flex flex-row p-4 space-x-4 z-20 relative"> - <Sidebar /> - - <main className="size-full overflow-hidden"> - <Outlet /> - </main> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/pages/instances-view.tsx b/packages/ui-new/src/pages/instances-view.tsx deleted file mode 100644 index ad6bd38..0000000 --- a/packages/ui-new/src/pages/instances-view.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import { Copy, Edit2, Plus, Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import InstanceCreationModal from "@/components/instance-creation-modal"; -import InstanceEditorModal from "@/components/instance-editor-modal"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/models/instances"; -import type { Instance } from "../types/bindings/instance"; - -export function InstancesView() { - const instancesStore = useInstancesStore(); - - // Modal / UI state - const [showCreateModal, setShowCreateModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [showDuplicateModal, setShowDuplicateModal] = useState(false); - - // Selected / editing instance state - const [selectedInstance, setSelectedInstance] = useState<Instance | null>( - null, - ); - const [editingInstance, setEditingInstance] = useState<Instance | null>(null); - - // Form fields - const [duplicateName, setDuplicateName] = useState(""); - - useEffect(() => { - instancesStore.refresh(); - }, [instancesStore.refresh]); - - // Handlers to open modals - const openCreate = () => { - setShowCreateModal(true); - }; - - const openEdit = (instance: Instance) => { - setEditingInstance({ ...instance }); - setShowEditModal(true); - }; - - const openDelete = (instance: Instance) => { - setSelectedInstance(instance); - setShowDeleteConfirm(true); - }; - - const openDuplicate = (instance: Instance) => { - setSelectedInstance(instance); - setDuplicateName(`${instance.name} (Copy)`); - setShowDuplicateModal(true); - }; - - const confirmDelete = async () => { - if (!selectedInstance) return; - await instancesStore.delete(selectedInstance.id); - setSelectedInstance(null); - setShowDeleteConfirm(false); - }; - - const confirmDuplicate = async () => { - if (!selectedInstance) return; - const name = duplicateName.trim(); - if (!name) return; - await instancesStore.duplicate(selectedInstance.id, name); - setSelectedInstance(null); - setDuplicateName(""); - setShowDuplicateModal(false); - }; - - const formatDate = (timestamp: number): string => - new Date(timestamp * 1000).toLocaleDateString(); - - const formatLastPlayed = (timestamp: number): string => { - const date = new Date(timestamp * 1000); - const now = new Date(); - const diff = now.getTime() - date.getTime(); - const days = Math.floor(diff / (1000 * 60 * 60 * 24)); - - if (days === 0) return "Today"; - if (days === 1) return "Yesterday"; - if (days < 7) return `${days} days ago`; - return date.toLocaleDateString(); - }; - - return ( - <div className="h-full flex flex-col gap-4 p-6 overflow-y-auto"> - <div className="flex items-center justify-between"> - <h1 className="text-2xl font-bold text-gray-900 dark:text-white"> - Instances - </h1> - <Button - type="button" - onClick={openCreate} - className="px-4 py-2 transition-colors" - > - <Plus size={18} /> - Create Instance - </Button> - </div> - - {instancesStore.instances.length === 0 ? ( - <div className="flex-1 flex items-center justify-center"> - <div className="text-center text-gray-500 dark:text-gray-400"> - <p className="text-lg mb-2">No instances yet</p> - <p className="text-sm">Create your first instance to get started</p> - </div> - </div> - ) : ( - <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> - {instancesStore.instances.map((instance) => { - const isActive = instancesStore.activeInstance?.id === instance.id; - - return ( - <li - key={instance.id} - onClick={() => instancesStore.setActiveInstance(instance)} - onKeyDown={(e) => - e.key === "Enter" && - instancesStore.setActiveInstance(instance) - } - className={`relative p-4 text-left border-2 transition-all cursor-pointer hover:border-blue-500 ${ - isActive ? "border-blue-500" : "border-transparent" - } bg-gray-100 dark:bg-gray-800`} - > - {/* Instance Icon */} - {instance.iconPath ? ( - <div className="w-12 h-12 mb-3 rounded overflow-hidden"> - <img - src={instance.iconPath} - alt={instance.name} - className="w-full h-full object-cover" - /> - </div> - ) : ( - <div className="w-12 h-12 mb-3 rounded bg-linear-to-br from-blue-500 to-purple-600 flex items-center justify-center"> - <span className="text-white font-bold text-lg"> - {instance.name.charAt(0).toUpperCase()} - </span> - </div> - )} - - <h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1"> - {instance.name} - </h3> - - <div className="space-y-1 text-sm text-gray-600 dark:text-gray-400"> - {instance.versionId ? ( - <p className="truncate">Version: {instance.versionId}</p> - ) : ( - <p className="text-gray-400">No version selected</p> - )} - - {instance.modLoader && ( - <p className="truncate"> - Mod Loader:{" "} - <span className="capitalize">{instance.modLoader}</span> - </p> - )} - - <p className="truncate"> - Created: {formatDate(toNumber(instance.createdAt))} - </p> - - {instance.lastPlayed && ( - <p className="truncate"> - Last played:{" "} - {formatLastPlayed(toNumber(instance.lastPlayed))} - </p> - )} - </div> - - {/* Action Buttons */} - <div className="mt-4 flex gap-2"> - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - openEdit(instance); - }} - className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors" - > - <Edit2 size={14} /> - Edit - </button> - - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - openDuplicate(instance); - }} - className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-sm transition-colors" - > - <Copy size={14} /> - Duplicate - </button> - - <button - type="button" - onClick={(e) => { - e.stopPropagation(); - openDelete(instance); - }} - className="flex-1 flex items-center justify-center gap-1 px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded text-sm transition-colors" - > - <Trash2 size={14} /> - Delete - </button> - </div> - </li> - ); - })} - </ul> - )} - - <InstanceCreationModal - open={showCreateModal} - onOpenChange={setShowCreateModal} - /> - - <InstanceEditorModal - open={showEditModal} - instance={editingInstance} - onOpenChange={(open) => { - setShowEditModal(open); - if (!open) setEditingInstance(null); - }} - /> - - {/* Delete Confirmation */} - <Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> - <DialogContent> - <DialogHeader> - <DialogTitle>Delete Instance</DialogTitle> - <DialogDescription> - Are you sure you want to delete "{selectedInstance?.name}"? This - action cannot be undone. - </DialogDescription> - </DialogHeader> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => { - setShowDeleteConfirm(false); - setSelectedInstance(null); - }} - > - Cancel - </Button> - <Button - type="button" - onClick={confirmDelete} - className="bg-red-600 text-white hover:bg-red-500" - > - Delete - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - - {/* Duplicate Modal */} - <Dialog open={showDuplicateModal} onOpenChange={setShowDuplicateModal}> - <DialogContent> - <DialogHeader> - <DialogTitle>Duplicate Instance</DialogTitle> - <DialogDescription> - Provide a name for the duplicated instance. - </DialogDescription> - </DialogHeader> - - <div className="mt-4"> - <Input - value={duplicateName} - onChange={(e) => setDuplicateName(e.target.value)} - placeholder="New instance name" - onKeyDown={(e) => e.key === "Enter" && confirmDuplicate()} - /> - </div> - - <DialogFooter> - <Button - type="button" - variant="outline" - onClick={() => { - setShowDuplicateModal(false); - setSelectedInstance(null); - setDuplicateName(""); - }} - > - Cancel - </Button> - <Button - type="button" - onClick={confirmDuplicate} - disabled={!duplicateName.trim()} - > - Duplicate - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ); -} diff --git a/packages/ui-new/src/pages/settings-view.tsx.bk b/packages/ui-new/src/pages/settings-view.tsx.bk deleted file mode 100644 index ac43d9b..0000000 --- a/packages/ui-new/src/pages/settings-view.tsx.bk +++ /dev/null @@ -1,1158 +0,0 @@ -import { open } from "@tauri-apps/plugin-dialog"; -import { - Coffee, - Download, - FileJson, - Loader2, - RefreshCw, - Upload, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Separator } from "@/components/ui/separator"; -import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Textarea } from "@/components/ui/textarea"; -import { useSettingsStore } from "../stores/settings-store"; - -const effectOptions = [ - { value: "saturn", label: "Saturn" }, - { value: "constellation", label: "Network (Constellation)" }, -]; - -const logServiceOptions = [ - { value: "paste.rs", label: "paste.rs (Free, No Account)" }, - { value: "pastebin.com", label: "pastebin.com (Requires API Key)" }, -]; - -const llmProviderOptions = [ - { value: "ollama", label: "Ollama (Local)" }, - { value: "openai", label: "OpenAI (Remote)" }, -]; - -const languageOptions = [ - { value: "auto", label: "Auto (Match User)" }, - { value: "English", label: "English" }, - { value: "Chinese", label: "中文" }, - { value: "Japanese", label: "日本語" }, - { value: "Korean", label: "한국어" }, - { value: "Spanish", label: "Español" }, - { value: "French", label: "Français" }, - { value: "German", label: "Deutsch" }, - { value: "Russian", label: "Русский" }, -]; - -const ttsProviderOptions = [ - { value: "disabled", label: "Disabled" }, - { value: "piper", label: "Piper TTS (Local)" }, - { value: "edge", label: "Edge TTS (Online)" }, -]; - -const personas = [ - { - value: "default", - label: "Minecraft Expert (Default)", - prompt: - "You are a helpful Minecraft expert assistant. You help players with game issues, mod installation, performance optimization, and gameplay tips. Analyze any game logs provided and give concise, actionable advice.", - }, - { - value: "technical", - label: "Technical Debugger", - prompt: - "You are a technical support specialist for Minecraft. Focus strictly on analyzing logs, identifying crash causes, and providing technical solutions. Be precise and avoid conversational filler.", - }, - { - value: "concise", - label: "Concise Helper", - prompt: - "You are a direct and concise assistant. Provide answers in as few words as possible while remaining accurate. Use bullet points for lists.", - }, - { - value: "explain", - label: "Teacher / Explainer", - prompt: - "You are a patient teacher. Explain Minecraft concepts, redstone mechanics, and mod features in simple, easy-to-understand terms suitable for beginners.", - }, - { - value: "pirate", - label: "Pirate Captain", - prompt: - "You are a salty Minecraft Pirate Captain! Yarr! Speak like a pirate while helping the crew (the user) with their blocky adventures. Use terms like 'matey', 'landlubber', and 'treasure'.", - }, -]; - -export function SettingsView() { - const { - settings, - backgroundUrl, - javaInstallations, - isDetectingJava, - showJavaDownloadModal, - selectedDownloadSource, - javaCatalog, - isLoadingCatalog, - catalogError, - selectedMajorVersion, - selectedImageType, - showOnlyRecommended, - searchQuery, - isDownloadingJava, - downloadProgress, - javaDownloadStatus, - pendingDownloads, - ollamaModels, - openaiModels, - isLoadingOllamaModels, - isLoadingOpenaiModels, - ollamaModelsError, - openaiModelsError, - showConfigEditor, - rawConfigContent, - configFilePath, - configEditorError, - filteredReleases, - availableMajorVersions, - installStatus, - selectedRelease, - currentModelOptions, - loadSettings, - saveSettings, - detectJava, - selectJava, - openJavaDownloadModal, - closeJavaDownloadModal, - loadJavaCatalog, - refreshCatalog, - loadPendingDownloads, - selectMajorVersion, - downloadJava, - cancelDownload, - resumeDownloads, - openConfigEditor, - closeConfigEditor, - saveRawConfig, - loadOllamaModels, - loadOpenaiModels, - set, - setSetting, - setAssistantSetting, - setFeatureFlag, - } = useSettingsStore(); - - // Mark potentially-unused variables as referenced so TypeScript does not report - // them as unused in this file (they are part of the store API and used elsewhere). - // This is a no-op but satisfies the compiler. - void selectedDownloadSource; - void javaCatalog; - void javaDownloadStatus; - void pendingDownloads; - void ollamaModels; - void openaiModels; - void isLoadingOllamaModels; - void isLoadingOpenaiModels; - void ollamaModelsError; - void openaiModelsError; - void selectedRelease; - void loadJavaCatalog; - void loadPendingDownloads; - void cancelDownload; - void resumeDownloads; - void setFeatureFlag; - const [selectedPersona, setSelectedPersona] = useState("default"); - const [migrating, setMigrating] = useState(false); - const [activeTab, setActiveTab] = useState("appearance"); - - useEffect(() => { - loadSettings(); - detectJava(); - }, [loadSettings, detectJava]); - - useEffect(() => { - if (activeTab === "assistant") { - if (settings.assistant.llmProvider === "ollama") { - loadOllamaModels(); - } else if (settings.assistant.llmProvider === "openai") { - loadOpenaiModels(); - } - } - }, [ - activeTab, - settings.assistant.llmProvider, - loadOllamaModels, - loadOpenaiModels, - ]); - - const handleSelectBackground = async () => { - try { - const selected = await open({ - multiple: false, - filters: [ - { - name: "Images", - extensions: ["png", "jpg", "jpeg", "webp", "gif"], - }, - ], - }); - - if (selected && typeof selected === "string") { - setSetting("customBackgroundPath", selected); - saveSettings(); - } - } catch (e) { - console.error("Failed to select background:", e); - toast.error("Failed to select background"); - } - }; - - const handleClearBackground = () => { - setSetting("customBackgroundPath", null); - saveSettings(); - }; - - const handleApplyPersona = (value: string) => { - const persona = personas.find((p) => p.value === value); - if (persona) { - setAssistantSetting("systemPrompt", persona.prompt); - setSelectedPersona(value); - saveSettings(); - } - }; - - const handleResetSystemPrompt = () => { - const defaultPersona = personas.find((p) => p.value === "default"); - if (defaultPersona) { - setAssistantSetting("systemPrompt", defaultPersona.prompt); - setSelectedPersona("default"); - saveSettings(); - } - }; - - const handleRunMigration = async () => { - if (migrating) return; - setMigrating(true); - try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - toast.success("Migration complete! Files migrated successfully"); - } catch (e) { - console.error("Migration failed:", e); - toast.error(`Migration failed: ${e}`); - } finally { - setMigrating(false); - } - }; - - return ( - <div className="h-full flex flex-col p-6 overflow-hidden"> - <div className="flex items-center justify-between mb-6"> - <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600"> - Settings - </h2> - - <Button - variant="outline" - size="sm" - onClick={openConfigEditor} - className="gap-2" - > - <FileJson className="h-4 w-4" /> - <span className="hidden sm:inline">Open JSON</span> - </Button> - </div> - - <Tabs - value={activeTab} - onValueChange={setActiveTab} - className="flex-1 overflow-hidden" - > - <TabsList className="grid grid-cols-4 mb-6"> - <TabsTrigger value="appearance">Appearance</TabsTrigger> - <TabsTrigger value="java">Java</TabsTrigger> - <TabsTrigger value="advanced">Advanced</TabsTrigger> - <TabsTrigger value="assistant">Assistant</TabsTrigger> - </TabsList> - - <ScrollArea className="flex-1 pr-2"> - <TabsContent value="appearance" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">Appearance</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label className="mb-3">Custom Background Image</Label> - <div className="flex items-center gap-6"> - <div className="w-40 h-24 rounded-xl overflow-hidden bg-secondary border relative group shadow-lg"> - {backgroundUrl ? ( - <img - src={backgroundUrl} - alt="Background Preview" - className="w-full h-full object-cover" - onError={(e) => { - console.error("Failed to load image"); - e.currentTarget.style.display = "none"; - }} - /> - ) : ( - <div className="w-full h-full bg-linear-to-br from-emerald-900 via-zinc-900 to-indigo-950" /> - )} - {!backgroundUrl && ( - <div className="absolute inset-0 flex items-center justify-center text-xs text-white/50 bg-black/20"> - Default Gradient - </div> - )} - </div> - - <div className="flex flex-col gap-2"> - <Button - variant="outline" - onClick={handleSelectBackground} - > - Select Image - </Button> - {backgroundUrl && ( - <Button - variant="ghost" - className="text-red-500" - onClick={handleClearBackground} - > - Reset to Default - </Button> - )} - </div> - </div> - <p className="text-sm text-muted-foreground mt-3"> - Select an image from your computer to replace the default - gradient background. - </p> - </div> - - <Separator /> - - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">Visual Effects</Label> - <p className="text-sm text-muted-foreground"> - Enable particle effects and animated gradients. - </p> - </div> - <Switch - checked={settings.enableVisualEffects} - onCheckedChange={(checked) => { - setSetting("enableVisualEffects", checked); - saveSettings(); - }} - /> - </div> - - {settings.enableVisualEffects && ( - <div className="pl-4 border-l-2 border-border"> - <div className="space-y-2"> - <Label>Theme Effect</Label> - <Select - value={settings.activeEffect} - onValueChange={(value) => { - setSetting("activeEffect", value); - saveSettings(); - }} - > - <SelectTrigger className="w-52"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {effectOptions.map((option) => ( - <SelectItem - key={option.value} - value={option.value} - > - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - <p className="text-sm text-muted-foreground"> - Select the active visual theme. - </p> - </div> - </div> - )} - - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">GPU Acceleration</Label> - <p className="text-sm text-muted-foreground"> - Enable GPU acceleration for the interface. - </p> - </div> - <Switch - checked={settings.enableGpuAcceleration} - onCheckedChange={(checked) => { - setSetting("enableGpuAcceleration", checked); - saveSettings(); - }} - /> - </div> - </div> - </CardContent> - </Card> - </TabsContent> - - <TabsContent value="java" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">Java Environment</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label className="mb-2">Java Path</Label> - <div className="flex gap-2"> - <Input - value={settings.javaPath} - onChange={(e) => setSetting("javaPath", e.target.value)} - className="flex-1" - placeholder="java or full path to java executable" - /> - <Button - variant="outline" - onClick={() => detectJava()} - disabled={isDetectingJava} - > - {isDetectingJava ? ( - <Loader2 className="h-4 w-4 animate-spin" /> - ) : ( - "Detect" - )} - </Button> - </div> - <p className="text-sm text-muted-foreground mt-2"> - Path to Java executable. - </p> - </div> - - <div> - <Label className="mb-2">Memory Settings (MB)</Label> - <div className="grid grid-cols-2 gap-4"> - <div> - <Label htmlFor="min-memory" className="text-sm"> - Minimum Memory - </Label> - <Input - id="min-memory" - type="number" - value={settings.minMemory} - onChange={(e) => - setSetting( - "minMemory", - parseInt(e.target.value, 10) || 1024, - ) - } - min={512} - step={256} - /> - </div> - <div> - <Label htmlFor="max-memory" className="text-sm"> - Maximum Memory - </Label> - <Input - id="max-memory" - type="number" - value={settings.maxMemory} - onChange={(e) => - setSetting( - "maxMemory", - parseInt(e.target.value, 10) || 2048, - ) - } - min={1024} - step={256} - /> - </div> - </div> - <p className="text-sm text-muted-foreground mt-2"> - Memory allocation for Minecraft. - </p> - </div> - - <Separator /> - - <div> - <div className="flex items-center justify-between mb-4"> - <Label className="text-base"> - Detected Java Installations - </Label> - <Button - variant="outline" - size="sm" - onClick={() => detectJava()} - disabled={isDetectingJava} - > - <RefreshCw - className={`h-4 w-4 mr-2 ${isDetectingJava ? "animate-spin" : ""}`} - /> - Rescan - </Button> - </div> - - {javaInstallations.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground border rounded-lg"> - <Coffee className="h-12 w-12 mx-auto mb-4 opacity-30" /> - <p>No Java installations detected</p> - </div> - ) : ( - <div className="space-y-2"> - {javaInstallations.map((installation) => ( - <Card - key={installation.path} - className={`p-3 cursor-pointer transition-colors ${ - settings.javaPath === installation.path - ? "border-primary bg-primary/5" - : "" - }`} - onClick={() => selectJava(installation.path)} - > - <div className="flex items-center justify-between"> - <div> - <div className="font-medium flex items-center gap-2"> - <Coffee className="h-4 w-4" /> - {installation.version} - </div> - <div className="text-sm text-muted-foreground font-mono"> - {installation.path} - </div> - </div> - {settings.javaPath === installation.path && ( - <div className="h-5 w-5 text-primary">✓</div> - )} - </div> - </Card> - ))} - </div> - )} - - <div className="mt-4"> - <Button - variant="default" - className="w-full" - onClick={openJavaDownloadModal} - > - <Download className="h-4 w-4 mr-2" /> - Download Java - </Button> - </div> - </div> - </CardContent> - </Card> - </TabsContent> - - <TabsContent value="advanced" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">Advanced Settings</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div> - <Label className="mb-2">Download Threads</Label> - <Input - type="number" - value={settings.downloadThreads} - onChange={(e) => - setSetting( - "downloadThreads", - parseInt(e.target.value, 10) || 32, - ) - } - min={1} - max={64} - /> - <p className="text-sm text-muted-foreground mt-2"> - Number of concurrent downloads. - </p> - </div> - - <div> - <Label className="mb-2">Log Upload Service</Label> - <Select - value={settings.logUploadService} - onValueChange={(value) => { - setSetting("logUploadService", value as any); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {logServiceOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - {settings.logUploadService === "pastebin.com" && ( - <div> - <Label className="mb-2">Pastebin API Key</Label> - <Input - type="password" - value={settings.pastebinApiKey || ""} - onChange={(e) => - setSetting("pastebinApiKey", e.target.value || null) - } - placeholder="Enter your Pastebin API key" - /> - </div> - )} - - <Separator /> - - <div className="space-y-4"> - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">Use Shared Caches</Label> - <p className="text-sm text-muted-foreground"> - Share downloaded assets between instances. - </p> - </div> - <Switch - checked={settings.useSharedCaches} - onCheckedChange={(checked) => { - setSetting("useSharedCaches", checked); - saveSettings(); - }} - /> - </div> - - {!settings.useSharedCaches && ( - <div className="flex items-center justify-between"> - <div> - <Label className="text-base"> - Keep Legacy Per-Instance Storage - </Label> - <p className="text-sm text-muted-foreground"> - Maintain separate cache folders for compatibility. - </p> - </div> - <Switch - checked={settings.keepLegacyPerInstanceStorage} - onCheckedChange={(checked) => { - setSetting("keepLegacyPerInstanceStorage", checked); - saveSettings(); - }} - /> - </div> - )} - - {settings.useSharedCaches && ( - <div className="mt-4"> - <Button - variant="outline" - className="w-full" - onClick={handleRunMigration} - disabled={migrating} - > - {migrating ? ( - <Loader2 className="h-4 w-4 mr-2 animate-spin" /> - ) : ( - <Upload className="h-4 w-4 mr-2" /> - )} - {migrating - ? "Migrating..." - : "Migrate to Shared Caches"} - </Button> - </div> - )} - </div> - </CardContent> - </Card> - </TabsContent> - - <TabsContent value="assistant" className="space-y-6"> - <Card className="border-border"> - <CardHeader> - <CardTitle className="text-lg">AI Assistant</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - <div className="flex items-center justify-between"> - <div> - <Label className="text-base">Enable Assistant</Label> - <p className="text-sm text-muted-foreground"> - Enable the AI assistant for help with Minecraft issues. - </p> - </div> - <Switch - checked={settings.assistant.enabled} - onCheckedChange={(checked) => { - setAssistantSetting("enabled", checked); - saveSettings(); - }} - /> - </div> - - {settings.assistant.enabled && ( - <> - <div> - <Label className="mb-2">LLM Provider</Label> - <Select - value={settings.assistant.llmProvider} - onValueChange={(value) => { - setAssistantSetting("llmProvider", value as any); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {llmProviderOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div> - <Label className="mb-2">Model</Label> - <Select - value={ - settings.assistant.llmProvider === "ollama" - ? settings.assistant.ollamaModel - : settings.assistant.openaiModel - } - onValueChange={(value) => { - if (settings.assistant.llmProvider === "ollama") { - setAssistantSetting("ollamaModel", value); - } else { - setAssistantSetting("openaiModel", value); - } - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {currentModelOptions.map((model) => ( - <SelectItem key={model.value} value={model.value}> - {model.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - {settings.assistant.llmProvider === "ollama" && ( - <div> - <Label className="mb-2">Ollama Endpoint</Label> - <Input - value={settings.assistant.ollamaEndpoint} - onChange={(e) => { - setAssistantSetting( - "ollamaEndpoint", - e.target.value, - ); - saveSettings(); - }} - placeholder="http://localhost:11434" - /> - </div> - )} - - {settings.assistant.llmProvider === "openai" && ( - <> - <div> - <Label className="mb-2">OpenAI API Key</Label> - <Input - type="password" - value={settings.assistant.openaiApiKey || ""} - onChange={(e) => { - setAssistantSetting( - "openaiApiKey", - e.target.value || null, - ); - saveSettings(); - }} - placeholder="Enter your OpenAI API key" - /> - </div> - <div> - <Label className="mb-2">OpenAI Endpoint</Label> - <Input - value={settings.assistant.openaiEndpoint} - onChange={(e) => { - setAssistantSetting( - "openaiEndpoint", - e.target.value, - ); - saveSettings(); - }} - placeholder="https://api.openai.com/v1" - /> - </div> - </> - )} - - <div> - <Label className="mb-2">Response Language</Label> - <Select - value={settings.assistant.responseLanguage} - onValueChange={(value) => { - setAssistantSetting("responseLanguage", value); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {languageOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div> - <Label className="mb-2">Assistant Persona</Label> - <Select - value={selectedPersona} - onValueChange={handleApplyPersona} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - {personas.map((persona) => ( - <SelectItem - key={persona.value} - value={persona.value} - > - {persona.label} - </SelectItem> - ))} - </SelectContent> - </Select> - <div className="mt-2"> - <Button - variant="outline" - size="sm" - onClick={handleResetSystemPrompt} - > - Reset to Default - </Button> - </div> - </div> - - <div> - <Label className="mb-2">System Prompt</Label> - - <Textarea - value={settings.assistant.systemPrompt} - onChange={(e) => { - setAssistantSetting("systemPrompt", e.target.value); - saveSettings(); - }} - rows={6} - className="font-mono text-sm" - /> - </div> - - <div> - <Label className="mb-2">Text-to-Speech</Label> - - <Select - value={settings.assistant.ttsProvider} - onValueChange={(value) => { - setAssistantSetting("ttsProvider", value); - saveSettings(); - }} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - - <SelectContent> - {ttsProviderOptions.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - </> - )} - </CardContent> - </Card> - </TabsContent> - </ScrollArea> - </Tabs> - - {/* Java Download Modal */} - <Dialog - open={showJavaDownloadModal} - onOpenChange={closeJavaDownloadModal} - > - <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Download Java</DialogTitle> - <DialogDescription> - Download and install Java for Minecraft. - </DialogDescription> - </DialogHeader> - - <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> - <div className="space-y-4"> - <div> - <Label className="mb-2">Java Version</Label> - <Select - value={selectedMajorVersion?.toString() || ""} - onValueChange={(v) => selectMajorVersion(parseInt(v, 10))} - > - <SelectTrigger> - <SelectValue placeholder="Select version" /> - </SelectTrigger> - <SelectContent> - {availableMajorVersions.map((version) => ( - <SelectItem key={version} value={version.toString()}> - Java {version} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div> - <Label className="mb-2">Type</Label> - <Select - value={selectedImageType} - onValueChange={(v) => set({ selectedImageType: v as any })} - > - <SelectTrigger> - <SelectValue /> - </SelectTrigger> - <SelectContent> - <SelectItem value="jre">JRE (Runtime)</SelectItem> - <SelectItem value="jdk">JDK (Development)</SelectItem> - </SelectContent> - </Select> - </div> - - <div className="flex items-center space-x-2"> - <Checkbox - id="recommended" - checked={showOnlyRecommended} - onCheckedChange={(checked) => - set({ showOnlyRecommended: !!checked }) - } - /> - <Label htmlFor="recommended">Show only LTS/Recommended</Label> - </div> - - <div> - <Label className="mb-2">Search</Label> - <Input - placeholder="Search versions..." - value={searchQuery} - onChange={(e) => set({ searchQuery: e.target.value })} - /> - </div> - - <Button - variant="outline" - size="sm" - onClick={refreshCatalog} - disabled={isLoadingCatalog} - > - <RefreshCw - className={`h-4 w-4 mr-2 ${isLoadingCatalog ? "animate-spin" : ""}`} - /> - Refresh Catalog - </Button> - </div> - - <div className="md:col-span-2"> - <ScrollArea className="h-75 pr-4"> - {isLoadingCatalog ? ( - <div className="flex items-center justify-center h-full"> - <Loader2 className="h-8 w-8 animate-spin" /> - </div> - ) : catalogError ? ( - <div className="text-red-500 p-4">{catalogError}</div> - ) : filteredReleases.length === 0 ? ( - <div className="text-muted-foreground p-4 text-center"> - No Java versions found - </div> - ) : ( - <div className="space-y-2"> - {filteredReleases.map((release) => { - const status = installStatus( - release.majorVersion, - release.imageType, - ); - return ( - <Card - key={`${release.majorVersion}-${release.imageType}`} - className="p-3 cursor-pointer hover:bg-accent" - onClick={() => - selectMajorVersion(release.majorVersion) - } - > - <div className="flex items-center justify-between"> - <div> - <div className="font-medium"> - Java {release.majorVersion}{" "} - {release.imageType.toUpperCase()} - </div> - <div className="text-sm text-muted-foreground"> - {release.releaseName} • {release.architecture}{" "} - {release.architecture} - </div> - </div> - <div className="flex items-center gap-2"> - {release.isLts && ( - <Badge variant="secondary">LTS</Badge> - )} - {status === "installed" && ( - <Badge variant="default">Installed</Badge> - )} - {status === "available" && ( - <Button - variant="ghost" - size="sm" - onClick={(e) => { - e.stopPropagation(); - selectMajorVersion(release.majorVersion); - downloadJava(); - }} - > - <Download className="h-3 w-3 mr-1" /> - Download - </Button> - )} - </div> - </div> - </Card> - ); - })} - </div> - )} - </ScrollArea> - </div> - </div> - - {isDownloadingJava && downloadProgress && ( - <div className="mt-4 p-4 border rounded-lg"> - <div className="flex justify-between items-center mb-2"> - <span className="text-sm font-medium"> - {downloadProgress.fileName} - </span> - <span className="text-sm text-muted-foreground"> - {Math.round(downloadProgress.percentage)}% - </span> - </div> - <div className="w-full bg-secondary h-2 rounded-full overflow-hidden"> - <div - className="bg-primary h-full transition-all duration-300" - style={{ width: `${downloadProgress.percentage}%` }} - /> - </div> - </div> - )} - - <DialogFooter> - <Button - variant="outline" - onClick={closeJavaDownloadModal} - disabled={isDownloadingJava} - > - Cancel - </Button> - {selectedMajorVersion && ( - <Button - onClick={() => downloadJava()} - disabled={isDownloadingJava} - > - {isDownloadingJava ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Downloading... - </> - ) : ( - <> - <Download className="mr-2 h-4 w-4" /> - Download Java {selectedMajorVersion} - </> - )} - </Button> - )} - </DialogFooter> - </DialogContent> - </Dialog> - - {/* Config Editor Modal */} - <Dialog open={showConfigEditor} onOpenChange={closeConfigEditor}> - <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Edit Configuration</DialogTitle> - <DialogDescription> - Edit the raw JSON configuration file. - </DialogDescription> - </DialogHeader> - - <div className="text-sm text-muted-foreground mb-2"> - File: {configFilePath} - </div> - - {configEditorError && ( - <div className="text-red-500 p-3 bg-red-50 dark:bg-red-950/30 rounded-md"> - {configEditorError} - </div> - )} - - <Textarea - value={rawConfigContent} - onChange={(e) => set({ rawConfigContent: e.target.value })} - className="font-mono text-sm h-100 resize-none" - spellCheck={false} - /> - - <DialogFooter> - <Button variant="outline" onClick={closeConfigEditor}> - Cancel - </Button> - <Button onClick={() => saveRawConfig()}>Save Changes</Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ); -} diff --git a/packages/ui-new/src/pages/settings.tsx b/packages/ui-new/src/pages/settings.tsx deleted file mode 100644 index 440a5dc..0000000 --- a/packages/ui-new/src/pages/settings.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import { toNumber } from "es-toolkit/compat"; -import { FileJsonIcon } from "lucide-react"; -import { useEffect, useState } from "react"; -import { migrateSharedCaches } from "@/client"; -import { ConfigEditor } from "@/components/config-editor"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Field, - FieldContent, - FieldDescription, - FieldGroup, - FieldLabel, - FieldLegend, - FieldSet, -} from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Spinner } from "@/components/ui/spinner"; -import { Switch } from "@/components/ui/switch"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useSettingsStore } from "@/models/settings"; - -export type SettingsTab = "general" | "appearance" | "advanced"; - -export function SettingsPage() { - const { config, ...settings } = useSettingsStore(); - const [showConfigEditor, setShowConfigEditor] = useState<boolean>(false); - const [activeTab, setActiveTab] = useState<SettingsTab>("general"); - - useEffect(() => { - if (!config) settings.refresh(); - }, [config, settings.refresh]); - - const renderScrollArea = () => { - if (!config) { - return ( - <div className="size-full justify-center items-center"> - <Spinner /> - </div> - ); - } - return ( - <ScrollArea className="size-full pr-2"> - <TabsContent value="general" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl">General</CardTitle> - </CardHeader> - <CardContent> - <FieldGroup> - <FieldSet> - <FieldLegend>Window Options</FieldLegend> - <FieldDescription> - May not work on some platforms like Linux Niri. - </FieldDescription> - <FieldGroup> - <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> - <Field> - <FieldLabel htmlFor="width"> - Window Default Width - </FieldLabel> - <Input - type="number" - name="width" - value={config?.width} - onChange={(e) => { - settings.merge({ - width: toNumber(e.target.value), - }); - }} - onBlur={() => { - settings.save(); - }} - min={800} - max={3840} - /> - </Field> - <Field> - <FieldLabel htmlFor="height"> - Window Default Height - </FieldLabel> - <Input - type="number" - name="height" - value={config?.height} - onChange={(e) => { - settings.merge({ - height: toNumber(e.target.value), - }); - }} - onBlur={() => { - settings.save(); - }} - min={600} - max={2160} - /> - </Field> - </div> - <Field className="flex flex-row items-center justify-between"> - <FieldContent> - <FieldLabel htmlFor="gpu-acceleration"> - GPU Acceleration - </FieldLabel> - <FieldDescription> - Enable GPU acceleration for the interface. - </FieldDescription> - </FieldContent> - <Switch - checked={config?.enableGpuAcceleration} - onCheckedChange={(checked) => { - settings.merge({ - enableGpuAcceleration: checked, - }); - settings.save(); - }} - /> - </Field> - </FieldGroup> - </FieldSet> - <FieldSet> - <FieldLegend>Network Options</FieldLegend> - <Field> - <Label htmlFor="download-threads">Download Threads</Label> - <Input - type="number" - name="download-threads" - value={config?.downloadThreads} - onChange={(e) => { - settings.merge({ - downloadThreads: toNumber(e.target.value), - }); - }} - onBlur={() => { - settings.save(); - }} - min={1} - max={64} - /> - </Field> - </FieldSet> - </FieldGroup> - </CardContent> - </Card> - </TabsContent> - <TabsContent value="java" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl"> - Java Installations - </CardTitle> - <CardContent></CardContent> - </CardHeader> - </Card> - </TabsContent> - <TabsContent value="appearance" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl">Appearance</CardTitle> - </CardHeader> - <CardContent> - <FieldGroup> - <Field className="flex flex-row"> - <FieldContent> - <FieldLabel htmlFor="theme">Theme</FieldLabel> - <FieldDescription> - Select your prefered theme. - </FieldDescription> - </FieldContent> - <Select - items={[ - { label: "Dark", value: "dark" }, - { label: "Light", value: "light" }, - { label: "System", value: "system" }, - ]} - value={config.theme} - onValueChange={async (value) => { - if ( - value === "system" || - value === "light" || - value === "dark" - ) { - settings.merge({ theme: value }); - await settings.save(); - settings.applyTheme(value); - } - }} - > - <SelectTrigger className="w-full max-w-48"> - <SelectValue placeholder="Please select a prefered theme" /> - </SelectTrigger> - <SelectContent alignItemWithTrigger={false}> - <SelectGroup> - <SelectItem value="system">System</SelectItem> - <SelectItem value="light">Light</SelectItem> - <SelectItem value="dark">Dark</SelectItem> - </SelectGroup> - </SelectContent> - </Select> - </Field> - </FieldGroup> - </CardContent> - </Card> - </TabsContent> - <TabsContent value="advanced" className="size-full"> - <Card className="size-full"> - <CardHeader> - <CardTitle className="font-bold text-xl">Advanced</CardTitle> - </CardHeader> - <CardContent> - <FieldGroup> - <FieldSet> - <FieldLegend>Advanced Options</FieldLegend> - <FieldGroup> - <Field className="flex flex-row items-center justify-between"> - <FieldContent> - <FieldLabel htmlFor="use-shared-caches"> - Use Shared Caches - </FieldLabel> - <FieldDescription> - Share downloaded assets between instances. - </FieldDescription> - </FieldContent> - <Switch - checked={config?.useSharedCaches} - onCheckedChange={async (checked) => { - checked && (await migrateSharedCaches()); - settings.merge({ - useSharedCaches: checked, - }); - settings.save(); - }} - /> - </Field> - <Field className="flex flex-row items-center justify-between"> - <FieldContent> - <FieldLabel htmlFor="keep-per-instance-storage"> - Keep Legacy Per-Instance Storage - </FieldLabel> - <FieldDescription> - Maintain separate cache folders for compatibility. - </FieldDescription> - </FieldContent> - <Switch - checked={config?.keepLegacyPerInstanceStorage} - onCheckedChange={(checked) => { - settings.merge({ - keepLegacyPerInstanceStorage: checked, - }); - settings.save(); - }} - /> - </Field> - </FieldGroup> - </FieldSet> - </FieldGroup> - </CardContent> - </Card> - </TabsContent> - </ScrollArea> - ); - }; - - return ( - <div className="size-full flex flex-col p-6 space-y-6"> - <div className="flex items-center justify-between"> - <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r dark:from-white dark:to-white/60 from-gray-900 to-gray-600"> - Settings - </h2> - - <Button - variant="outline" - size="sm" - onClick={() => setShowConfigEditor(true)} - > - <FileJsonIcon /> - <span className="hidden sm:inline">Open JSON</span> - </Button> - </div> - - <Tabs - value={activeTab} - onValueChange={setActiveTab} - className="size-full flex flex-col gap-6" - > - <TabsList> - <TabsTrigger value="general">General</TabsTrigger> - <TabsTrigger value="java">Java</TabsTrigger> - <TabsTrigger value="appearance">Appearance</TabsTrigger> - <TabsTrigger value="advanced">Advanced</TabsTrigger> - </TabsList> - {renderScrollArea()} - </Tabs> - - <ConfigEditor - open={showConfigEditor} - onOpenChange={() => setShowConfigEditor(false)} - /> - </div> - ); -} diff --git a/packages/ui-new/src/pages/versions-view.tsx.bk b/packages/ui-new/src/pages/versions-view.tsx.bk deleted file mode 100644 index d54596d..0000000 --- a/packages/ui-new/src/pages/versions-view.tsx.bk +++ /dev/null @@ -1,662 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { Coffee, Loader2, Search, Trash2 } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useInstancesStore } from "../models/instances"; -import { useGameStore } from "../stores/game-store"; -import type { Version } from "../types/bindings/manifest"; - -interface InstalledModdedVersion { - id: string; - javaVersion?: number; -} - -type TypeFilter = "all" | "release" | "snapshot" | "installed"; - -export function VersionsView() { - const { versions, selectedVersion, loadVersions, setSelectedVersion } = - useGameStore(); - const { activeInstance } = useInstancesStore(); - - const [searchQuery, setSearchQuery] = useState(""); - const [typeFilter, setTypeFilter] = useState<TypeFilter>("all"); - const [installedModdedVersions, setInstalledModdedVersions] = useState< - InstalledModdedVersion[] - >([]); - const [, setIsLoadingModded] = useState(false); - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [versionToDelete, setVersionToDelete] = useState<string | null>(null); - const [isDeleting, setIsDeleting] = useState(false); - const [selectedVersionMetadata, setSelectedVersionMetadata] = useState<{ - id: string; - javaVersion?: number; - isInstalled: boolean; - } | null>(null); - const [isLoadingMetadata, setIsLoadingMetadata] = useState(false); - const [showModLoaderSelector, setShowModLoaderSelector] = useState(false); - - const normalizedQuery = searchQuery.trim().toLowerCase().replace(/。/g, "."); - - // Load installed modded versions with Java version info - const loadInstalledModdedVersions = useCallback(async () => { - if (!activeInstance) { - setInstalledModdedVersions([]); - setIsLoadingModded(false); - return; - } - - setIsLoadingModded(true); - try { - const allInstalled = await invoke<Array<{ id: string; type: string }>>( - "list_installed_versions", - { instanceId: activeInstanceId }, - ); - - const moddedIds = allInstalled - .filter((v) => v.type === "fabric" || v.type === "forge") - .map((v) => v.id); - - const versionsWithJava = await Promise.all( - moddedIds.map(async (id) => { - try { - const javaVersion = await invoke<number | null>( - "get_version_java_version", - { - instanceId: activeInstanceId, - versionId: id, - }, - ); - return { - id, - javaVersion: javaVersion ?? undefined, - }; - } catch (e) { - console.error(`Failed to get Java version for ${id}:`, e); - return { id, javaVersion: undefined }; - } - }), - ); - - setInstalledModdedVersions(versionsWithJava); - } catch (e) { - console.error("Failed to load installed modded versions:", e); - toast.error("Error loading modded versions"); - } finally { - setIsLoadingModded(false); - } - }, [activeInstanceId]); - - // Combined versions list (vanilla + modded) - const allVersions = (() => { - const moddedVersions: Version[] = installedModdedVersions.map((v) => { - const versionType = v.id.startsWith("fabric-loader-") - ? "fabric" - : v.id.includes("-forge-") - ? "forge" - : "fabric"; - return { - id: v.id, - type: versionType, - url: "", - time: "", - releaseTime: new Date().toISOString(), - javaVersion: BigInt(v.javaVersion ?? 0), - isInstalled: true, - }; - }); - return [...moddedVersions, ...versions]; - })(); - - // Filter versions based on search and type filter - const filteredVersions = allVersions.filter((version) => { - if (typeFilter === "release" && version.type !== "release") return false; - if (typeFilter === "snapshot" && version.type !== "snapshot") return false; - if (typeFilter === "installed" && !version.isInstalled) return false; - - if ( - normalizedQuery && - !version.id.toLowerCase().includes(normalizedQuery) - ) { - return false; - } - - return true; - }); - - // Get version badge styling - const getVersionBadge = (type: string) => { - switch (type) { - case "release": - return { - text: "Release", - variant: "default" as const, - className: "bg-emerald-500 hover:bg-emerald-600", - }; - case "snapshot": - return { - text: "Snapshot", - variant: "secondary" as const, - className: "bg-amber-500 hover:bg-amber-600", - }; - case "fabric": - return { - text: "Fabric", - variant: "outline" as const, - className: "border-indigo-500 text-indigo-700 dark:text-indigo-300", - }; - case "forge": - return { - text: "Forge", - variant: "outline" as const, - className: "border-orange-500 text-orange-700 dark:text-orange-300", - }; - case "modpack": - return { - text: "Modpack", - variant: "outline" as const, - className: "border-purple-500 text-purple-700 dark:text-purple-300", - }; - default: - return { - text: type, - variant: "outline" as const, - className: "border-gray-500 text-gray-700 dark:text-gray-300", - }; - } - }; - - // Load version metadata - const loadVersionMetadata = useCallback( - async (versionId: string) => { - if (!versionId || !activeInstanceId) { - setSelectedVersionMetadata(null); - return; - } - - setIsLoadingMetadata(true); - try { - const metadata = await invoke<{ - id: string; - javaVersion?: number; - isInstalled: boolean; - }>("get_version_metadata", { - instanceId: activeInstanceId, - versionId, - }); - setSelectedVersionMetadata(metadata); - } catch (e) { - console.error("Failed to load version metadata:", e); - setSelectedVersionMetadata(null); - } finally { - setIsLoadingMetadata(false); - } - }, - [activeInstanceId], - ); - - // Get base version for mod loader selector - const selectedBaseVersion = (() => { - if (!selectedVersion) return ""; - - if (selectedVersion.startsWith("fabric-loader-")) { - const parts = selectedVersion.split("-"); - return parts[parts.length - 1]; - } - if (selectedVersion.includes("-forge-")) { - return selectedVersion.split("-forge-")[0]; - } - - const version = versions.find((v) => v.id === selectedVersion); - return version ? selectedVersion : ""; - })(); - - // Handle version deletion - const handleDeleteVersion = async () => { - if (!versionToDelete || !activeInstanceId) return; - - setIsDeleting(true); - try { - await invoke("delete_version", { - instanceId: activeInstanceId, - versionId: versionToDelete, - }); - - if (selectedVersion === versionToDelete) { - setSelectedVersion(""); - } - - setShowDeleteDialog(false); - setVersionToDelete(null); - toast.success("Version deleted successfully"); - - await loadVersions(activeInstanceId); - await loadInstalledModdedVersions(); - } catch (e) { - console.error("Failed to delete version:", e); - toast.error(`Failed to delete version: ${e}`); - } finally { - setIsDeleting(false); - } - }; - - // Show delete confirmation dialog - const showDeleteConfirmation = (versionId: string, e: React.MouseEvent) => { - e.stopPropagation(); - setVersionToDelete(versionId); - setShowDeleteDialog(true); - }; - - // Setup event listeners for version updates - useEffect(() => { - let unlisteners: UnlistenFn[] = []; - - const setupEventListeners = async () => { - try { - const versionDeletedUnlisten = await listen( - "version-deleted", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const downloadCompleteUnlisten = await listen( - "download-complete", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const versionInstalledUnlisten = await listen( - "version-installed", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const fabricInstalledUnlisten = await listen( - "fabric-installed", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - const forgeInstalledUnlisten = await listen( - "forge-installed", - async () => { - await loadVersions(activeInstanceId ?? undefined); - await loadInstalledModdedVersions(); - }, - ); - - unlisteners = [ - versionDeletedUnlisten, - downloadCompleteUnlisten, - versionInstalledUnlisten, - fabricInstalledUnlisten, - forgeInstalledUnlisten, - ]; - } catch (e) { - console.error("Failed to setup event listeners:", e); - } - }; - - setupEventListeners(); - loadInstalledModdedVersions(); - - return () => { - unlisteners.forEach((unlisten) => { - unlisten(); - }); - }; - }, [activeInstanceId, loadVersions, loadInstalledModdedVersions]); - - // Load metadata when selected version changes - useEffect(() => { - if (selectedVersion) { - loadVersionMetadata(selectedVersion); - } else { - setSelectedVersionMetadata(null); - } - }, [selectedVersion, loadVersionMetadata]); - - return ( - <div className="h-full flex flex-col p-6 overflow-hidden"> - <div className="flex items-center justify-between mb-6"> - <h2 className="text-3xl font-black bg-clip-text text-transparent bg-linear-to-r from-gray-900 to-gray-600 dark:from-white dark:to-white/60"> - Version Manager - </h2> - <div className="text-sm dark:text-white/40 text-black/50"> - Select a version to play or modify - </div> - </div> - - <div className="flex-1 grid grid-cols-1 lg:grid-cols-3 gap-6 overflow-hidden"> - {/* Left: Version List */} - <div className="lg:col-span-2 flex flex-col gap-4 overflow-hidden"> - {/* Search and Filters */} - <div className="flex gap-3"> - <div className="relative flex-1"> - <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> - <Input - type="text" - placeholder="Search versions..." - className="pl-9 bg-white/60 dark:bg-black/20 border-black/10 dark:border-white/10 dark:text-white text-gray-900 placeholder-black/30 dark:placeholder-white/30 focus:border-indigo-500/50 dark:focus:bg-black/40 focus:bg-white/80 backdrop-blur-sm" - value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} - /> - </div> - </div> - - {/* Type Filter Tabs */} - <Tabs - value={typeFilter} - onValueChange={(v) => setTypeFilter(v as TypeFilter)} - className="w-full" - > - <TabsList className="grid grid-cols-4 bg-white/60 dark:bg-black/20 border-black/5 dark:border-white/5"> - <TabsTrigger value="all">All</TabsTrigger> - <TabsTrigger value="release">Release</TabsTrigger> - <TabsTrigger value="snapshot">Snapshot</TabsTrigger> - <TabsTrigger value="installed">Installed</TabsTrigger> - </TabsList> - </Tabs> - - {/* Version List */} - <ScrollArea className="flex-1 pr-2"> - {versions.length === 0 ? ( - <div className="flex items-center justify-center h-40 dark:text-white/30 text-black/30 italic animate-pulse"> - Loading versions... - </div> - ) : filteredVersions.length === 0 ? ( - <div className="flex flex-col items-center justify-center h-40 dark:text-white/30 text-black/30 gap-2"> - <span className="text-2xl">👻</span> - <span>No matching versions found</span> - </div> - ) : ( - <div className="space-y-2"> - {filteredVersions.map((version) => { - const badge = getVersionBadge(version.type); - const isSelected = selectedVersion === version.id; - - return ( - <Card - key={version.id} - className={`w-full cursor-pointer transition-all duration-200 relative overflow-hidden group ${ - isSelected - ? "border-indigo-200 dark:border-indigo-500/50 bg-indigo-50 dark:bg-indigo-600/20 shadow-[0_0_20px_rgba(99,102,241,0.2)]" - : "border-black/5 dark:border-white/5 bg-white/40 dark:bg-white/5 hover:bg-white/60 dark:hover:bg-white/10 hover:border-black/10 dark:hover:border-white/10 hover:translate-x-1" - }`} - onClick={() => setSelectedVersion(version.id)} - > - {isSelected && ( - <div className="absolute inset-0 bg-linear-to-r from-indigo-500/10 to-transparent pointer-events-none" /> - )} - - <CardContent className="p-4"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-4 flex-1"> - <Badge - variant={badge.variant} - className={badge.className} - > - {badge.text} - </Badge> - <div className="flex-1"> - <div - className={`font-bold font-mono text-lg tracking-tight ${ - isSelected - ? "text-black dark:text-white" - : "text-gray-700 dark:text-zinc-300 group-hover:text-black dark:group-hover:text-white" - }`} - > - {version.id} - </div> - <div className="flex items-center gap-2 mt-0.5"> - {version.releaseTime && - version.type !== "fabric" && - version.type !== "forge" && ( - <div className="text-xs dark:text-white/30 text-black/30"> - {new Date( - version.releaseTime, - ).toLocaleDateString()} - </div> - )} - {version.javaVersion && ( - <div className="flex items-center gap-1 text-xs dark:text-white/40 text-black/40"> - <Coffee className="h-3 w-3 opacity-60" /> - <span className="font-medium"> - Java {version.javaVersion} - </span> - </div> - )} - </div> - </div> - </div> - - <div className="flex items-center gap-2"> - {version.isInstalled && ( - <Button - variant="ghost" - size="icon" - className="opacity-0 group-hover:opacity-100 transition-opacity text-red-500 dark:text-red-400 hover:bg-red-500/10 dark:hover:bg-red-500/20" - onClick={(e) => - showDeleteConfirmation(version.id, e) - } - title="Delete version" - > - <Trash2 className="h-4 w-4" /> - </Button> - )} - </div> - </div> - </CardContent> - </Card> - ); - })} - </div> - )} - </ScrollArea> - </div> - - {/* Right: Version Details */} - <div className="flex flex-col gap-6"> - <Card className="border-black/5 dark:border-white/5 bg-white/40 dark:bg-white/5 backdrop-blur-sm"> - <CardHeader> - <CardTitle className="text-lg">Version Details</CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - {selectedVersion ? ( - <> - <div> - <div className="text-sm text-muted-foreground mb-1"> - Selected Version - </div> - <div className="font-mono text-xl font-bold"> - {selectedVersion} - </div> - </div> - - {isLoadingMetadata ? ( - <div className="flex items-center gap-2"> - <Loader2 className="h-4 w-4 animate-spin" /> - <span className="text-sm">Loading metadata...</span> - </div> - ) : selectedVersionMetadata ? ( - <div className="space-y-3"> - <div> - <div className="text-sm text-muted-foreground mb-1"> - Installation Status - </div> - <Badge - variant={ - selectedVersionMetadata.isInstalled - ? "default" - : "outline" - } - > - {selectedVersionMetadata.isInstalled - ? "Installed" - : "Not Installed"} - </Badge> - </div> - - {selectedVersionMetadata.javaVersion && ( - <div> - <div className="text-sm text-muted-foreground mb-1"> - Java Version - </div> - <div className="flex items-center gap-2"> - <Coffee className="h-4 w-4" /> - <span> - Java {selectedVersionMetadata.javaVersion} - </span> - </div> - </div> - )} - - {!selectedVersionMetadata.isInstalled && ( - <Button - className="w-full" - onClick={() => setShowModLoaderSelector(true)} - > - Install with Mod Loader - </Button> - )} - </div> - ) : null} - </> - ) : ( - <div className="text-center py-8 text-muted-foreground"> - Select a version to view details - </div> - )} - </CardContent> - </Card> - - {/* Mod Loader Installation */} - {showModLoaderSelector && selectedBaseVersion && ( - <Card className="border-black/5 dark:border-white/5 bg-white/40 dark:bg-white/5 backdrop-blur-sm"> - <CardHeader> - <CardTitle className="text-lg">Install Mod Loader</CardTitle> - </CardHeader> - <CardContent className="space-y-4"> - <div className="text-sm text-muted-foreground"> - Install {selectedBaseVersion} with Fabric or Forge - </div> - <div className="flex gap-2"> - <Button - variant="outline" - className="flex-1" - onClick={async () => { - if (!activeInstanceId) return; - try { - await invoke("install_fabric", { - instanceId: activeInstanceId, - gameVersion: selectedBaseVersion, - loaderVersion: "latest", - }); - toast.success("Fabric installation started"); - setShowModLoaderSelector(false); - } catch (e) { - console.error("Failed to install Fabric:", e); - toast.error(`Failed to install Fabric: ${e}`); - } - }} - > - Install Fabric - </Button> - <Button - variant="outline" - className="flex-1" - onClick={async () => { - if (!activeInstanceId) return; - try { - await invoke("install_forge", { - instanceId: activeInstanceId, - gameVersion: selectedBaseVersion, - installerVersion: "latest", - }); - toast.success("Forge installation started"); - setShowModLoaderSelector(false); - } catch (e) { - console.error("Failed to install Forge:", e); - toast.error(`Failed to install Forge: ${e}`); - } - }} - > - Install Forge - </Button> - </div> - <Button - variant="ghost" - size="sm" - onClick={() => setShowModLoaderSelector(false)} - > - Cancel - </Button> - </CardContent> - </Card> - )} - </div> - </div> - - {/* Delete Confirmation Dialog */} - <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> - <DialogContent> - <DialogHeader> - <DialogTitle>Delete Version</DialogTitle> - <DialogDescription> - Are you sure you want to delete version "{versionToDelete}"? This - action cannot be undone. - </DialogDescription> - </DialogHeader> - <DialogFooter> - <Button - variant="outline" - onClick={() => { - setShowDeleteDialog(false); - setVersionToDelete(null); - }} - disabled={isDeleting} - > - Cancel - </Button> - <Button - variant="destructive" - onClick={handleDeleteVersion} - disabled={isDeleting} - > - {isDeleting ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Deleting... - </> - ) : ( - "Delete" - )} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - </div> - ); -} diff --git a/packages/ui-new/src/stores/assistant-store.ts b/packages/ui-new/src/stores/assistant-store.ts deleted file mode 100644 index 180031b..0000000 --- a/packages/ui-new/src/stores/assistant-store.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { create } from "zustand"; -import type { GenerationStats, StreamChunk } from "@/types/bindings/assistant"; - -export interface Message { - role: "user" | "assistant" | "system"; - content: string; - stats?: GenerationStats; -} - -interface AssistantState { - // State - messages: Message[]; - isProcessing: boolean; - isProviderHealthy: boolean | undefined; - streamingContent: string; - initialized: boolean; - streamUnlisten: UnlistenFn | null; - - // Actions - init: () => Promise<void>; - checkHealth: () => Promise<void>; - sendMessage: ( - content: string, - isEnabled: boolean, - provider: string, - endpoint: string, - ) => Promise<void>; - finishStreaming: () => void; - clearHistory: () => void; - setMessages: (messages: Message[]) => void; - setIsProcessing: (isProcessing: boolean) => void; - setIsProviderHealthy: (isProviderHealthy: boolean | undefined) => void; - setStreamingContent: (streamingContent: string) => void; -} - -export const useAssistantStore = create<AssistantState>((set, get) => ({ - // Initial state - messages: [], - isProcessing: false, - isProviderHealthy: false, - streamingContent: "", - initialized: false, - streamUnlisten: null, - - // Actions - init: async () => { - const { initialized } = get(); - if (initialized) return; - set({ initialized: true }); - await get().checkHealth(); - }, - - checkHealth: async () => { - try { - const isHealthy = await invoke<boolean>("assistant_check_health"); - set({ isProviderHealthy: isHealthy }); - } catch (e) { - console.error("Failed to check provider health:", e); - set({ isProviderHealthy: false }); - } - }, - - finishStreaming: () => { - const { streamUnlisten } = get(); - set({ isProcessing: false, streamingContent: "" }); - - if (streamUnlisten) { - streamUnlisten(); - set({ streamUnlisten: null }); - } - }, - - sendMessage: async (content, isEnabled, provider, endpoint) => { - if (!content.trim()) return; - - const { messages } = get(); - - if (!isEnabled) { - const newMessage: Message = { - role: "assistant", - content: "Assistant is disabled. Enable it in Settings > AI Assistant.", - }; - set({ messages: [...messages, { role: "user", content }, newMessage] }); - return; - } - - // Add user message - const userMessage: Message = { role: "user", content }; - const updatedMessages = [...messages, userMessage]; - set({ - messages: updatedMessages, - isProcessing: true, - streamingContent: "", - }); - - // Add empty assistant message for streaming - const assistantMessage: Message = { role: "assistant", content: "" }; - const withAssistantMessage = [...updatedMessages, assistantMessage]; - set({ messages: withAssistantMessage }); - - try { - // Set up stream listener - const unlisten = await listen<StreamChunk>( - "assistant-stream", - (event) => { - const chunk = event.payload; - const currentState = get(); - - if (chunk.content) { - const newStreamingContent = - currentState.streamingContent + chunk.content; - const currentMessages = [...currentState.messages]; - const lastIdx = currentMessages.length - 1; - - if (lastIdx >= 0 && currentMessages[lastIdx].role === "assistant") { - currentMessages[lastIdx] = { - ...currentMessages[lastIdx], - content: newStreamingContent, - }; - set({ - streamingContent: newStreamingContent, - messages: currentMessages, - }); - } - } - - if (chunk.done) { - const finalMessages = [...currentState.messages]; - const lastIdx = finalMessages.length - 1; - - if ( - chunk.stats && - lastIdx >= 0 && - finalMessages[lastIdx].role === "assistant" - ) { - finalMessages[lastIdx] = { - ...finalMessages[lastIdx], - stats: chunk.stats, - }; - set({ messages: finalMessages }); - } - - get().finishStreaming(); - } - }, - ); - - set({ streamUnlisten: unlisten }); - - // Start streaming chat - await invoke<string>("assistant_chat_stream", { - messages: withAssistantMessage.slice(0, -1), // Exclude the empty assistant message - }); - } catch (e) { - console.error("Failed to send message:", e); - const errorMessage = e instanceof Error ? e.message : String(e); - - let helpText = ""; - if (provider === "ollama") { - helpText = `\n\nPlease ensure Ollama is running at ${endpoint}.`; - } else if (provider === "openai") { - helpText = "\n\nPlease check your OpenAI API key in Settings."; - } - - // Update the last message with error - const currentMessages = [...get().messages]; - const lastIdx = currentMessages.length - 1; - if (lastIdx >= 0 && currentMessages[lastIdx].role === "assistant") { - currentMessages[lastIdx] = { - role: "assistant", - content: `Error: ${errorMessage}${helpText}`, - }; - set({ messages: currentMessages }); - } - - get().finishStreaming(); - } - }, - - clearHistory: () => { - set({ messages: [], streamingContent: "" }); - }, - - setMessages: (messages) => { - set({ messages }); - }, - - setIsProcessing: (isProcessing) => { - set({ isProcessing }); - }, - - setIsProviderHealthy: (isProviderHealthy) => { - set({ isProviderHealthy }); - }, - - setStreamingContent: (streamingContent) => { - set({ streamingContent }); - }, -})); diff --git a/packages/ui-new/src/stores/auth-store.ts b/packages/ui-new/src/stores/auth-store.ts deleted file mode 100644 index bf7e3c5..0000000 --- a/packages/ui-new/src/stores/auth-store.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { open } from "@tauri-apps/plugin-shell"; -import { toast } from "sonner"; -import { create } from "zustand"; -import type { Account, DeviceCodeResponse } from "../types/bindings/auth"; - -interface AuthState { - // State - currentAccount: Account | null; - isLoginModalOpen: boolean; - isLogoutConfirmOpen: boolean; - loginMode: "select" | "offline" | "microsoft"; - offlineUsername: string; - deviceCodeData: DeviceCodeResponse | null; - msLoginLoading: boolean; - msLoginStatus: string; - - // Private state - pollInterval: ReturnType<typeof setInterval> | null; - isPollingRequestActive: boolean; - authProgressUnlisten: UnlistenFn | null; - - // Actions - checkAccount: () => Promise<void>; - openLoginModal: () => void; - openLogoutConfirm: () => void; - cancelLogout: () => void; - confirmLogout: () => Promise<void>; - closeLoginModal: () => void; - resetLoginState: () => void; - performOfflineLogin: () => Promise<void>; - startMicrosoftLogin: () => Promise<void>; - checkLoginStatus: (deviceCode: string) => Promise<void>; - stopPolling: () => void; - cancelMicrosoftLogin: () => void; - setLoginMode: (mode: "select" | "offline" | "microsoft") => void; - setOfflineUsername: (username: string) => void; -} - -export const useAuthStore = create<AuthState>((set, get) => ({ - // Initial state - currentAccount: null, - isLoginModalOpen: false, - isLogoutConfirmOpen: false, - loginMode: "select", - offlineUsername: "", - deviceCodeData: null, - msLoginLoading: false, - msLoginStatus: "Waiting for authorization...", - - // Private state - pollInterval: null, - isPollingRequestActive: false, - authProgressUnlisten: null, - - // Actions - checkAccount: async () => { - try { - const acc = await invoke<Account | null>("get_active_account"); - set({ currentAccount: acc }); - } catch (error) { - console.error("Failed to check account:", error); - } - }, - - openLoginModal: () => { - const { currentAccount } = get(); - if (currentAccount) { - // Show custom logout confirmation dialog - set({ isLogoutConfirmOpen: true }); - return; - } - get().resetLoginState(); - set({ isLoginModalOpen: true }); - }, - - openLogoutConfirm: () => { - set({ isLogoutConfirmOpen: true }); - }, - - cancelLogout: () => { - set({ isLogoutConfirmOpen: false }); - }, - - confirmLogout: async () => { - set({ isLogoutConfirmOpen: false }); - try { - await invoke("logout"); - set({ currentAccount: null }); - } catch (error) { - console.error("Logout failed:", error); - } - }, - - closeLoginModal: () => { - get().stopPolling(); - set({ isLoginModalOpen: false }); - }, - - resetLoginState: () => { - set({ - loginMode: "select", - offlineUsername: "", - deviceCodeData: null, - msLoginLoading: false, - msLoginStatus: "Waiting for authorization...", - }); - }, - - performOfflineLogin: async () => { - const { offlineUsername } = get(); - if (!offlineUsername.trim()) return; - - try { - const account = await invoke<Account>("login_offline", { - username: offlineUsername, - }); - set({ - currentAccount: account, - isLoginModalOpen: false, - offlineUsername: "", - }); - } catch (error) { - // Keep UI-friendly behavior consistent with prior code - alert("Login failed: " + String(error)); - } - }, - - startMicrosoftLogin: async () => { - // Prepare UI state - set({ - msLoginLoading: true, - msLoginStatus: "Waiting for authorization...", - loginMode: "microsoft", - deviceCodeData: null, - }); - - // Listen to general launcher logs so we can display progress to the user. - // The backend emits logs via "launcher-log"; using that keeps this store decoupled - // from a dedicated auth event channel (backend may reuse launcher-log). - try { - const unlisten = await listen("launcher-log", (event) => { - const payload = event.payload; - // Normalize payload to string if possible - const message = - typeof payload === "string" - ? payload - : (payload?.toString?.() ?? JSON.stringify(payload)); - set({ msLoginStatus: message }); - }); - set({ authProgressUnlisten: unlisten }); - } catch (err) { - console.warn("Failed to attach launcher-log listener:", err); - } - - try { - const deviceCodeData = await invoke<DeviceCodeResponse>( - "start_microsoft_login", - ); - set({ deviceCodeData }); - - if (deviceCodeData) { - // Try to copy user code to clipboard for convenience (best-effort) - try { - await navigator.clipboard?.writeText(deviceCodeData.userCode ?? ""); - } catch (err) { - // ignore clipboard errors - console.debug("Clipboard copy failed:", err); - } - - // Open verification URI in default browser - try { - if (deviceCodeData.verificationUri) { - await open(deviceCodeData.verificationUri); - } - } catch (err) { - console.debug("Failed to open verification URI:", err); - } - - // Start polling for completion - // `interval` from the bindings is a bigint (seconds). Convert safely to number. - const intervalSeconds = - deviceCodeData.interval !== undefined && - deviceCodeData.interval !== null - ? Number(deviceCodeData.interval) - : 5; - const intervalMs = intervalSeconds * 1000; - const pollInterval = setInterval( - () => get().checkLoginStatus(deviceCodeData.deviceCode), - intervalMs, - ); - set({ pollInterval }); - } - } catch (error) { - toast.error(`Failed to start Microsoft login: ${error}`); - set({ loginMode: "select" }); - // cleanup listener if present - const { authProgressUnlisten } = get(); - if (authProgressUnlisten) { - authProgressUnlisten(); - set({ authProgressUnlisten: null }); - } - } finally { - set({ msLoginLoading: false }); - } - }, - - checkLoginStatus: async (deviceCode: string) => { - const { isPollingRequestActive } = get(); - if (isPollingRequestActive) return; - - set({ isPollingRequestActive: true }); - - try { - const account = await invoke<Account>("complete_microsoft_login", { - deviceCode, - }); - - // On success, stop polling and cleanup listener - get().stopPolling(); - const { authProgressUnlisten } = get(); - if (authProgressUnlisten) { - authProgressUnlisten(); - set({ authProgressUnlisten: null }); - } - - set({ - currentAccount: account, - isLoginModalOpen: false, - }); - } catch (error: unknown) { - const errStr = String(error); - if (errStr.includes("authorization_pending")) { - // Still waiting — keep polling - } else { - set({ msLoginStatus: "Error: " + errStr }); - - if ( - errStr.includes("expired_token") || - errStr.includes("access_denied") - ) { - // Terminal errors — stop polling and reset state - get().stopPolling(); - const { authProgressUnlisten } = get(); - if (authProgressUnlisten) { - authProgressUnlisten(); - set({ authProgressUnlisten: null }); - } - alert("Login failed: " + errStr); - set({ loginMode: "select" }); - } - } - } finally { - set({ isPollingRequestActive: false }); - } - }, - - stopPolling: () => { - const { pollInterval, authProgressUnlisten } = get(); - if (pollInterval) { - try { - clearInterval(pollInterval); - } catch (err) { - console.debug("Failed to clear poll interval:", err); - } - set({ pollInterval: null }); - } - if (authProgressUnlisten) { - try { - authProgressUnlisten(); - } catch (err) { - console.debug("Failed to unlisten auth progress:", err); - } - set({ authProgressUnlisten: null }); - } - }, - - cancelMicrosoftLogin: () => { - get().stopPolling(); - set({ - deviceCodeData: null, - msLoginLoading: false, - msLoginStatus: "", - loginMode: "select", - }); - }, - - setLoginMode: (mode: "select" | "offline" | "microsoft") => { - set({ loginMode: mode }); - }, - - setOfflineUsername: (username: string) => { - set({ offlineUsername: username }); - }, -})); diff --git a/packages/ui-new/src/stores/game-store.ts b/packages/ui-new/src/stores/game-store.ts deleted file mode 100644 index fa0f9f8..0000000 --- a/packages/ui-new/src/stores/game-store.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { toast } from "sonner"; -import { create } from "zustand"; -import { getVersions } from "@/client"; -import type { Version } from "@/types/bindings/manifest"; - -interface GameState { - // State - versions: Version[]; - selectedVersion: string; - - // Computed property - latestRelease: Version | undefined; - - // Actions - loadVersions: (instanceId?: string) => Promise<void>; - startGame: ( - currentAccount: any, - openLoginModal: () => void, - activeInstanceId: string | null, - setView: (view: any) => void, - ) => Promise<void>; - setSelectedVersion: (version: string) => void; - setVersions: (versions: Version[]) => void; -} - -export const useGameStore = create<GameState>((set, get) => ({ - // Initial state - versions: [], - selectedVersion: "", - - // Computed property - get latestRelease() { - return get().versions.find((v) => v.type === "release"); - }, - - // Actions - loadVersions: async (instanceId?: string) => { - console.log("Loading versions for instance:", instanceId); - try { - // Ask the backend for known versions (optionally scoped to an instance). - // The Tauri command `get_versions` is expected to return an array of `Version`. - const versions = await getVersions(); - set({ versions: versions ?? [] }); - } catch (e) { - console.error("Failed to load versions:", e); - // Keep the store consistent on error by clearing versions. - set({ versions: [] }); - } - }, - - startGame: async ( - currentAccount, - openLoginModal, - activeInstanceId, - setView, - ) => { - const { selectedVersion } = get(); - - if (!currentAccount) { - alert("Please login first!"); - openLoginModal(); - return; - } - - if (!selectedVersion) { - alert("Please select a version!"); - return; - } - - if (!activeInstanceId) { - alert("Please select an instance first!"); - setView("instances"); - return; - } - - toast.info("Preparing to launch " + selectedVersion + "..."); - - try { - // Note: In production, this would call Tauri invoke - // const msg = await invoke<string>("start_game", { - // instanceId: activeInstanceId, - // versionId: selectedVersion, - // }); - - // Simulate success - await new Promise((resolve) => setTimeout(resolve, 1000)); - toast.success("Game started successfully!"); - } catch (e) { - console.error(e); - toast.error(`Error: ${e}`); - } - }, - - setSelectedVersion: (version: string) => { - set({ selectedVersion: version }); - }, - - setVersions: (versions: Version[]) => { - set({ versions }); - }, -})); diff --git a/packages/ui-new/src/stores/logs-store.ts b/packages/ui-new/src/stores/logs-store.ts deleted file mode 100644 index b19f206..0000000 --- a/packages/ui-new/src/stores/logs-store.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { listen } from "@tauri-apps/api/event"; -import { create } from "zustand"; - -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"; -} - -interface LogsState { - // State - logs: LogEntry[]; - sources: Set<string>; - nextId: number; - maxLogs: number; - initialized: boolean; - - // Actions - addLog: (level: LogEntry["level"], source: string, message: string) => void; - addGameLog: (rawLine: string, isStderr: boolean) => void; - clear: () => void; - exportLogs: (filteredLogs: LogEntry[]) => string; - init: () => Promise<void>; - setLogs: (logs: LogEntry[]) => void; - setSources: (sources: Set<string>) => void; -} - -export const useLogsStore = create<LogsState>((set, get) => ({ - // Initial state - logs: [], - sources: new Set(["Launcher"]), - nextId: 0, - maxLogs: 5000, - initialized: false, - - // Actions - addLog: (level, source, message) => { - const { nextId, logs, maxLogs, sources } = get(); - const now = new Date(); - const timestamp = - now.toLocaleTimeString() + - "." + - now.getMilliseconds().toString().padStart(3, "0"); - - const newLog: LogEntry = { - id: nextId, - timestamp, - level, - source, - message, - }; - - const newLogs = [...logs, newLog]; - const newSources = new Set(sources); - - // Track source - if (!newSources.has(source)) { - newSources.add(source); - } - - // Trim logs if exceeding max - const trimmedLogs = - newLogs.length > maxLogs ? newLogs.slice(-maxLogs) : newLogs; - - set({ - logs: trimmedLogs, - sources: newSources, - nextId: nextId + 1, - }); - }, - - addGameLog: (rawLine, isStderr) => { - 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]}`; - get().addLog(level, source, message); - } else { - // Fallback: couldn't parse, use stderr as error indicator - const level = isStderr ? "error" : "info"; - get().addLog(level, "Game", rawLine); - } - }, - - clear: () => { - set({ - logs: [], - sources: new Set(["Launcher"]), - }); - get().addLog("info", "Launcher", "Logs cleared"); - }, - - exportLogs: (filteredLogs) => { - return filteredLogs - .map( - (l) => - `[${l.timestamp}] [${l.source}/${l.level.toUpperCase()}] ${l.message}`, - ) - .join("\n"); - }, - - init: async () => { - const { initialized } = get(); - if (initialized) return; - - set({ initialized: true }); - - // Initial log - get().addLog("info", "Launcher", "Logs initialized"); - - // General Launcher Logs - await listen<string>("launcher-log", (e) => { - get().addLog("info", "Launcher", e.payload); - }); - - // Game Stdout - parse log level - await listen<string>("game-stdout", (e) => { - get().addGameLog(e.payload, false); - }); - - // Game Stderr - parse log level, default to error - await listen<string>("game-stderr", (e) => { - get().addGameLog(e.payload, true); - }); - - // Download Events (Summarized) - await listen("download-start", (e: any) => { - get().addLog( - "info", - "Downloader", - `Starting batch download of ${e.payload} files...`, - ); - }); - - await listen("download-complete", () => { - get().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")) { - get().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) { - get().addLog( - "info", - "JavaInstaller", - `Downloading Java: ${p.file_name}`, - ); - } else if (p.status === "Completed") { - get().addLog("info", "JavaInstaller", `Java installed: ${p.file_name}`); - } else if (p.status === "Error") { - get().addLog("error", "JavaInstaller", `Java download error`); - } - }); - }, - - setLogs: (logs) => { - set({ logs }); - }, - - setSources: (sources) => { - set({ sources }); - }, -})); diff --git a/packages/ui-new/src/stores/releases-store.ts b/packages/ui-new/src/stores/releases-store.ts deleted file mode 100644 index 56afa08..0000000 --- a/packages/ui-new/src/stores/releases-store.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { create } from "zustand"; -import type { GithubRelease } from "@/types/bindings/core"; - -interface ReleasesState { - // State - releases: GithubRelease[]; - isLoading: boolean; - isLoaded: boolean; - error: string | null; - - // Actions - loadReleases: () => Promise<void>; - setReleases: (releases: GithubRelease[]) => void; - setIsLoading: (isLoading: boolean) => void; - setIsLoaded: (isLoaded: boolean) => void; - setError: (error: string | null) => void; -} - -export const useReleasesStore = create<ReleasesState>((set, get) => ({ - // Initial state - releases: [], - isLoading: false, - isLoaded: false, - error: null, - - // Actions - loadReleases: async () => { - const { isLoaded, isLoading } = get(); - - // If already loaded or currently loading, skip to prevent duplicate requests - if (isLoaded || isLoading) return; - - set({ isLoading: true, error: null }); - - try { - const releases = await invoke<GithubRelease[]>("get_github_releases"); - set({ releases, isLoaded: true }); - } catch (e) { - const error = e instanceof Error ? e.message : String(e); - console.error("Failed to load releases:", e); - set({ error }); - } finally { - set({ isLoading: false }); - } - }, - - setReleases: (releases) => { - set({ releases }); - }, - - setIsLoading: (isLoading) => { - set({ isLoading }); - }, - - setIsLoaded: (isLoaded) => { - set({ isLoaded }); - }, - - setError: (error) => { - set({ error }); - }, -})); diff --git a/packages/ui-new/src/stores/settings-store.ts b/packages/ui-new/src/stores/settings-store.ts deleted file mode 100644 index 0bfc1e1..0000000 --- a/packages/ui-new/src/stores/settings-store.ts +++ /dev/null @@ -1,568 +0,0 @@ -import { convertFileSrc, invoke } from "@tauri-apps/api/core"; -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { toast } from "sonner"; -import { create } from "zustand"; -import { downloadAdoptiumJava } from "@/client"; -import type { ModelInfo } from "../types/bindings/assistant"; -import type { LauncherConfig } from "../types/bindings/config"; -import type { - JavaDownloadProgress, - PendingJavaDownload, -} from "../types/bindings/downloader"; -import type { - JavaCatalog, - JavaInstallation, - JavaReleaseInfo, -} from "../types/bindings/java"; - -type JavaDownloadSource = "adoptium" | "mojang" | "azul"; - -/** - * State shape for settings store. - * - * Note: Uses camelCase naming to match ts-rs generated bindings (which now use - * `serde(rename_all = "camelCase")`). When reading raw binding objects from - * invoke, convert/mapping should be applied where necessary. - */ -interface SettingsState { - // State - settings: LauncherConfig; - javaInstallations: JavaInstallation[]; - isDetectingJava: boolean; - showJavaDownloadModal: boolean; - selectedDownloadSource: JavaDownloadSource; - javaCatalog: JavaCatalog | null; - isLoadingCatalog: boolean; - catalogError: string; - selectedMajorVersion: number | null; - selectedImageType: "jre" | "jdk"; - showOnlyRecommended: boolean; - searchQuery: string; - isDownloadingJava: boolean; - downloadProgress: JavaDownloadProgress | null; - javaDownloadStatus: string; - pendingDownloads: PendingJavaDownload[]; - ollamaModels: ModelInfo[]; - openaiModels: ModelInfo[]; - isLoadingOllamaModels: boolean; - isLoadingOpenaiModels: boolean; - ollamaModelsError: string; - openaiModelsError: string; - showConfigEditor: boolean; - rawConfigContent: string; - configFilePath: string; - configEditorError: string; - - // Computed / derived - backgroundUrl: string | undefined; - filteredReleases: JavaReleaseInfo[]; - availableMajorVersions: number[]; - installStatus: ( - version: number, - imageType: string, - ) => "installed" | "downloading" | "available"; - selectedRelease: JavaReleaseInfo | null; - currentModelOptions: Array<{ - value: string; - label: string; - details?: string; - }>; - - // Actions - loadSettings: () => Promise<void>; - saveSettings: () => Promise<void>; - // compatibility helper to mirror the older set({ key: value }) usage - set: (patch: Partial<Record<string, unknown>>) => void; - - detectJava: () => Promise<void>; - selectJava: (path: string) => void; - - openJavaDownloadModal: () => Promise<void>; - closeJavaDownloadModal: () => void; - loadJavaCatalog: (forceRefresh: boolean) => Promise<void>; - refreshCatalog: () => Promise<void>; - loadPendingDownloads: () => Promise<void>; - selectMajorVersion: (version: number) => void; - downloadJava: () => Promise<void>; - cancelDownload: () => Promise<void>; - resumeDownloads: () => Promise<void>; - - openConfigEditor: () => Promise<void>; - closeConfigEditor: () => void; - saveRawConfig: () => Promise<void>; - - loadOllamaModels: () => Promise<void>; - loadOpenaiModels: () => Promise<void>; - - setSetting: <K extends keyof LauncherConfig>( - key: K, - value: LauncherConfig[K], - ) => void; - setAssistantSetting: <K extends keyof LauncherConfig["assistant"]>( - key: K, - value: LauncherConfig["assistant"][K], - ) => void; - setFeatureFlag: <K extends keyof LauncherConfig["featureFlags"]>( - key: K, - value: LauncherConfig["featureFlags"][K], - ) => void; - - // Private - progressUnlisten: UnlistenFn | null; -} - -/** - * Default settings (camelCase) — lightweight defaults used until `get_settings` - * returns real values. - */ -const defaultSettings: LauncherConfig = { - minMemory: 1024, - maxMemory: 2048, - javaPath: "java", - width: 854, - height: 480, - downloadThreads: 32, - enableGpuAcceleration: false, - enableVisualEffects: true, - activeEffect: "constellation", - theme: "dark", - customBackgroundPath: null, - logUploadService: "paste.rs", - pastebinApiKey: null, - assistant: { - enabled: true, - llmProvider: "ollama", - ollamaEndpoint: "http://localhost:11434", - ollamaModel: "llama3", - openaiApiKey: null, - openaiEndpoint: "https://api.openai.com/v1", - openaiModel: "gpt-3.5-turbo", - systemPrompt: - "You are a helpful Minecraft expert assistant. You help players with game issues, mod installation, performance optimization, and gameplay tips. Analyze any game logs provided and give concise, actionable advice.", - responseLanguage: "auto", - ttsEnabled: false, - ttsProvider: "disabled", - }, - useSharedCaches: false, - keepLegacyPerInstanceStorage: true, - featureFlags: { - demoUser: false, - quickPlayEnabled: false, - quickPlayPath: null, - quickPlaySingleplayer: true, - quickPlayMultiplayerServer: null, - }, -}; - -export const useSettingsStore = create<SettingsState>((set, get) => ({ - // initial state - settings: defaultSettings, - javaInstallations: [], - isDetectingJava: false, - showJavaDownloadModal: false, - selectedDownloadSource: "adoptium", - javaCatalog: null, - isLoadingCatalog: false, - catalogError: "", - selectedMajorVersion: null, - selectedImageType: "jre", - showOnlyRecommended: true, - searchQuery: "", - isDownloadingJava: false, - downloadProgress: null, - javaDownloadStatus: "", - pendingDownloads: [], - ollamaModels: [], - openaiModels: [], - isLoadingOllamaModels: false, - isLoadingOpenaiModels: false, - ollamaModelsError: "", - openaiModelsError: "", - showConfigEditor: false, - rawConfigContent: "", - configFilePath: "", - configEditorError: "", - progressUnlisten: null, - - // derived getters - get backgroundUrl() { - const { settings } = get(); - if (settings.customBackgroundPath) { - return convertFileSrc(settings.customBackgroundPath); - } - return undefined; - }, - - get filteredReleases() { - const { - javaCatalog, - selectedMajorVersion, - selectedImageType, - showOnlyRecommended, - searchQuery, - } = get(); - - if (!javaCatalog) return []; - - let releases = javaCatalog.releases; - - if (selectedMajorVersion !== null) { - releases = releases.filter( - (r) => r.majorVersion === selectedMajorVersion, - ); - } - - releases = releases.filter((r) => r.imageType === selectedImageType); - - if (showOnlyRecommended) { - releases = releases.filter((r) => r.isLts); - } - - if (searchQuery.trim() !== "") { - const q = searchQuery.toLowerCase(); - releases = releases.filter( - (r) => - r.version.toLowerCase().includes(q) || - (r.releaseName ?? "").toLowerCase().includes(q), - ); - } - - // sort newest-first by parsed version number - return releases.sort((a, b) => { - const aVer = parseFloat(a.version.split("-")[0]); - const bVer = parseFloat(b.version.split("-")[0]); - return bVer - aVer; - }); - }, - - get availableMajorVersions() { - return get().javaCatalog?.availableMajorVersions || []; - }, - - installStatus: (version: number, imageType: string) => { - const { - javaInstallations, - pendingDownloads, - isDownloadingJava, - downloadProgress, - } = get(); - - const installed = javaInstallations.some( - (inst) => parseInt(inst.version.split(".")[0], 10) === version, - ); - if (installed) return "installed"; - - if ( - isDownloadingJava && - downloadProgress?.fileName?.includes(`${version}`) - ) { - return "downloading"; - } - - const pending = pendingDownloads.some( - (d) => d.majorVersion === version && d.imageType === imageType, - ); - if (pending) return "downloading"; - - return "available"; - }, - - get selectedRelease() { - const { javaCatalog, selectedMajorVersion, selectedImageType } = get(); - if (!javaCatalog || selectedMajorVersion === null) return null; - return ( - javaCatalog.releases.find( - (r) => - r.majorVersion === selectedMajorVersion && - r.imageType === selectedImageType, - ) || null - ); - }, - - get currentModelOptions() { - const { settings, ollamaModels, openaiModels } = get(); - const provider = settings.assistant.llmProvider; - if (provider === "ollama") { - return ollamaModels.map((m) => ({ - value: m.id, - label: m.name, - details: m.details || m.size || "", - })); - } else { - return openaiModels.map((m) => ({ - value: m.id, - label: m.name, - details: m.details || "", - })); - } - }, - - // actions - loadSettings: async () => { - try { - const result = await invoke<LauncherConfig>("get_settings"); - // result already uses camelCase fields from bindings - set({ settings: result }); - - // enforce dark theme at app-level if necessary - if (result.theme !== "dark") { - const updated = { ...result, theme: "dark" } as LauncherConfig; - set({ settings: updated }); - await invoke("save_settings", { config: updated }); - } - - // ensure customBackgroundPath is undefined rather than null for reactiveness - if (!result.customBackgroundPath) { - set((s) => ({ - settings: { ...s.settings, customBackgroundPath: null }, - })); - } - } catch (e) { - console.error("Failed to load settings:", e); - } - }, - - saveSettings: async () => { - try { - const { settings } = get(); - - // Clean up empty strings to null where appropriate - if ((settings.customBackgroundPath ?? "") === "") { - set((state) => ({ - settings: { ...state.settings, customBackgroundPath: null }, - })); - } - - await invoke("save_settings", { config: settings }); - toast.success("Settings saved!"); - } catch (e) { - console.error("Failed to save settings:", e); - toast.error(`Error saving settings: ${String(e)}`); - } - }, - - set: (patch: Partial<Record<string, unknown>>) => { - set(patch); - }, - - detectJava: async () => { - set({ isDetectingJava: true }); - try { - const installs = await invoke<JavaInstallation[]>("detect_java"); - set({ javaInstallations: installs }); - if (installs.length === 0) toast.info("No Java installations found"); - else toast.success(`Found ${installs.length} Java installation(s)`); - } catch (e) { - console.error("Failed to detect Java:", e); - toast.error(`Error detecting Java: ${String(e)}`); - } finally { - set({ isDetectingJava: false }); - } - }, - - selectJava: (path: string) => { - set((s) => ({ settings: { ...s.settings, javaPath: path } })); - }, - - openJavaDownloadModal: async () => { - set({ - showJavaDownloadModal: true, - javaDownloadStatus: "", - catalogError: "", - downloadProgress: null, - }); - - // attach event listener for download progress - const state = get(); - if (state.progressUnlisten) { - state.progressUnlisten(); - } - - const unlisten = await listen<JavaDownloadProgress>( - "java-download-progress", - (event) => { - set({ downloadProgress: event.payload }); - }, - ); - - set({ progressUnlisten: unlisten }); - - // load catalog and pending downloads - await get().loadJavaCatalog(false); - await get().loadPendingDownloads(); - }, - - closeJavaDownloadModal: () => { - const { isDownloadingJava, progressUnlisten } = get(); - - if (!isDownloadingJava) { - set({ showJavaDownloadModal: false }); - if (progressUnlisten) { - try { - progressUnlisten(); - } catch { - // ignore - } - set({ progressUnlisten: null }); - } - } - }, - - loadJavaCatalog: async (forceRefresh: boolean) => { - set({ isLoadingCatalog: true, catalogError: "" }); - try { - const cmd = forceRefresh ? "refresh_java_catalog" : "get_java_catalog"; - const result = await invoke<JavaCatalog>(cmd); - set({ javaCatalog: result, isLoadingCatalog: false }); - } catch (e) { - console.error("Failed to load Java catalog:", e); - set({ catalogError: String(e), isLoadingCatalog: false }); - } - }, - - refreshCatalog: async () => { - await get().loadJavaCatalog(true); - }, - - loadPendingDownloads: async () => { - try { - const pending = await invoke<PendingJavaDownload[]>( - "get_pending_java_downloads", - ); - set({ pendingDownloads: pending }); - } catch (e) { - console.error("Failed to load pending downloads:", e); - } - }, - - selectMajorVersion: (version: number) => { - set({ selectedMajorVersion: version }); - }, - - downloadJava: async () => { - const { selectedMajorVersion, selectedImageType, selectedDownloadSource } = - get(); - if (!selectedMajorVersion) return; - set({ isDownloadingJava: true, javaDownloadStatus: "Starting..." }); - try { - const result = await downloadAdoptiumJava( - selectedMajorVersion, - selectedImageType, - selectedDownloadSource, - ); - set({ - javaDownloadStatus: `Java ${selectedMajorVersion} download started: ${result.path}`, - }); - toast.success("Download started"); - } catch (e) { - console.error("Failed to download Java:", e); - toast.error(`Failed to start Java download: ${String(e)}`); - } finally { - set({ isDownloadingJava: false }); - } - }, - - cancelDownload: async () => { - try { - await invoke("cancel_java_download"); - toast.success("Cancelled Java download"); - set({ isDownloadingJava: false, javaDownloadStatus: "" }); - } catch (e) { - console.error("Failed to cancel download:", e); - toast.error(`Failed to cancel download: ${String(e)}`); - } - }, - - resumeDownloads: async () => { - try { - const installed = await invoke<boolean>("resume_java_downloads"); - if (installed) toast.success("Resumed Java downloads"); - else toast.info("No downloads to resume"); - } catch (e) { - console.error("Failed to resume downloads:", e); - toast.error(`Failed to resume downloads: ${String(e)}`); - } - }, - - openConfigEditor: async () => { - try { - const path = await invoke<string>("get_config_path"); - const content = await invoke<string>("read_config_raw"); - set({ - configFilePath: path, - rawConfigContent: content, - showConfigEditor: true, - }); - } catch (e) { - console.error("Failed to open config editor:", e); - set({ configEditorError: String(e) }); - } - }, - - closeConfigEditor: () => { - set({ - showConfigEditor: false, - rawConfigContent: "", - configFilePath: "", - configEditorError: "", - }); - }, - - saveRawConfig: async () => { - try { - await invoke("write_config_raw", { content: get().rawConfigContent }); - toast.success("Config saved"); - set({ showConfigEditor: false }); - } catch (e) { - console.error("Failed to save config:", e); - set({ configEditorError: String(e) }); - toast.error(`Failed to save config: ${String(e)}`); - } - }, - - loadOllamaModels: async () => { - set({ isLoadingOllamaModels: true, ollamaModelsError: "" }); - try { - const models = await invoke<ModelInfo[]>("get_ollama_models"); - set({ ollamaModels: models, isLoadingOllamaModels: false }); - } catch (e) { - console.error("Failed to load Ollama models:", e); - set({ isLoadingOllamaModels: false, ollamaModelsError: String(e) }); - } - }, - - loadOpenaiModels: async () => { - set({ isLoadingOpenaiModels: true, openaiModelsError: "" }); - try { - const models = await invoke<ModelInfo[]>("get_openai_models"); - set({ openaiModels: models, isLoadingOpenaiModels: false }); - } catch (e) { - console.error("Failed to load OpenAI models:", e); - set({ isLoadingOpenaiModels: false, openaiModelsError: String(e) }); - } - }, - - setSetting: (key, value) => { - set((s) => ({ - settings: { ...s.settings, [key]: value } as unknown as LauncherConfig, - })); - }, - - setAssistantSetting: (key, value) => { - set((s) => ({ - settings: { - ...s.settings, - assistant: { ...s.settings.assistant, [key]: value }, - } as LauncherConfig, - })); - }, - - setFeatureFlag: (key, value) => { - set((s) => ({ - settings: { - ...s.settings, - featureFlags: { ...s.settings.featureFlags, [key]: value }, - } as LauncherConfig, - })); - }, -})); diff --git a/packages/ui-new/src/stores/ui-store.ts b/packages/ui-new/src/stores/ui-store.ts deleted file mode 100644 index 89b9191..0000000 --- a/packages/ui-new/src/stores/ui-store.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { create } from "zustand"; - -export type ViewType = "home" | "versions" | "settings" | "guide" | "instances"; - -interface UIState { - // State - currentView: ViewType; - showConsole: boolean; - appVersion: string; - - // Actions - toggleConsole: () => void; - setView: (view: ViewType) => void; - setAppVersion: (version: string) => void; -} - -export const useUIStore = create<UIState>((set) => ({ - // Initial state - currentView: "home", - showConsole: false, - appVersion: "...", - - // Actions - toggleConsole: () => { - set((state) => ({ showConsole: !state.showConsole })); - }, - - setView: (view: ViewType) => { - set({ currentView: view }); - }, - - setAppVersion: (version: string) => { - set({ appVersion: version }); - }, -})); - -// Provide lowercase alias for compatibility with existing imports. -// Use a function wrapper to ensure the named export exists as a callable value -// at runtime (some bundlers/tree-shakers may remove simple aliases). -export function useUiStore() { - return useUIStore(); -} diff --git a/packages/ui-new/src/types/bindings/account.ts b/packages/ui-new/src/types/bindings/account.ts deleted file mode 100644 index 168d138..0000000 --- a/packages/ui-new/src/types/bindings/account.ts +++ /dev/null @@ -1,28 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { OfflineAccount } from "./auth"; - -export type AccountStorage = { file_path: string }; - -/** - * Stored account data for persistence - */ -export type AccountStore = { - accounts: Array<StoredAccount>; - active_account_id: string | null; -}; - -export type StoredAccount = - | ({ type: "Offline" } & OfflineAccount) - | ({ type: "Microsoft" } & StoredMicrosoftAccount); - -/** - * Microsoft account with refresh token for persistence - */ -export type StoredMicrosoftAccount = { - username: string; - uuid: string; - access_token: string; - refresh_token: string | null; - ms_refresh_token: string | null; - expires_at: bigint; -}; diff --git a/packages/ui-new/src/types/bindings/assistant.ts b/packages/ui-new/src/types/bindings/assistant.ts deleted file mode 100644 index 827f008..0000000 --- a/packages/ui-new/src/types/bindings/assistant.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type GenerationStats = { - totalDuration: bigint; - loadDuration: bigint; - promptEvalCount: bigint; - promptEvalDuration: bigint; - evalCount: bigint; - evalDuration: bigint; -}; - -export type Message = { role: string; content: string }; - -export type ModelInfo = { - id: string; - name: string; - size: string | null; - details: string | null; -}; - -export type StreamChunk = { - content: string; - done: boolean; - stats: GenerationStats | null; -}; diff --git a/packages/ui-new/src/types/bindings/auth.ts b/packages/ui-new/src/types/bindings/auth.ts deleted file mode 100644 index 563a924..0000000 --- a/packages/ui-new/src/types/bindings/auth.ts +++ /dev/null @@ -1,32 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type Account = - | ({ type: "offline" } & OfflineAccount) - | ({ type: "microsoft" } & MicrosoftAccount); - -export type DeviceCodeResponse = { - userCode: string; - deviceCode: string; - verificationUri: string; - expiresIn: bigint; - interval: bigint; - message: string | null; -}; - -export type MicrosoftAccount = { - username: string; - uuid: string; - accessToken: string; - refreshToken: string | null; - expiresAt: bigint; -}; - -export type MinecraftProfile = { id: string; name: string }; - -export type OfflineAccount = { username: string; uuid: string }; - -export type TokenResponse = { - access_token: string; - refresh_token: string | null; - expires_in: bigint; -}; diff --git a/packages/ui-new/src/types/bindings/config.ts b/packages/ui-new/src/types/bindings/config.ts deleted file mode 100644 index e9de4f5..0000000 --- a/packages/ui-new/src/types/bindings/config.ts +++ /dev/null @@ -1,61 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AssistantConfig = { - enabled: boolean; - llmProvider: string; - ollamaEndpoint: string; - ollamaModel: string; - openaiApiKey: string | null; - openaiEndpoint: string; - openaiModel: string; - systemPrompt: string; - responseLanguage: string; - ttsEnabled: boolean; - ttsProvider: string; -}; - -/** - * Feature-gated arguments configuration - */ -export type FeatureFlags = { - /** - * Demo user: enables demo-related arguments when rules require it - */ - demoUser: boolean; - /** - * Quick Play: enable quick play arguments - */ - quickPlayEnabled: boolean; - /** - * Quick Play singleplayer world path (if provided) - */ - quickPlayPath: string | null; - /** - * Quick Play singleplayer flag - */ - quickPlaySingleplayer: boolean; - /** - * Quick Play multiplayer server address (optional) - */ - quickPlayMultiplayerServer: string | null; -}; - -export type LauncherConfig = { - minMemory: number; - maxMemory: number; - javaPath: string; - width: number; - height: number; - downloadThreads: number; - customBackgroundPath: string | null; - enableGpuAcceleration: boolean; - enableVisualEffects: boolean; - activeEffect: string; - theme: string; - logUploadService: string; - pastebinApiKey: string | null; - assistant: AssistantConfig; - useSharedCaches: boolean; - keepLegacyPerInstanceStorage: boolean; - featureFlags: FeatureFlags; -}; diff --git a/packages/ui-new/src/types/bindings/core.ts b/packages/ui-new/src/types/bindings/core.ts deleted file mode 100644 index 94e3bde..0000000 --- a/packages/ui-new/src/types/bindings/core.ts +++ /dev/null @@ -1,47 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * File information for instance file browser - */ -export type FileInfo = { - name: string; - path: string; - isDirectory: boolean; - size: bigint; - modified: bigint; -}; - -export type GithubRelease = { - tagName: string; - name: string; - publishedAt: string; - body: string; - htmlUrl: string; -}; - -/** - * Installed version info - */ -export type InstalledVersion = { id: string; type: string }; - -/** - * Migrate instance caches to shared global caches - */ -export type MigrationResult = { - movedFiles: number; - hardlinks: number; - copies: number; - savedBytes: bigint; - savedMb: number; -}; - -export type PastebinResponse = { url: string }; - -/** - * Version metadata for display in the UI - */ -export type VersionMetadata = { - id: string; - javaVersion: bigint | null; - isInstalled: boolean; -}; diff --git a/packages/ui-new/src/types/bindings/downloader.ts b/packages/ui-new/src/types/bindings/downloader.ts deleted file mode 100644 index f2be278..0000000 --- a/packages/ui-new/src/types/bindings/downloader.ts +++ /dev/null @@ -1,73 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Metadata for resumable downloads stored in .part.meta file - */ -export type DownloadMetadata = { - url: string; - fileName: string; - totalSize: bigint; - downloadedBytes: bigint; - checksum: string | null; - timestamp: bigint; - segments: Array<DownloadSegment>; -}; - -/** - * Download queue for persistence - */ -export type DownloadQueue = { pendingDownloads: Array<PendingJavaDownload> }; - -/** - * A download segment for multi-segment parallel downloading - */ -export type DownloadSegment = { - start: bigint; - end: bigint; - downloaded: bigint; - completed: boolean; -}; - -export type DownloadTask = { - url: string; - path: string; - sha1: string | null; - sha256: string | null; -}; - -/** - * Progress event for Java download - */ -export type JavaDownloadProgress = { - fileName: string; - downloadedBytes: bigint; - totalBytes: bigint; - speedBytesPerSec: bigint; - etaSeconds: bigint; - status: string; - percentage: number; -}; - -/** - * Pending download task for queue persistence - */ -export type PendingJavaDownload = { - majorVersion: number; - imageType: string; - downloadUrl: string; - fileName: string; - fileSize: bigint; - checksum: string | null; - installPath: string; - createdAt: bigint; -}; - -export type ProgressEvent = { - file: string; - downloaded: bigint; - total: bigint; - status: string; - completedFiles: number; - totalFiles: number; - totalDownloadedBytes: bigint; -}; diff --git a/packages/ui-new/src/types/bindings/fabric.ts b/packages/ui-new/src/types/bindings/fabric.ts deleted file mode 100644 index 181f8be..0000000 --- a/packages/ui-new/src/types/bindings/fabric.ts +++ /dev/null @@ -1,74 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Represents a Minecraft version supported by Fabric. - */ -export type FabricGameVersion = { version: string; stable: boolean }; - -/** - * Represents a Fabric intermediary mapping version. - */ -export type FabricIntermediaryVersion = { - maven: string; - version: string; - stable: boolean; -}; - -/** - * Launcher metadata from Fabric Meta API. - */ -export type FabricLauncherMeta = { - version: number; - libraries: FabricLibraries; - mainClass: FabricMainClass; -}; - -/** - * Libraries required by Fabric loader. - */ -export type FabricLibraries = { - client: Array<FabricLibrary>; - common: Array<FabricLibrary>; - server: Array<FabricLibrary>; -}; - -/** - * A single Fabric library dependency. - */ -export type FabricLibrary = { name: string; url: string | null }; - -/** - * Represents a combined loader + intermediary version entry. - */ -export type FabricLoaderEntry = { - loader: FabricLoaderVersion; - intermediary: FabricIntermediaryVersion; - launcherMeta: FabricLauncherMeta; -}; - -/** - * Represents a Fabric loader version from the Meta API. - */ -export type FabricLoaderVersion = { - separator: string; - build: number; - maven: string; - version: string; - stable: boolean; -}; - -/** - * Main class configuration for Fabric. - * Can be either a struct with client/server fields or a simple string. - */ -export type FabricMainClass = { client: string; server: string } | string; - -/** - * Information about an installed Fabric version. - */ -export type InstalledFabricVersion = { - id: string; - minecraftVersion: string; - loaderVersion: string; - path: string; -}; diff --git a/packages/ui-new/src/types/bindings/forge.ts b/packages/ui-new/src/types/bindings/forge.ts deleted file mode 100644 index a9790e7..0000000 --- a/packages/ui-new/src/types/bindings/forge.ts +++ /dev/null @@ -1,21 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Represents a Forge version entry. - */ -export type ForgeVersion = { - version: string; - minecraftVersion: string; - recommended: boolean; - latest: boolean; -}; - -/** - * Information about an installed Forge version. - */ -export type InstalledForgeVersion = { - id: string; - minecraftVersion: string; - forgeVersion: string; - path: string; -}; diff --git a/packages/ui-new/src/types/bindings/game-version.ts b/packages/ui-new/src/types/bindings/game-version.ts deleted file mode 100644 index 1b1c395..0000000 --- a/packages/ui-new/src/types/bindings/game-version.ts +++ /dev/null @@ -1,89 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type Arguments = { - game: Record<string, unknown>; - jvm: Record<string, unknown>; -}; - -export type AssetIndex = { - id: string; - sha1: string; - size: bigint; - url: string; - totalSize: bigint | null; -}; - -export type DownloadArtifact = { - sha1: string | null; - size: bigint | null; - url: string; - path: string | null; -}; - -export type Downloads = { - client: DownloadArtifact; - server: DownloadArtifact | null; -}; - -/** - * Represents a Minecraft version JSON, supporting both vanilla and modded (Fabric/Forge) formats. - * Modded versions use `inheritsFrom` to reference a parent vanilla version. - */ -export type GameVersion = { - id: string; - /** - * Optional for mod loaders that inherit from vanilla - */ - downloads: Downloads | null; - /** - * Optional for mod loaders that inherit from vanilla - */ - assetIndex: AssetIndex | null; - libraries: Array<Library>; - mainClass: string; - minecraftArguments: string | null; - arguments: Arguments | null; - javaVersion: JavaVersion | null; - /** - * For mod loaders: the vanilla version this inherits from - */ - inheritsFrom: string | null; - /** - * Fabric/Forge may specify a custom assets version - */ - assets: string | null; - /** - * Release type (release, snapshot, old_beta, etc.) - */ - type: string | null; -}; - -export type JavaVersion = { component: string; majorVersion: bigint }; - -export type Library = { - downloads: LibraryDownloads | null; - name: string; - rules: Array<Rule> | null; - natives: Record<string, unknown>; - /** - * Maven repository URL for mod loader libraries - */ - url: string | null; -}; - -export type LibraryDownloads = { - artifact: DownloadArtifact | null; - classifiers: Record<string, unknown>; -}; - -export type OsRule = { - name: string | null; - version: string | null; - arch: string | null; -}; - -export type Rule = { - action: string; - os: OsRule | null; - features: Record<string, unknown>; -}; diff --git a/packages/ui-new/src/types/bindings/index.ts b/packages/ui-new/src/types/bindings/index.ts deleted file mode 100644 index 9bde037..0000000 --- a/packages/ui-new/src/types/bindings/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from "./account"; -export * from "./assistant"; -export * from "./auth"; -export * from "./config"; -export * from "./core"; -export * from "./downloader"; -export * from "./fabric"; -export * from "./forge"; -export * from "./game-version"; -export * from "./instance"; -export * from "./java"; -export * from "./manifest"; diff --git a/packages/ui-new/src/types/bindings/instance.ts b/packages/ui-new/src/types/bindings/instance.ts deleted file mode 100644 index 2c4f8ae..0000000 --- a/packages/ui-new/src/types/bindings/instance.ts +++ /dev/null @@ -1,33 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Represents a game instance/profile - */ -export type Instance = { - id: string; - name: string; - gameDir: string; - versionId: string | null; - createdAt: bigint; - lastPlayed: bigint | null; - iconPath: string | null; - notes: string | null; - modLoader: string | null; - modLoaderVersion: string | null; - jvmArgsOverride: string | null; - memoryOverride: MemoryOverride | null; - javaPathOverride: string | null; -}; - -/** - * Configuration for all instances - */ -export type InstanceConfig = { - instances: Array<Instance>; - activeInstanceId: string | null; -}; - -/** - * Memory settings override for an instance - */ -export type MemoryOverride = { min: number; max: number }; diff --git a/packages/ui-new/src/types/bindings/java/core.ts b/packages/ui-new/src/types/bindings/java/core.ts deleted file mode 100644 index 099dea9..0000000 --- a/packages/ui-new/src/types/bindings/java/core.ts +++ /dev/null @@ -1,41 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type JavaCatalog = { - releases: Array<JavaReleaseInfo>; - availableMajorVersions: Array<number>; - ltsVersions: Array<number>; - cachedAt: bigint; -}; - -export type JavaDownloadInfo = { - version: string; - release_name: string; - download_url: string; - file_name: string; - file_size: bigint; - checksum: string | null; - image_type: string; -}; - -export type JavaInstallation = { - path: string; - version: string; - arch: string; - vendor: string; - source: string; - is64bit: boolean; -}; - -export type JavaReleaseInfo = { - majorVersion: number; - imageType: string; - version: string; - releaseName: string; - releaseDate: string | null; - fileSize: bigint; - checksum: string | null; - downloadUrl: string; - isLts: boolean; - isAvailable: boolean; - architecture: string; -}; diff --git a/packages/ui-new/src/types/bindings/java/index.ts b/packages/ui-new/src/types/bindings/java/index.ts deleted file mode 100644 index 2f2754c..0000000 --- a/packages/ui-new/src/types/bindings/java/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./core"; -export * from "./persistence"; -export * from "./providers"; diff --git a/packages/ui-new/src/types/bindings/java/persistence.ts b/packages/ui-new/src/types/bindings/java/persistence.ts deleted file mode 100644 index 7a2b576..0000000 --- a/packages/ui-new/src/types/bindings/java/persistence.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type JavaConfig = { - user_defined_paths: Array<string>; - preferred_java_path: string | null; - last_detection_time: bigint; -}; diff --git a/packages/ui-new/src/types/bindings/java/providers/adoptium.ts b/packages/ui-new/src/types/bindings/java/providers/adoptium.ts deleted file mode 100644 index 65fc42b..0000000 --- a/packages/ui-new/src/types/bindings/java/providers/adoptium.ts +++ /dev/null @@ -1,37 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type AdoptiumAsset = { - binary: AdoptiumBinary; - release_name: string; - version: AdoptiumVersionData; -}; - -export type AdoptiumBinary = { - os: string; - architecture: string; - image_type: string; - package: AdoptiumPackage; - updated_at: string | null; -}; - -export type AdoptiumPackage = { - name: string; - link: string; - size: bigint; - checksum: string | null; -}; - -export type AdoptiumVersionData = { - major: number; - minor: number; - security: number; - semver: string; - openjdk_version: string; -}; - -export type AvailableReleases = { - available_releases: Array<number>; - available_lts_releases: Array<number>; - most_recent_lts: number | null; - most_recent_feature_release: number | null; -}; diff --git a/packages/ui-new/src/types/bindings/java/providers/index.ts b/packages/ui-new/src/types/bindings/java/providers/index.ts deleted file mode 100644 index 3e28711..0000000 --- a/packages/ui-new/src/types/bindings/java/providers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./adoptium"; diff --git a/packages/ui-new/src/types/bindings/manifest.ts b/packages/ui-new/src/types/bindings/manifest.ts deleted file mode 100644 index 2180962..0000000 --- a/packages/ui-new/src/types/bindings/manifest.ts +++ /dev/null @@ -1,22 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -export type Latest = { release: string; snapshot: string }; - -export type Version = { - id: string; - type: string; - url: string; - time: string; - releaseTime: string; - /** - * Java version requirement (major version number) - * This is populated from the version JSON file if the version is installed locally - */ - javaVersion: bigint | null; - /** - * Whether this version is installed locally - */ - isInstalled: boolean | null; -}; - -export type VersionManifest = { latest: Latest; versions: Array<Version> }; diff --git a/packages/ui-new/src/types/index.ts b/packages/ui-new/src/types/index.ts deleted file mode 100644 index 9e592d7..0000000 --- a/packages/ui-new/src/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./bindings"; |