aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/ui/CHANGELOG.md16
-rw-r--r--packages/ui/package.json2
-rw-r--r--packages/ui/src/components/bottom-bar.tsx19
-rw-r--r--packages/ui/src/components/sidebar.tsx27
-rw-r--r--packages/ui/src/components/ui/radio-group.tsx36
-rw-r--r--packages/ui/src/main.tsx2
-rw-r--r--packages/ui/src/models/instance.ts10
-rw-r--r--packages/ui/src/models/java.ts25
-rw-r--r--packages/ui/src/pages/instances-view.tsx214
-rw-r--r--packages/ui/src/pages/settings.tsx90
-rw-r--r--packages/ui/vite.config.ts2
11 files changed, 305 insertions, 138 deletions
diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md
index bc780fb..d9e5b4d 100644
--- a/packages/ui/CHANGELOG.md
+++ b/packages/ui/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## v0.1.0-alpha.3
+
+### Refactors
+
+- [`24a229e`](https://github.com/HydroRoll-Team/DropOut/commit/24a229ede321e8296ea99b332ccfa61213791d10): Partial rewrite layout of instances page.
+
+### Bug Fixes
+
+- [`9e40b5b`](https://github.com/HydroRoll-Team/DropOut/commit/9e40b5b7bea60e6802a4b448ef315b14fba4de7f): Auto select game version if version is unique.
+
+### New Features
+
+- [`0ac743f`](https://github.com/HydroRoll-Team/DropOut/commit/0ac743f6d126d047352e6b247ea1ee513361d240): Improve sidebar avatar on large and small screens.
+- [`9e40b5b`](https://github.com/HydroRoll-Team/DropOut/commit/9e40b5b7bea60e6802a4b448ef315b14fba4de7f): Support detect and select java path.
+- [`47aeabf`](https://github.com/HydroRoll-Team/DropOut/commit/47aeabf5d44d7483101d30d289cb4c56761e3faa): Improve position and colors of the UI toast.
+
## v0.1.0-alpha.2
### Chores
diff --git a/packages/ui/package.json b/packages/ui/package.json
index b85f887..42705f8 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "@dropout/ui",
"private": true,
- "version": "0.1.0-alpha.2",
+ "version": "0.1.0-alpha.3",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/packages/ui/src/components/bottom-bar.tsx b/packages/ui/src/components/bottom-bar.tsx
index 5489675..0710c3a 100644
--- a/packages/ui/src/components/bottom-bar.tsx
+++ b/packages/ui/src/components/bottom-bar.tsx
@@ -6,7 +6,6 @@ import { listInstalledVersions, startGame } from "@/client";
import { cn } from "@/lib/utils";
import { useAuthStore } from "@/models/auth";
import { useInstanceStore } from "@/models/instance";
-import { useGameStore } from "@/stores/game-store";
import { LoginModal } from "./login-modal";
import { Button } from "./ui/button";
import {
@@ -26,7 +25,6 @@ interface InstalledVersion {
export function BottomBar() {
const authStore = useAuthStore();
- const gameStore = useGameStore();
const instancesStore = useInstanceStore();
const [isLaunched, setIsLaunched] = useState<boolean>(false);
@@ -51,24 +49,18 @@ export function BottomBar() {
const versions = await listInstalledVersions(
instancesStore.activeInstance.id,
);
-
- const installed = versions || [];
- setInstalledVersions(installed);
+ setInstalledVersions(versions);
// 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);
+ if (!selectedVersion && versions.length > 0) {
+ setSelectedVersion(versions[0].id);
}
} catch (error) {
console.error("Failed to load installed versions:", error);
} finally {
setIsLoadingVersions(false);
}
- }, [
- instancesStore.activeInstance,
- gameStore.selectedVersion,
- gameStore.setSelectedVersion,
- ]);
+ }, [instancesStore.activeInstance, selectedVersion]);
useEffect(() => {
loadInstalledVersions();
@@ -225,6 +217,7 @@ export function BottomBar() {
</div>
<Select
+ value={selectedVersion}
items={versionOptions}
onValueChange={setSelectedVersion}
disabled={isLoadingVersions}
@@ -238,7 +231,7 @@ export function BottomBar() {
}
/>
</SelectTrigger>
- <SelectContent>
+ <SelectContent alignItemWithTrigger={false}>
<SelectGroup>
{versionOptions.map((item) => (
<SelectItem
diff --git a/packages/ui/src/components/sidebar.tsx b/packages/ui/src/components/sidebar.tsx
index 0147b0a..d81156f 100644
--- a/packages/ui/src/components/sidebar.tsx
+++ b/packages/ui/src/components/sidebar.tsx
@@ -49,12 +49,33 @@ function NavItem({ Icon, label, to }: NavItemProps) {
export function Sidebar() {
const authStore = useAuthStore();
+ const renderUserAvatar = () => {
+ return (
+ <div className="w-full flex flex-col items-center hover:bg-accent/90 transition-colors cursor-pointer">
+ <div className="lg:hidden">
+ <UserAvatar />
+ </div>
+ <div className="w-full hidden lg:flex bg-accent/90 p-3 flex-row space-x-3">
+ <UserAvatar />
+ <div className="">
+ <p className="text-sm font-medium text-white">
+ {authStore.account?.username}
+ </p>
+ <p className="text-xs text-zinc-400">
+ {authStore.account?.type === "microsoft" ? "Online" : "Offline"}
+ </p>
+ </div>
+ </div>
+ </div>
+ );
+ };
+
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",
+ "w-20 lg:w-64 shrink-0 pt-6 pb-6 lg:pb-3 h-full",
)}
>
{/* Logo Area */}
@@ -162,9 +183,9 @@ export function Sidebar() {
<NavItem Icon={Settings} label="Settings" to="/settings" />
</nav>
- <div className="flex-1 flex flex-col justify-end">
+ <div className="w-full lg:px-3 flex-1 flex flex-col justify-end">
<DropdownMenu>
- <DropdownMenuTrigger render={<UserAvatar />}>
+ <DropdownMenuTrigger render={renderUserAvatar()} className="w-full">
Open
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="right" sideOffset={20}>
diff --git a/packages/ui/src/components/ui/radio-group.tsx b/packages/ui/src/components/ui/radio-group.tsx
new file mode 100644
index 0000000..d8b39dd
--- /dev/null
+++ b/packages/ui/src/components/ui/radio-group.tsx
@@ -0,0 +1,36 @@
+import { Radio as RadioPrimitive } from "@base-ui/react/radio"
+import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group"
+
+import { cn } from "@/lib/utils"
+
+function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
+ return (
+ <RadioGroupPrimitive
+ data-slot="radio-group"
+ className={cn("grid w-full gap-2", className)}
+ {...props}
+ />
+ )
+}
+
+function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
+ return (
+ <RadioPrimitive.Root
+ data-slot="radio-group-item"
+ 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 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 dark:aria-invalid:border-destructive/50 group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3",
+ className
+ )}
+ {...props}
+ >
+ <RadioPrimitive.Indicator
+ data-slot="radio-group-indicator"
+ className="flex size-4 items-center justify-center"
+ >
+ <span className="bg-primary-foreground absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full" />
+ </RadioPrimitive.Indicator>
+ </RadioPrimitive.Root>
+ )
+}
+
+export { RadioGroup, RadioGroupItem }
diff --git a/packages/ui/src/main.tsx b/packages/ui/src/main.tsx
index a3157bd..c5cbfc8 100644
--- a/packages/ui/src/main.tsx
+++ b/packages/ui/src/main.tsx
@@ -33,6 +33,6 @@ const root = createRoot(document.getElementById("root") as HTMLElement);
root.render(
<StrictMode>
<RouterProvider router={router} />
- <Toaster />
+ <Toaster position="top-right" richColors />
</StrictMode>,
);
diff --git a/packages/ui/src/models/instance.ts b/packages/ui/src/models/instance.ts
index a3fda3d..b1b463e 100644
--- a/packages/ui/src/models/instance.ts
+++ b/packages/ui/src/models/instance.ts
@@ -96,14 +96,8 @@ export const useInstanceStore = create<InstanceState>((set, get) => ({
},
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");
- }
+ await setActiveInstance(instance.id);
+ set({ activeInstance: instance });
},
duplicate: async (id, newName) => {
diff --git a/packages/ui/src/models/java.ts b/packages/ui/src/models/java.ts
new file mode 100644
index 0000000..3e5d2d0
--- /dev/null
+++ b/packages/ui/src/models/java.ts
@@ -0,0 +1,25 @@
+import { create } from "zustand/react";
+import { detectJava, refreshJavaCatalog } from "@/client";
+import type { JavaCatalog, JavaInstallation } from "@/types";
+
+export interface JavaState {
+ catalog: JavaCatalog | null;
+ installations: JavaInstallation[] | null;
+
+ refresh: () => Promise<void>;
+ refreshInstallations: () => Promise<void>;
+}
+
+export const useJavaStore = create<JavaState>((set) => ({
+ catalog: null,
+ installations: null,
+
+ refresh: async () => {
+ const catalog = await refreshJavaCatalog();
+ set({ catalog });
+ },
+ refreshInstallations: async () => {
+ const installations = await detectJava();
+ set({ installations });
+ },
+}));
diff --git a/packages/ui/src/pages/instances-view.tsx b/packages/ui/src/pages/instances-view.tsx
index 1634905..e99004c 100644
--- a/packages/ui/src/pages/instances-view.tsx
+++ b/packages/ui/src/pages/instances-view.tsx
@@ -1,5 +1,7 @@
-import { Copy, Edit2, Plus, Trash2 } from "lucide-react";
+import { CopyIcon, EditIcon, Plus, RocketIcon, Trash2Icon } from "lucide-react";
import { useEffect, useState } from "react";
+import { toast } from "sonner";
+import { startGame } from "@/client";
import InstanceCreationModal from "@/components/instance-creation-modal";
import InstanceEditorModal from "@/components/instance-editor-modal";
import { Button } from "@/components/ui/button";
@@ -12,9 +14,9 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
-import { toNumber } from "@/lib/tsrs-utils";
+import { cn } from "@/lib/utils";
import { useInstanceStore } from "@/models/instance";
-import type { Instance } from "../types/bindings/instance";
+import type { Instance } from "@/types";
export function InstancesView() {
const instancesStore = useInstanceStore();
@@ -76,21 +78,6 @@ export function InstancesView() {
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">
@@ -115,7 +102,7 @@ export function InstancesView() {
</div>
</div>
) : (
- <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ <ul className="flex flex-col space-y-3">
{instancesStore.instances.map((instance) => {
const isActive = instancesStore.activeInstance?.id === instance.id;
@@ -123,98 +110,109 @@ export function InstancesView() {
<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`}
+ onKeyDown={async (e) => {
+ if (e.key === "Enter") {
+ try {
+ await instancesStore.setActiveInstance(instance);
+ } catch (e) {
+ console.error("Failed to set active instance:", e);
+ toast.error("Error setting active instance");
+ }
+ }
+ }}
+ className="cursor-pointer"
>
- {/* 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>
+ <div
+ className={cn(
+ "flex flex-row space-x-3 p-3 justify-between",
+ "border bg-card/5 backdrop-blur-xl",
+ "hover:bg-accent/50 transition-colors",
+ isActive && "border-primary",
)}
+ >
+ <div className="flex flex-row space-x-4">
+ {instance.iconPath ? (
+ <div className="w-12 h-12 rounded overflow-hidden">
+ <img
+ src={instance.iconPath}
+ alt={instance.name}
+ className="w-full h-full object-cover"
+ />
+ </div>
+ ) : (
+ <div className="w-12 h-12 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>
+ )}
+
+ <div className="flex flex-col">
+ <h3 className="text-lg font-semibold">{instance.name}</h3>
+ {instance.versionId ? (
+ <p className="text-sm text-muted-foreground">
+ {instance.versionId}
+ </p>
+ ) : (
+ <p className="text-sm text-muted-foreground">
+ No version selected
+ </p>
+ )}
+ </div>
+ </div>
- <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 className="flex items-center">
+ <div className="flex flex-row space-x-2">
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={async () => {
+ if (!instance.versionId) {
+ toast.error("No version selected or installed");
+ return;
+ }
+ try {
+ await startGame(instance.id, instance.versionId);
+ } catch (e) {
+ console.error("Failed to start game:", e);
+ toast.error("Error starting game");
+ }
+ }}
+ >
+ <RocketIcon />
+ </Button>
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={(e) => {
+ e.stopPropagation();
+ openDuplicate(instance);
+ }}
+ >
+ <CopyIcon />
+ </Button>
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={(e) => {
+ e.stopPropagation();
+ openEdit(instance);
+ }}
+ >
+ <EditIcon />
+ </Button>
+ <Button
+ variant="destructive"
+ size="icon"
+ onClick={(e) => {
+ e.stopPropagation();
+ openDelete(instance);
+ }}
+ >
+ <Trash2Icon />
+ </Button>
+ </div>
+ </div>
</div>
</li>
);
diff --git a/packages/ui/src/pages/settings.tsx b/packages/ui/src/pages/settings.tsx
index 440a5dc..9387e23 100644
--- a/packages/ui/src/pages/settings.tsx
+++ b/packages/ui/src/pages/settings.tsx
@@ -1,6 +1,7 @@
import { toNumber } from "es-toolkit/compat";
import { FileJsonIcon } from "lucide-react";
import { useEffect, useState } from "react";
+import { toast } from "sonner";
import { migrateSharedCaches } from "@/client";
import { ConfigEditor } from "@/components/config-editor";
import { Button } from "@/components/ui/button";
@@ -13,9 +14,11 @@ import {
FieldLabel,
FieldLegend,
FieldSet,
+ FieldTitle,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
@@ -28,18 +31,40 @@ import {
import { Spinner } from "@/components/ui/spinner";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { useJavaStore } from "@/models/java";
import { useSettingsStore } from "@/models/settings";
export type SettingsTab = "general" | "appearance" | "advanced";
export function SettingsPage() {
const { config, ...settings } = useSettingsStore();
+ const javaStore = useJavaStore();
const [showConfigEditor, setShowConfigEditor] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<SettingsTab>("general");
useEffect(() => {
- if (!config) settings.refresh();
- }, [config, settings.refresh]);
+ const refresh = async () => {
+ try {
+ await settings.refresh();
+ } catch (error) {
+ console.error(error);
+ toast.error(`Failed to refresh settings: ${error}`);
+ }
+ try {
+ await javaStore.refreshInstallations();
+ if (!javaStore.catalog) await javaStore.refresh();
+ } catch (error) {
+ console.error(error);
+ toast.error(`Failed to refresh java catalogs: ${error}`);
+ }
+ };
+ refresh();
+ }, [
+ settings.refresh,
+ javaStore.refresh,
+ javaStore.refreshInstallations,
+ javaStore.catalog,
+ ]);
const renderScrollArea = () => {
if (!config) {
@@ -158,7 +183,66 @@ export function SettingsPage() {
<CardTitle className="font-bold text-xl">
Java Installations
</CardTitle>
- <CardContent></CardContent>
+ <CardContent>
+ <FieldGroup>
+ <Field>
+ <FieldLabel htmlFor="java-path">Java Path</FieldLabel>
+ <Input
+ type="text"
+ name="java-path"
+ value={config?.javaPath}
+ onChange={(e) => {
+ settings.merge({
+ javaPath: e.target.value,
+ });
+ }}
+ onBlur={() => {
+ settings.save();
+ }}
+ />
+ </Field>
+ <FieldSet>
+ <FieldLegend>Java Installations</FieldLegend>
+ {javaStore.installations ? (
+ <RadioGroup
+ value={config.javaPath}
+ onValueChange={(value) => {
+ settings.merge({
+ javaPath: value,
+ });
+ settings.save();
+ }}
+ >
+ {javaStore.installations?.map((installation) => (
+ <FieldLabel
+ key={installation.path}
+ htmlFor={installation.path}
+ >
+ <Field orientation="horizontal">
+ <FieldContent>
+ <FieldTitle>
+ {installation.vendor} ({installation.version})
+ </FieldTitle>
+ <FieldDescription>
+ {installation.path}
+ </FieldDescription>
+ </FieldContent>
+ <RadioGroupItem
+ value={installation.path}
+ id={installation.path}
+ />
+ </Field>
+ </FieldLabel>
+ ))}
+ </RadioGroup>
+ ) : (
+ <div className="flex justify-center items-center h-30">
+ <Spinner />
+ </div>
+ )}
+ </FieldSet>
+ </FieldGroup>
+ </CardContent>
</CardHeader>
</Card>
</TabsContent>
diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts
index 27ce1ff..8c90267 100644
--- a/packages/ui/vite.config.ts
+++ b/packages/ui/vite.config.ts
@@ -1,6 +1,6 @@
+import path from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
-import path from "path";
import { defineConfig } from "vite";
// https://vite.dev/config/