feat: Less empty home page + work on integrating backend with frontend

This commit is contained in:
LynithDev 2024-07-23 18:43:24 +02:00
parent 0dcc7083a9
commit 5e6c628f55
11 changed files with 253 additions and 180 deletions

View file

@ -1,13 +1,9 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::anyhow;
use chrono::DateTime;
use interpulse::api::minecraft::Version;
use onelauncher::constants::{NATIVE_ARCH, TARGET_OS, VERSION};
use onelauncher::data::{ClusterMeta, Loader, MinecraftCredentials, PackageData, Settings};
use onelauncher::store::{Cluster, ClusterPath, MinecraftLogin};
use onelauncher::data::{Loader, MinecraftCredentials, PackageData, Settings};
use onelauncher::store::{Cluster, MinecraftLogin};
use onelauncher::{cluster, minecraft, settings};
use serde::{Deserialize, Serialize};
use specta::Type;
@ -81,58 +77,56 @@ pub async fn create_cluster(props: CreateCluster) -> Result<Uuid, String> {
// }
fn placeholder_cluster() -> Cluster {
let path = ClusterPath("test".into());
Cluster {
uuid: Uuid::from_str("56d1cbcf-1961-4477-b263-80e3b1c7a9d1").unwrap(),
stage: onelauncher::store::ClusterStage::Installed,
path: path.0,
meta: ClusterMeta {
created_at: DateTime::from_timestamp_millis(1718297861712).unwrap(),
modified_at: DateTime::from_timestamp_millis(1718297861712).unwrap(),
group: vec![],
icon: None,
icon_url: None,
loader: Loader::Vanilla,
loader_version: None,
mc_version: "1.8.9".into(),
name: "Test Cluster".into(),
overall_played: 58195,
recently_played: 0,
package_data: None,
played_at: None,
},
memory: None,
java: None,
resolution: None,
force_fullscreen: None,
init_hooks: None,
packages: HashMap::new(),
update: None,
// fn placeholder_cluster() -> Cluster {
// let path = ClusterPath("test".into());
// Cluster {
// uuid: Uuid::from_str("56d1cbcf-1961-4477-b263-80e3b1c7a9d1").unwrap(),
// stage: onelauncher::store::ClusterStage::Installed,
// path: path.0,
// meta: ClusterMeta {
// created_at: DateTime::from_timestamp_millis(1718297861712).unwrap(),
// modified_at: DateTime::from_timestamp_millis(1718297861712).unwrap(),
// group: vec![],
// icon: None,
// icon_url: None,
// loader: Loader::Vanilla,
// loader_version: None,
// mc_version: "1.8.9".into(),
// name: "Test Cluster".into(),
// overall_played: 58195,
// recently_played: 0,
// package_data: None,
// played_at: None,
// },
// memory: None,
// java: None,
// resolution: None,
// force_fullscreen: None,
// init_hooks: None,
// packages: HashMap::new(),
// update: None,
// }
// }
#[specta::specta]
#[tauri::command]
pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
match cluster::get_by_uuid(uuid, None).await? {
Some(cluster) => Ok(cluster),
None => Err("Cluster does not exist".into())
}
}
#[specta::specta]
#[tauri::command]
pub async fn get_cluster(uuid: Uuid) -> Result<Cluster, String> {
Ok(placeholder_cluster())
pub async fn get_clusters() -> Result<Vec<Cluster>, String> {
// let mut map = HashMap::<ClusterPath, Cluster>::new();
// let cluster = placeholder_cluster();
// map.insert(cluster.cluster_path(), cluster);
// match cluster::get_by_uuid(uuid, None).await? {
// Some(cluster) => Ok(cluster),
// None => Err("Cluster does not exist".into())
// }
}
// Ok(map)
#[specta::specta]
#[tauri::command]
pub async fn get_clusters() -> Result<HashMap<ClusterPath, Cluster>, String> {
let mut map = HashMap::<ClusterPath, Cluster>::new();
let cluster = placeholder_cluster();
map.insert(cluster.cluster_path(), cluster);
Ok(map)
// cluster::list(None).await.map_err(|op| op.into())
Ok(cluster::list(None).await?)
}
#[specta::specta]
@ -212,3 +206,13 @@ pub async fn finish_msa(
pub async fn remove_user(uuid: Uuid) -> Result<(), String> {
Ok(minecraft::remove_user(uuid).await?)
}
// #[specta::specta]
// #[tauri::command]
// pub async fn import_instance() -> Result<(), String> {
// onelauncher::api::package::import::import_instance()
// }
// #[specta::specta]
// #[tauri::command]
// pub async fn

View file

@ -16,31 +16,40 @@ function App(props: ParentProps) {
});
return (
<ClusterModalController>
<SettingsProvider>
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-fg-primary">
<WindowFrame />
<div class="flex flex-col px-8">
<Navbar />
</div>
<GlobalContexts>
<main class="flex flex-col bg-primary w-full min-h-screen overflow-hidden h-screen max-h-screen text-fg-primary">
<WindowFrame />
<div class="flex flex-col px-8">
<Navbar />
</div>
<div class="relative h-full w-full overflow-hidden">
<div class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden">
<ErrorBoundary>
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8 pb-8">
<AnimatedRoutes>
{props.children}
</AnimatedRoutes>
</OverlayScrollbarsComponent>
</ErrorBoundary>
</div>
<div class="relative h-full w-full overflow-hidden">
<div class="absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden">
<ErrorBoundary>
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar absolute top-0 left-0 flex flex-col h-full w-full overflow-x-hidden overflow-y-auto px-8 pb-8">
<AnimatedRoutes>
{props.children}
</AnimatedRoutes>
</OverlayScrollbarsComponent>
</ErrorBoundary>
</div>
</div>
<NotificationOverlay />
</main>
</SettingsProvider>
</ClusterModalController>
<NotificationOverlay />
</main>
</GlobalContexts>
);
}
export default App;
function GlobalContexts(props: ParentProps) {
// TODO(refactor): Something something lots of nesting will look awful soon
return (
<SettingsProvider>
<ClusterModalController>
{props.children}
</ClusterModalController>
</SettingsProvider>
);
}

View file

@ -1,13 +1,15 @@
import { type Accessor, type Context, Match, type ParentProps, type Setter, Show, Switch, createContext, createEffect, createSignal, useContext } from 'solid-js';
import { type Accessor, type Context, Match, type ParentProps, type Setter, Show, Switch, createContext, createEffect, createSignal, untrack, useContext } from 'solid-js';
import { ArrowRightIcon, Server01Icon } from '@untitled-theme/icons-solid';
import HeaderImage from '../../../../assets/images/header.png';
import FullscreenOverlay, { type FullscreenOverlayProps } from '../FullscreenOverlay';
import FullscreenOverlay from '../FullscreenOverlay';
import ClusterStepOne from './ClusterStepOne';
import { ClusterStepTwo } from './ClusterStepTwo';
import Button from '~ui/components/base/Button';
import type { CreateCluster } from '~bindings';
import { bridge } from '~imports';
type PartialCluster = Partial<CreateCluster>;
type PartialClusterUpdateFunc = <K extends keyof PartialCluster>(key: K, value: PartialCluster[K]) => any;
// Why the fuck do I need to use a context for all this???
// TODO: Rewrite for use with the new Modal stacking system
@ -16,7 +18,9 @@ interface ClusterModalContextFunc {
setStep: Setter<number>;
partialCluster: Accessor<PartialCluster>;
setPartialCluster: Setter<PartialCluster>;
updatePartialCluster: <K extends keyof PartialCluster>(key: K, value: PartialCluster[K]) => any;
updatePartialCluster: PartialClusterUpdateFunc;
finish: () => void;
visible: Accessor<boolean>;
setVisible: Setter<boolean>;
}
@ -34,19 +38,43 @@ export function ClusterModalController(props: ParentProps) {
const [visible, setVisible] = createSignal(false);
const [partialCluster, setPartialCluster] = createSignal<PartialCluster>({});
const updatePartialCluster: PartialClusterUpdateFunc = (key, value) => {
setPartialCluster((prev) => {
return {
...prev,
[key]: value,
};
});
};
const finish = () => {
const untracked = untrack(partialCluster);
if (untracked.name === undefined || untracked.mc_version === undefined || untracked.mod_loader === undefined)
throw new Error('Cluster is missing required fields');
bridge.commands.createCluster({
name: untracked.name!,
mod_loader: untracked.mod_loader!,
mc_version: untracked.mc_version!,
// TODO: Implement the rest of the fields
icon: null,
icon_url: null,
loader_version: null,
package_data: null,
skip: null,
skip_watch: null,
});
};
const stepper: ClusterModalContextFunc = {
step,
setStep,
partialCluster,
setPartialCluster,
updatePartialCluster(key, value) {
setPartialCluster((prev) => {
return {
...prev,
[key]: value,
};
});
},
updatePartialCluster,
finish,
visible,
setVisible,
};
@ -60,13 +88,7 @@ export function ClusterModalController(props: ParentProps) {
{props.children}
{/* Makes sure theres a new instance of the modal */}
<Show when={visible()}>
<ClusterCreationModal
visible={visible}
setVisible={setVisible}
step={step}
setStep={setStep}
buttonIsNext={step() !== ((Object.keys(ClusterModalStages).length / 2) - 1)}
/>
<ClusterCreationModal />
</Show>
</ClusterModalContext.Provider>
);
@ -81,13 +103,10 @@ export interface ClusterStepProps {
setCanGoForward: Setter<boolean>;
};
type ClusterCreationModalProps = FullscreenOverlayProps & {
step: Accessor<number>;
setStep: Setter<number>;
buttonIsNext: boolean;
};
function ClusterCreationModal() {
const controller = useClusterModalController();
const buttonIsNext = () => controller.step() !== ((Object.keys(ClusterModalStages).length / 2) - 1);
function ClusterCreationModal(props: ClusterCreationModalProps) {
const [canGoForward, setCanGoForward] = createSignal(false);
// Indexed by the current step
const messages: FixedArray<string, ClusterModalStagesLength> = [
@ -101,18 +120,32 @@ function ClusterCreationModal(props: ClusterCreationModalProps) {
].map((Component, index) => (
Component({
setCanGoForward,
isVisible: () => props.visible() && props.step() === index,
isVisible: () => controller.visible() && controller.step() === index,
})
));
const Step = (props: { step: number }) => <>{stepComponents[props.step]}</>;
function cancel() {
controller.setVisible(false);
}
function prev() {
controller.setStep(prev => prev - 1);
}
function next() {
controller.setStep(prev => prev + 1);
}
function finish() {
controller.finish();
}
return (
<FullscreenOverlay
visible={props.visible}
setVisible={props.setVisible}
mount={props.mount}
zIndex={props.zIndex || 1000}
visible={controller.visible}
setVisible={controller.setVisible}
>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
<div class="bg-primary rounded-lg text-center flex flex-col min-w-sm">
@ -127,49 +160,50 @@ function ClusterCreationModal(props: ClusterCreationModalProps) {
<div class="flex flex-col items-start justify-center">
{/** weird positioning?? */}
<h1 class="h-10 -mt-2">New Cluster</h1>
<span>{messages[props.step()]}</span>
<span>{messages[controller.step()]}</span>
</div>
</div>
</div>
<div class="flex flex-col border border-white/5 rounded-b-lg">
<div class="p-3">
<Step step={props.step()} />
<Step step={controller.step()} />
</div>
<div class="flex flex-row gap-x-2 justify-end pt-0 p-3">
<Switch>
<Match when={props.step() === 0}>
<Match when={controller.step() === 0}>
<Button
children="Cancel"
buttonStyle="ghost"
onClick={() => props.setVisible(false)}
onClick={cancel}
/>
</Match>
<Match when={props.step() >= 1}>
<Match when={controller.step() >= 1}>
<Button
children="Previous"
buttonStyle="ghost"
onClick={() => props.setStep(prev => prev - 1)}
onClick={prev}
/>
</Match>
</Switch>
<Switch>
<Match when={props.buttonIsNext === true}>
<Match when={buttonIsNext() === true}>
<Button
children="Next"
buttonStyle="primary"
disabled={!canGoForward()}
iconRight={<ArrowRightIcon />}
onClick={() => props.setStep(prev => prev + 1)}
onClick={next}
/>
</Match>
<Match when={props.buttonIsNext === false}>
<Match when={buttonIsNext() === false}>
<Button
children="Create"
buttonStyle="primary"
disabled={!canGoForward()}
iconRight={<ArrowRightIcon />}
onClick={finish}
/>
</Match>
</Switch>

View file

@ -18,17 +18,9 @@ const providers: Omit<ProviderCardProps, 'selected' | 'setSelected'>[] = [
icon: <CurseforgeIcon color="#F16436" />,
},
{
name: 'New',
name: 'Import',
icon: <User03Icon />,
},
{
name: 'Modrinth',
icon: <ModrinthIcon color="#1bd96a" />,
},
{
name: 'Curseforge',
icon: <CurseforgeIcon color="#F16436" />,
},
];
export default function ClusterStepOne(props: ClusterStepProps) {

View file

@ -1,5 +1,5 @@
import { TextInputIcon } from '@untitled-theme/icons-solid';
import { For, Index, type JSX, type Setter, Show, createEffect, createSignal, on, splitProps, untrack } from 'solid-js';
import { For, Index, type JSX, Show, createEffect, createSignal, on, onMount, splitProps, untrack } from 'solid-js';
import { type ClusterStepProps, useClusterModalController } from './ClusterCreationModal';
import Dropdown from '~ui/components/base/Dropdown';
import TextField from '~ui/components/base/TextField';
@ -55,7 +55,11 @@ export function ClusterStepTwo(props: ClusterStepProps) {
const setName = (name: string) => updatePartialCluster('name', name);
const setVersion = (version: string) => updatePartialCluster('mc_version', version);
const setLoader = (loader: string) => updatePartialCluster('mod_loader', loader.toLowerCase() as Loader);
const setLoader = (loader: Loader | string) => updatePartialCluster('mod_loader', loader.toLowerCase() as Loader);
onMount(() => {
setLoader('vanilla');
});
return (
<div class="flex flex-col gap-y-4">
@ -101,7 +105,7 @@ function VersionSelector(props: { setVersion: (version: string) => any }) {
const [filters, setFilters] = createSignal<VersionReleaseFilters>({
old_alpha: false,
old_beta: false,
release: false,
release: true,
snapshot: false,
});
@ -142,7 +146,7 @@ function VersionSelector(props: { setVersion: (version: string) => any }) {
refresh(filters(), list);
});
createEffect(() => {
onMount(() => {
setFilteredVersions(versions()!);
});
@ -164,7 +168,12 @@ function VersionSelector(props: { setVersion: (version: string) => any }) {
<div class="bg-component-bg rounded-lg overflow-hidden border border-gray-05 flex-1 p-2 flex flex-col gap-y-2">
<For each={Object.keys(filters())}>
{name => (
<Checkbox onChecked={() => toggleFilter(name)}>{formatVersionRelease(name as VersionType)}</Checkbox>
<Checkbox
defaultChecked={filters()[name as VersionType]!}
onChecked={() => toggleFilter(name)}
>
{formatVersionRelease(name as VersionType)}
</Checkbox>
)}
</For>
</div>

View file

@ -7,7 +7,7 @@ import {
SearchMdIcon,
} from '@untitled-theme/icons-solid';
import type { Accessor, Setter } from 'solid-js';
import { For, Show, createSignal } from 'solid-js';
import { For, Match, Switch, createSignal } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import image from '../../assets/images/header.png';
import ContextMenu from '../components/overlay/ContextMenu';
@ -21,6 +21,49 @@ import { bridge } from '~imports';
import { upperFirst } from '~utils/primitives';
import { useClusterModalController } from '~ui/components/overlay/cluster/ClusterCreationModal';
function HomePage() {
const [clusters, { refetch }] = useCommand(bridge.commands.getClusters);
const controller = useClusterModalController();
async function newCluster() {
controller.setVisible(true);
}
return (
<div class="flex flex-col h-full gap-y-4 text-fg-primary">
<Banner />
<div class="flex flex-row justify-between items-center">
<div>
<TextField iconLeft={<SearchMdIcon />} placeholder="Search for clusters..." />
</div>
<div class="flex flex-row gap-x-4">
<Button
buttonStyle="primary"
iconLeft={<PlusIcon class="!w-5 stroke-[2.2]" />}
children="New Cluster"
onClick={newCluster}
/>
</div>
</div>
<Switch>
<Match when={clusters()?.length === 0}>
<div class="flex flex-col flex-1 gap-y-4 max-h-64 justify-center items-center">
<span class="text-lg font-bold uppercase text-fg-secondary">No clusters were found.</span>
<span class="text-xl font-bold">Create one now with the New Cluster button.</span>
</div>
</Match>
<Match when={clusters() !== undefined}>
<ClusterGroup title="Clusters" clusters={clusters()!} />
</Match>
</Switch>
</div>
);
}
export default HomePage;
// TODO: Replace this into it's own component
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
function OneConfigLogo() {
@ -40,6 +83,15 @@ function OneConfigLogo() {
}
function Banner() {
// TODO: Banner information
/**
* If there are any clusters, display the most recent cluster name + some statistics as the "description".
* The background would prioritise
* any screenshots taken from the cluster, if there are none, it would display the cluster cover if it has been set.
* The button would launch the cluster.
*
* If there are no clusters, display a generic background with the button action creating a new cluster.
*/
return (
<div class="relative w-full h-52">
<img src={image} class="absolute rounded-xl w-full h-52 object-cover" />
@ -50,10 +102,15 @@ function Banner() {
</div>
<div class="flex w-full flex-row justify-between items-end">
<div class="flex flex-row items-center gap-x-4">
<Button iconLeft={<PlayIcon />}>Launch 1.19.2</Button>
<Button buttonStyle="icon" class="w-8 h-8">
<DotsVerticalIcon />
</Button>
<Button
buttonStyle="primary"
iconLeft={<PlayIcon />}
children="Launch 1.19.2"
/>
<Button
buttonStyle="icon"
children={<DotsVerticalIcon />}
/>
</div>
<div class="flex flex-row gap-x-2">
<Tag iconLeft={<OneConfigLogo />} />
@ -184,44 +241,3 @@ function ClusterGroup(props: ClusterGroupProps) {
</div>
);
}
function HomePage() {
const [clusters, { refetch }] = useCommand(bridge.commands.getClusters);
const { setVisible } = useClusterModalController();
async function newInstance() {
setVisible(true);
}
return (
<div class="flex flex-col gap-y-4 text-fg-primary">
<Banner />
<div class="flex flex-row justify-between items-center">
<div>
<TextField iconLeft={<SearchMdIcon />} placeholder="Search for something..." />
</div>
<div class="flex flex-row gap-x-4">
<Button
buttonStyle="primary"
iconLeft={<PlusIcon class="!w-5 stroke-[2.2]" />}
children="New Cluster"
onClick={() => newInstance()}
/>
{/* <Button
styleType="secondary"
iconLeft={<Download01Icon />}
children="From URL"
/> */}
</div>
</div>
<Show when={clusters() !== undefined}>
<ClusterGroup title="Placeholder" clusters={Object.values(clusters()!)} />
</Show>
</div>
);
}
export default HomePage;

View file

@ -107,9 +107,9 @@ pub async fn create_cluster(
.await?;
}
if !skip.unwrap_or(false) {
crate::game::install_minecraft(&cluster, None, false).await?;
}
// if !skip.unwrap_or(false) {
// crate::game::install_minecraft(&cluster, None, false).await?;
// }
State::sync().await?;

View file

@ -45,7 +45,7 @@ pub async fn get(path: &ClusterPath, clear: Option<bool>) -> crate::Result<Optio
/// get a list of all [`Cluster`]s
#[tracing::instrument]
pub async fn list(clear: Option<bool>) -> crate::Result<HashMap<ClusterPath, Cluster>> {
pub async fn list(clear: Option<bool>) -> crate::Result<Vec<Cluster>> {
let state = State::get().await?;
let clusters = state.clusters.read().await;
Ok(clusters
@ -56,11 +56,18 @@ pub async fn list(clear: Option<bool>) -> crate::Result<HashMap<ClusterPath, Clu
if clear.unwrap_or(false) {
it.1.packages = HashMap::new();
}
it
it.1
})
.collect())
}
// #[tracing::instrument]
// pub async fn list_grouped(clear: Option<bool>) -> crate::Result<HashMap<String, Cluster>> {
// let clusters = list(clear).await?;
// let mut map = HashMap::new();
// }
/// run a Minecraft [`Cluster`] using the default credentials.
#[tracing::instrument]
pub async fn run(path: &ClusterPath) -> crate::Result<Arc<RwLock<ProcessorChild>>> {

View file

@ -21,6 +21,7 @@ pub mod technic;
pub mod tlauncher;
/// List of launcher types we support importing from.
#[cfg_attr(feature = "tauri", derive(specta::Type))]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ImportType {
/// MultiMC based launchers

View file

@ -15,4 +15,4 @@ onelauncher = { path = "../core", features = [ "cli" ] }
tokio = { workspace = true }
webbrowser = { version = "0.8" }
url = { version = "2.5" }
url = { version = "2.5.2" }

View file

@ -23,11 +23,12 @@ pub async fn authenticate_mc() -> onelauncher::Result<MinecraftCredentials> {
let parsed = url::Url::parse(input.trim()).expect("idk");
let code = if let Some((_, code)) = parsed.query_pairs().find(|x| x.0 == "code") {
&code.clone()
let code = code.clone();
code.to_string()
} else {
panic!()
};
let creds = minecraft::finish(code, login).await?;
let creds = minecraft::finish(code.as_str(), login).await?;
println!("logged in {}", creds.username);
@ -53,8 +54,8 @@ async fn main() -> onelauncher::Result<()> {
println!("clearing clusters");
{
let c = cluster::list(None).await?;
for (id, _) in c.into_iter() {
cluster::remove(&id).await?;
for cluster in c.into_iter() {
cluster::remove(&cluster.cluster_path()).await?;
}
}