From 46cca448371d9af377d9e1005dbed37bff6ee4e2 Mon Sep 17 00:00:00 2001 From: emirsassan Date: Wed, 28 May 2025 12:56:44 +0300 Subject: [PATCH] feat(cluster): add settings, screenshots, and worlds pages --- apps/onelauncher/desktop/src/api/commands.rs | 1 + .../desktop/src/api/commands/settings.rs | 8 + .../src/components/ScrollableContainer.tsx | 2 +- .../frontend/src/components/SettingsRow.tsx | 2 +- .../frontend/src/components/base/Dropdown.tsx | 62 ++++++ .../src/components/launcher/LoaderIcon.tsx | 39 ++++ .../frontend/src/components/overlay/Modal.tsx | 2 +- .../frontend/src/hooks/useCluster.ts | 42 ++++ .../frontend/src/routes/app/browser/index.tsx | 6 + .../frontend/src/routes/app/cluster/index.tsx | 184 +++++++++++++++++- .../frontend/src/routes/app/cluster/route.tsx | 21 +- .../src/routes/app/cluster/screenshots.tsx | 46 +++++ .../src/routes/app/cluster/settings.tsx | 26 +++ .../src/routes/app/cluster/worlds.tsx | 20 ++ .../frontend/src/routes/app/index.tsx | 59 ++++-- .../src/routes/app/settings/route.tsx | 8 +- packages/core/src/api/setting_profiles.rs | 34 ++++ 17 files changed, 530 insertions(+), 32 deletions(-) create mode 100644 apps/onelauncher/frontend/src/components/base/Dropdown.tsx create mode 100644 apps/onelauncher/frontend/src/components/launcher/LoaderIcon.tsx create mode 100644 apps/onelauncher/frontend/src/hooks/useCluster.ts create mode 100644 apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx create mode 100644 apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx create mode 100644 apps/onelauncher/frontend/src/routes/app/cluster/worlds.tsx diff --git a/apps/onelauncher/desktop/src/api/commands.rs b/apps/onelauncher/desktop/src/api/commands.rs index b0e39fa3..48fa8e68 100644 --- a/apps/onelauncher/desktop/src/api/commands.rs +++ b/apps/onelauncher/desktop/src/api/commands.rs @@ -22,6 +22,7 @@ macro_rules! collect_commands { cluster::get_cluster_by_id, settings::get_global_profile, + settings::get_profile_or_default, other::open_dev_tools, ] diff --git a/apps/onelauncher/desktop/src/api/commands/settings.rs b/apps/onelauncher/desktop/src/api/commands/settings.rs index 7edbc6b7..316b5346 100644 --- a/apps/onelauncher/desktop/src/api/commands/settings.rs +++ b/apps/onelauncher/desktop/src/api/commands/settings.rs @@ -7,4 +7,12 @@ use crate::api::error::SerializableResult; #[tauri::command] pub async fn get_global_profile() -> SerializableResult { Ok(setting_profiles::get_global_profile().await) +} + +#[specta::specta] +#[tauri::command] +pub async fn get_profile_or_default( + name: Option, +) -> SerializableResult { + Ok(setting_profiles::dao::get_profile_or_default(name.as_ref()).await?) } \ No newline at end of file diff --git a/apps/onelauncher/frontend/src/components/ScrollableContainer.tsx b/apps/onelauncher/frontend/src/components/ScrollableContainer.tsx index 1354c32c..20cda4b3 100644 --- a/apps/onelauncher/frontend/src/components/ScrollableContainer.tsx +++ b/apps/onelauncher/frontend/src/components/ScrollableContainer.tsx @@ -6,7 +6,7 @@ interface ScrollableContainerProps { function ScrollableContainer(props: ScrollableContainerProps) { return ( -
+
{props.children} diff --git a/apps/onelauncher/frontend/src/components/SettingsRow.tsx b/apps/onelauncher/frontend/src/components/SettingsRow.tsx index 19b3feb4..b5cd826a 100644 --- a/apps/onelauncher/frontend/src/components/SettingsRow.tsx +++ b/apps/onelauncher/frontend/src/components/SettingsRow.tsx @@ -12,7 +12,7 @@ function SettingsRow(props: SettingsRowProps) { return (
diff --git a/apps/onelauncher/frontend/src/components/base/Dropdown.tsx b/apps/onelauncher/frontend/src/components/base/Dropdown.tsx new file mode 100644 index 00000000..f0e57f7d --- /dev/null +++ b/apps/onelauncher/frontend/src/components/base/Dropdown.tsx @@ -0,0 +1,62 @@ +import type { ClassNameString } from '@/types/global'; +import type { + SelectProps as AriaComboBoxProps, + ListBoxItemProps as AriaListBoxItemProps +} from 'react-aria-components'; +import { + Select as AriaComboBox, + Label as AriaLabel, + ListBox as AriaListBox, + ListBoxItem as AriaListBoxItem, + Popover as AriaPopover, + SelectValue +} from 'react-aria-components'; +import { tv } from 'tailwind-variants'; +import Button from './Button'; + +interface ComboBoxWrapperProps extends Omit, 'className'>, ClassNameString { + label?: string; +} + +const dropdownStyles = tv({ + base: [ + "flex flex-col border border-component-bg-hover w-fit rounded-lg focus:outline-none" + ] +}) + +function Dropdown({ + className, + label, + children, + ...rest +}: ComboBoxWrapperProps) { + return ( + + {label && {label}} +
+ + +
+ + + {children} + + +
+ ); +} + +export default Dropdown; + +Dropdown.Item = ({ + className, + ...rest +}: AriaListBoxItemProps & ClassNameString) => ( + +); \ No newline at end of file diff --git a/apps/onelauncher/frontend/src/components/launcher/LoaderIcon.tsx b/apps/onelauncher/frontend/src/components/launcher/LoaderIcon.tsx new file mode 100644 index 00000000..58f03aad --- /dev/null +++ b/apps/onelauncher/frontend/src/components/launcher/LoaderIcon.tsx @@ -0,0 +1,39 @@ +import type { Model, GameLoader } from '@/bindings.gen'; +import FabricImage from '@/assets/logos/loaders/fabric.png'; +import ForgeImage from '@/assets/logos/loaders/forge.png'; +import NeoForgeImage from '@/assets/logos/loaders/neoforge.png'; +import QuiltImage from '@/assets/logos/loaders/quilt.png'; +import VanillaImage from '@/assets/logos/minecraft.png'; +import type { RefAttributes } from 'react'; + +export function getLoaderLogoSrc(loader: Model | GameLoader): string { + const loaderName = (typeof loader === 'string' ? loader : loader.mc_loader)?.toLowerCase() as GameLoader; + + const mapping: Record = { + vanilla: VanillaImage, + fabric: FabricImage, + legacyfabric: FabricImage, + forge: ForgeImage, + neoforge: NeoForgeImage, + quilt: QuiltImage, + }; + + return mapping[loaderName]; +} + +type LoaderIconProp = RefAttributes & { + loader: GameLoader | undefined; + className?: string; +}; + +function LoaderIcon(props: LoaderIconProp) { + return ( + {`${props.loader}'s + ); +} + +export default LoaderIcon; diff --git a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx index 6ac741fd..728cd4da 100644 --- a/apps/onelauncher/frontend/src/components/overlay/Modal.tsx +++ b/apps/onelauncher/frontend/src/components/overlay/Modal.tsx @@ -11,7 +11,7 @@ interface ModalProps extends DialogProps, ClassNameString { } function Modal({ className, ...props }: ModalProps) { return ( - + ); } diff --git a/apps/onelauncher/frontend/src/hooks/useCluster.ts b/apps/onelauncher/frontend/src/hooks/useCluster.ts new file mode 100644 index 00000000..56754a99 --- /dev/null +++ b/apps/onelauncher/frontend/src/hooks/useCluster.ts @@ -0,0 +1,42 @@ +import { bindings } from "@/main"; +import useCommand from "./useCommand"; +import { useEffect, useState } from "react"; +import type { Model } from "@/bindings.gen"; + +function useRecentCluster() { + const result = useCommand("getClusters", bindings.commands.getClusters); + const [cluster, setCluster] = useState(); + + useEffect(() => { + if (!result.data) return; + + let mostRecentCluster: Model | undefined; + + for (const c of result.data) { + if (!mostRecentCluster) { + mostRecentCluster = c; + continue; + } + + const currentPlayed = mostRecentCluster.last_played; + const newPlayed = c.last_played; + + if (typeof currentPlayed !== 'string' && typeof newPlayed === 'string') { + mostRecentCluster = c; + } else if (typeof currentPlayed === 'string' && typeof newPlayed === 'string') { + const playedAt = new Date(currentPlayed); + const clusterPlayedAt = new Date(newPlayed); + + if (clusterPlayedAt > playedAt) { + mostRecentCluster = c; + } + } + } + + setCluster(mostRecentCluster); + }, [result.data]); + + return cluster; +} + +export default useRecentCluster; \ No newline at end of file diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index c58edf9a..c812a9ae 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -1,4 +1,5 @@ import Button from '@/components/base/Button'; +import Dropdown from '@/components/base/Dropdown'; import { TextField } from '@/components/base/TextField'; import Modal from '@/components/overlay/Modal'; import { createFileRoute } from '@tanstack/react-router'; @@ -22,6 +23,11 @@ function RouteComponent() {

sadsadsad

+ + {/* + Slmalr + Slmalr 2 + */}
); } diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx index a1fc23e4..ac118e5f 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/index.tsx @@ -2,6 +2,16 @@ import useCommand from '@/hooks/useCommand' import { bindings } from '@/main' import { createFileRoute } from '@tanstack/react-router' import Sidebar from '../settings/route' +import Button from '@/components/base/Button' +import { Edit02Icon, FolderIcon, ImagePlusIcon, Share07Icon, Tool02Icon, Trash01Icon } from '@untitled-theme/icons-react' +import LoaderIcon from '@/components/launcher/LoaderIcon' +import { Show } from '@/components/base/Show' +import { TextField } from '@/components/base/TextField' +import type { Model } from '@/bindings.gen' +import ScrollableContainer from '@/components/ScrollableContainer' +import { useState, type Dispatch, type SetStateAction } from 'react' +import SettingsRow from '@/components/SettingsRow' +import ToggleButton from '@/components/base/ToggleButton' export const Route = createFileRoute('/app/cluster/')({ component: RouteComponent @@ -9,15 +19,183 @@ export const Route = createFileRoute('/app/cluster/')({ function RouteComponent() { const { id } = Route.useSearch() + const [newCover, setNewCover] = useState("") + const [newName, setNewName] = useState("") // dumbass fix ik const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint)) return ( -
-
{JSON.stringify(cluster, null, 2)}
-
+

Overview

+ +
+ + + Folders and Files + + )} + description={"burada path var işte yersen"} + disabled={false} + icon={} + title="Cluster Folder" + /> + + Cluster Actions + Edit + )} + description="Edit the cluster name and cover image." + icon={} + title="Edit Cluster" + /> + + )} + description="Delete this cluster and all its data." + disabled={false} + icon={} + title="Delete Cluster" + /> + + )} + description="Verifies whether all assets, libraries and natives were properly installed." + icon={} + title="Verify Cluster" + /> +
+
) } + +interface BannerProps { + cluster: Model | null | undefined; + editMode: boolean; + newName: string; + setNewName: Dispatch>; + newCover: string; + setNewCover: Dispatch>; + refetch: () => void; +} + +function Banner(props: BannerProps) { + // async function launchFilePicker() { + // const selected = await dialog.open({ + // multiple: false, + // directory: false, + // filters: [{ + // name: 'Image', + // extensions: ['png', 'jpg', 'jpeg', 'webp'], + // }], + // }); + + // if (selected === null) + // return; + + // props.setNewCover(selected); + // } + + function updateName(name: string) { + if (name.length > 30 || name.length <= 0) + return; + + props.setNewName(name); + } + + return ( +
+
+ +
+ +
+
+ + {/* */} + +
+ +
+
+ {props.cluster?.name} + } + when={props.editMode} + > + updateName(e.target.value)} + placeholder={props.cluster?.name} + /> + +
+ +
+
+ + + {props.cluster?.mc_version} + {props.cluster?.mc_loader || 'unknown'} + {props.cluster?.mc_loader_version && {props.cluster.mc_loader_version}} + + + Played for + {' '} + {props.cluster?.overall_played || 0} + . + +
+ +
+
+
+ +
+
+ ); +} diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx index 3908ae5f..9a5c0515 100644 --- a/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/cluster/route.tsx @@ -1,6 +1,8 @@ import { createFileRoute, Outlet } from '@tanstack/react-router' import Sidebar from '../settings/route' import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-react' +import useCommand from '@/hooks/useCommand' +import { bindings } from '@/main' type ClusterSearch = { id: bigint @@ -16,6 +18,12 @@ export const Route = createFileRoute('/app/cluster')({ }) function RouteComponent() { + const { id } = Route.useSearch() + + const cluster = useCommand("getClusterById", () => bindings.commands.getClusterById(Number(id.toString()) as unknown as bigint)) + + const isModded = cluster.data?.mc_loader != "vanilla" + return (
@@ -23,12 +31,13 @@ function RouteComponent() { base="/app/cluster" links={{ 'Cluster Settings': [ - [, 'Overview', '/'], - [, 'Mods', '/mods'], - [, 'Screenshots', '/screenshots'], - [, 'Worlds', '/worlds'], - [, 'Logs', '/logs'], - [, 'Game Settings', '/settings'], + // if someone has a better way to do this, please let me know + [, 'Overview', '/?id=' + id], + (isModded ? [, 'Mods', '/mods?id=' + id] : undefined), + [, 'Screenshots', '/screenshots?id=' + id], + [, 'Worlds', '/worlds?id=' + id], + [, 'Logs', '/logs?id=' + id], + [, 'Game Settings', '/settings?id=' + id], ], }} /> diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx new file mode 100644 index 00000000..b1da46fc --- /dev/null +++ b/apps/onelauncher/frontend/src/routes/app/cluster/screenshots.tsx @@ -0,0 +1,46 @@ +import { createFileRoute } from '@tanstack/react-router' +import Sidebar from '../settings/route'; +import ScrollableContainer from '@/components/ScrollableContainer'; +import { Show } from '@/components/base/Show'; + +export const Route = createFileRoute('/app/cluster/screenshots')({ + component: RouteComponent, +}) + +function RouteComponent() { + const arr = [1, 2, 3, 4, 5, 6] + return ( + +

Screenshots

+ +
+ No screenshots found. Press F2 in game to take a screenshot!
} + when={true} + > +
+ {arr.map((data) => ( + + ))} +
+ +
+ + + ) +} + +function ScreenshotEntry(props: { path: string; cluster_path: string }) { + function onClick() { + //screen shot viewer overlay + } + + return ( +
+ {props.path} +
+ ); +} diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx new file mode 100644 index 00000000..0d1c47f2 --- /dev/null +++ b/apps/onelauncher/frontend/src/routes/app/cluster/settings.tsx @@ -0,0 +1,26 @@ +import { createFileRoute } from '@tanstack/react-router' +import Sidebar from '../settings/route' +import ScrollableContainer from '@/components/ScrollableContainer' +import useCommand from '@/hooks/useCommand' +import { bindings } from '@/main' + +export const Route = createFileRoute('/app/cluster/settings')({ + component: RouteComponent, +}) + +function RouteComponent() { + const { id } = Route.useSearch() + + const result = useCommand("getProfileOrDefault", () => bindings.commands.getProfileOrDefault(id.toString())) + + return ( + +

Settings

+ +
+
{JSON.stringify(result.data, null, 2)}
+
+
+
+ ) +} diff --git a/apps/onelauncher/frontend/src/routes/app/cluster/worlds.tsx b/apps/onelauncher/frontend/src/routes/app/cluster/worlds.tsx new file mode 100644 index 00000000..df186f95 --- /dev/null +++ b/apps/onelauncher/frontend/src/routes/app/cluster/worlds.tsx @@ -0,0 +1,20 @@ +import { createFileRoute } from '@tanstack/react-router' +import Sidebar from '../settings/route' +import ScrollableContainer from '@/components/ScrollableContainer' + +export const Route = createFileRoute('/app/cluster/worlds')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( + +

Worlds

+ +
+ +
+
+
+ ) +} diff --git a/apps/onelauncher/frontend/src/routes/app/index.tsx b/apps/onelauncher/frontend/src/routes/app/index.tsx index 474d0140..9d283424 100644 --- a/apps/onelauncher/frontend/src/routes/app/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/index.tsx @@ -3,10 +3,12 @@ import type { Model } from '@/bindings.gen'; import Button from '@/components/base/Button'; import useCommand from '@/hooks/useCommand'; import { bindings } from '@/main'; -import { useIsFetching, useQueryClient } from '@tanstack/react-query'; import { createFileRoute, Link, useNavigate } from '@tanstack/react-router'; import { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; import DefaultInstancePhoto from "@/assets/images/default_instance_cover.jpg" +import Modal from '@/components/overlay/Modal'; +import { Server01Icon } from '@untitled-theme/icons-react'; +import HeaderImage from "@/assets/images/default_instance_cover.jpg"; export const Route = createFileRoute('/app/')({ component: RouteComponent, @@ -30,7 +32,7 @@ function RouteComponent() {
- +
); @@ -108,8 +110,6 @@ function Banner() { } function ClusterCreate() { - const queryClient = useQueryClient(); - const result = useCommand("createCluster", () => bindings.commands.createCluster({ icon_url: "asd", mc_loader: "vanilla", @@ -128,30 +128,59 @@ function ClusterCreate() { alert(result.error.message) return; } - - queryClient.invalidateQueries({ - queryKey: ["getClusters"], - }) } return ( <> -
+
+ + + ) } interface ClusterGroupProps { clusters: Model[] | undefined; + isFetching?: boolean; } function ClusterGroup(props: ClusterGroupProps) { - const isFetching = useIsFetching(); - - if (isFetching) { + if (props.isFetching) { return (
diff --git a/apps/onelauncher/frontend/src/routes/app/settings/route.tsx b/apps/onelauncher/frontend/src/routes/app/settings/route.tsx index 7a715179..6f67d813 100644 --- a/apps/onelauncher/frontend/src/routes/app/settings/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/settings/route.tsx @@ -66,10 +66,8 @@ function Sidebar(props: SidebarProps) { navigate({ to: url }) } - function isActive(link: string, params: URLSearchParams | undefined) { - const pathnameMatch = location === `${props.base}${link}` || `${location}/` === `${props.base}${link}`; - const paramsMatch = params ? location.endsWith(params.toString()) : true; - return pathnameMatch && paramsMatch; + function isActive(link: string, _params: URLSearchParams | undefined) { + return location === `${props.base}${link}` || `${location}/` === `${props.base}${link}`; } return ( @@ -109,7 +107,7 @@ function Sidebar(props: SidebarProps) { interface SidebarPageProps { className?: string; - children: JSX.Element; + children: JSX.Element | JSX.Element[]; } Sidebar.Page = function (props: SidebarPageProps) { diff --git a/packages/core/src/api/setting_profiles.rs b/packages/core/src/api/setting_profiles.rs index d8611c86..81b1ef18 100644 --- a/packages/core/src/api/setting_profiles.rs +++ b/packages/core/src/api/setting_profiles.rs @@ -6,6 +6,7 @@ use crate::store::State; pub mod dao { use onelauncher_entity::setting_profiles; + use sea_orm::IntoActiveModel; use sea_orm::{ActiveModelTrait, EntityTrait}; use crate::error::{DaoError, LauncherResult}; @@ -57,6 +58,39 @@ pub mod dao { Ok(profile.insert(db).await?) } + pub async fn update_profile_by_name( + name: &str, + block: B, + ) -> LauncherResult + where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult { + let state = State::get().await?; + let db = &state.db; + + let model = get_profile_by_name(name).await?.ok_or(DaoError::NotFound)?; + let model = block(model.into_active_model()).await?; + let model = model.update(db).await?; + + Ok(model) + } + + /// Updates an existing profile in the database. + pub async fn update_profile( + profile: &mut setting_profiles::Model, + block: B, + ) -> LauncherResult<&mut setting_profiles::Model> + where B: AsyncFnOnce(setting_profiles::ActiveModel) -> LauncherResult { + let state = State::get().await?; + let db = &state.db; + + let model = profile.clone().into_active_model(); + let model = block(model).await?; + let model = model.update(db).await?; + + *profile = model; + + Ok(profile) + } + pub async fn delete_profile_by_name(name: &str) -> LauncherResult<()> { let state = State::get().await?; let db = &state.db;