feat: Somewhat working Browser UI

This commit is contained in:
LynithDev 2024-07-24 15:57:33 +02:00
parent 170762fc7b
commit 2f80658db9
11 changed files with 198 additions and 93 deletions

View file

@ -26,6 +26,10 @@ fn main() {
"set_settings",
// Metadata
"get_minecraft_versions",
// Package
"random_mods",
"get_mod",
"download_mod",
// Other
"get_program_info",
]),

View file

@ -27,6 +27,11 @@ permissions = [
# Metadata
"allow-get-minecraft-versions",
# Package
"allow-random-mods",
"allow-get-mod",
"allow-download-mod",
# Other
"allow-get-program-info",
]

View file

@ -2,7 +2,8 @@ use std::path::PathBuf;
use interpulse::api::minecraft::Version;
use onelauncher::constants::{NATIVE_ARCH, TARGET_OS, VERSION};
use onelauncher::data::{Loader, MinecraftCredentials, PackageData, Settings};
use onelauncher::data::{Loader, ManagedPackage, MinecraftCredentials, PackageData, Settings};
use onelauncher::package::content;
use onelauncher::store::{Cluster, ClusterPath, MinecraftLogin};
use onelauncher::{cluster, minecraft, processor, settings};
use serde::{Deserialize, Serialize};
@ -40,6 +41,10 @@ macro_rules! collect_commands {
set_settings,
// Metadata
get_minecraft_versions,
// Package
random_mods,
get_mod,
download_mod,
// Other
get_program_info,
])
@ -244,8 +249,34 @@ pub async fn remove_user(uuid: Uuid) -> Result<(), String> {
Ok(minecraft::remove_user(uuid).await?)
}
// #[specta::specta]
// #[tauri::command]
// pub async fn searchMods(query: String) -> Result<Vec<ModrinthMod>, String> {
// // Ok(onelauncher::api::)
// }
#[specta::specta]
#[tauri::command]
pub async fn random_mods() -> Result<Vec<ManagedPackage>, String> {
let provider = content::Providers::Modrinth;
Ok(provider.list().await?)
}
#[specta::specta]
#[tauri::command]
pub async fn get_mod(project_id: String) -> Result<ManagedPackage, String> {
let provider = content::Providers::Modrinth;
Ok(provider.get(&project_id).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn download_mod(cluster_id: Uuid, version_id: String) -> Result<(), String> {
let cluster = cluster::get_by_uuid(cluster_id, None).await?.ok_or("cluster not found")?;
let provider = content::Providers::Modrinth;
let game_version = cluster.meta.mc_version.clone();
provider.get_version_for_game_version(&version_id, &game_version)
.await?
.files
.first()
.ok_or("no files found")?
.download_to_cluster(&cluster)
.await?;
Ok(())
}

View file

@ -21,17 +21,4 @@ declare namespace globalThis {
// }
// type Result = UnionToArray<keyof typeof Example>['length'] // = 5
}
// TODO remove in place for specta-generated types
interface Mod {
id: string;
name: string;
description: string;
author: string;
icon_url: string;
provider: Provider;
page_url: string;
downloads: number;
ratings: number;
}

View file

@ -2,6 +2,7 @@ import { Download01Icon, HeartIcon } from '@untitled-theme/icons-solid';
import { useNavigate } from '@solidjs/router';
import { abbreviateNumber } from '../../../utils/primitives';
import BrowserMod from '~ui/pages/browser/BrowserMod';
import type { ManagedPackage } from '~bindings';
// TODO: Remove in place for specta generated types
export enum Provider {
@ -11,37 +12,37 @@ export enum Provider {
Skyclient,
}
function ModCard(props: Mod) {
function ModCard(props: ManagedPackage) {
const navigate = useNavigate();
function redirect() {
navigate(BrowserMod.getUrl({
id: props.id,
provider: Provider[props.provider] || 'unknown',
provider: 'modrinth',
}));
}
return (
<div onClick={redirect} class="flex flex-col overflow-hidden rounded-lg bg-component-bg max-w-53 min-w-53 w-full max-h-68 min-h-68 h-full">
<div class="relative h-28 overflow-hidden flex justify-center items-center">
<img class="absolute filter-blur-xl z-0 max-w-none w-7/6" src={props.icon_url} alt={`Icon for ${props.name}`} />
<img class="relative w-2/5 aspect-ratio-square z-1 rounded-2xl" src={props.icon_url} alt={`Icon for ${props.name}`} />
<img class="absolute filter-blur-xl z-0 max-w-none w-7/6" src={props.icon_url || ''} alt={`Icon for ${props.title}`} />
<img class="relative w-2/5 aspect-ratio-square z-1 rounded-md image-render-auto" src={props.icon_url || ''} alt={`Icon for ${props.title}`} />
</div>
<div class="flex flex-col flex-1 p-3 gap-4">
<div class="flex flex-col gap-2">
<h5 class="font-medium text-fg-primary">{props.name}</h5>
<h5 class="font-medium text-fg-primary">{props.title}</h5>
<p class="text-fg-secondary text-xs">
By
{' '}
<span class="text-fg-primary">{props.author}</span>
<span class="text-fg-primary">Author TODO</span>
{' '}
on
{' '}
{Provider[props.provider]}
Modrinth
</p>
</div>
<p class="text-fg-secondary text-sm flex-1">{props.description}</p>
<p class="text-fg-secondary text-sm flex-1 max-h-13 overflow-hidden line-height-snug">{props.description}</p>
<div class="flex flex-row gap-4 text-xs">
<div class="flex flex-row items-center gap-2">
@ -51,7 +52,7 @@ function ModCard(props: Mod) {
<div class="flex flex-row items-center gap-2">
<HeartIcon class="w-4 h-4" />
{abbreviateNumber(props.ratings)}
{abbreviateNumber(props.followers)}
</div>
</div>
</div>

View file

@ -1,20 +1,20 @@
import { type Context, type ParentProps, createContext, useContext } from 'solid-js';
// import { type Context, type ParentProps, createContext, useContext } from 'solid-js';
interface BrowserContextType {
getMod: (id: string) => void;
getMods: (category: string) => void;
};
// interface BrowserContextType {
// getMod: (id: string) => void;
// getMods: (category: string) => void;
// };
const BrowserContext = createContext() as Context<BrowserContextType>;
// const BrowserContext = createContext() as Context<BrowserContextType>;
export function BrowserContextProvider(props: ParentProps) {
return (
<BrowserContext.Provider value={{}}>
{props.children}
</BrowserContext.Provider>
);
}
// export function BrowserContextProvider(props: ParentProps) {
// return (
// <BrowserContext.Provider value={}>
// {props.children}
// </BrowserContext.Provider>
// );
// }
export default function useBrowserController() {
return useContext(BrowserContext);
}
// export default function useBrowserController() {
// return useContext(BrowserContext);
// }

View file

@ -10,12 +10,7 @@ export default function useCommand<R, E, Args extends unknown[]>(
cmd: (...args: Args) => Promise<Result<R, E>>,
...args: Args
): ResourceReturn<R> {
return createResource(async () => {
console.log('aaaa', cmd);
const test = await tryResult(cmd, ...args);
console.log('bbbb');
return test;
});
return createResource(async () => await tryResult(cmd, ...args));
}
export async function tryResult<R, E, Args extends unknown[]>(

View file

@ -1,10 +1,13 @@
import { A } from '@solidjs/router';
import { Download01Icon, FileCode01Icon, FilterLinesIcon, SearchMdIcon } from '@untitled-theme/icons-solid';
import { For, createSignal } from 'solid-js';
import { For, createEffect, createSignal, onMount } from 'solid-js';
import type { ManagedPackage } from '~bindings';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import Dropdown from '~ui/components/base/Dropdown';
import TextField from '~ui/components/base/TextField';
import ModCard, { Provider } from '~ui/components/content/ModCard';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
import createSortable from '~utils/sorting';
interface CardProps {
@ -24,6 +27,36 @@ function BrowserMain() {
provider: [Provider.Curseforge],
});
const [modRowData, setModRowData] = createSignal<ModsRowProps[]>([]);
onMount(() => {
Promise.all([
tryResult(bridge.commands.randomMods),
tryResult(bridge.commands.getMod, 'oneconfig'),
tryResult(bridge.commands.getMod, 'chatting'),
tryResult(bridge.commands.getMod, 'patcher'),
]).then((res) => {
const list: ModsRowProps[] = [
{
header: 'Polyfrost',
category: 'polyfrost',
packages: [
res[1],
res[2],
res[3],
],
},
{
header: 'Random Mods',
category: 'random',
packages: res[0],
},
];
setModRowData(list);
});
});
const sortable = createSortable<CardProps>([], {
'A-Z': (a, b) => a.name.localeCompare(b.name),
'Z-A': (a, b) => b.name.localeCompare(a.name),
@ -33,7 +66,7 @@ function BrowserMain() {
return (
<div class="flex flex-col flex-1 h-full gap-2 relative">
<div class="flex flex-row flex-1 justify-between sticky top-0 z-10 bg-primary">
<div class="flex flex-row justify-between sticky top-0 z-10 bg-primary">
<div class="flex flex-row gap-2 h-8">
<TextField
@ -66,8 +99,8 @@ function BrowserMain() {
</div>
</div>
<div class="flex flex-col h-full flex-1 gap-4 py-2">
<For each={_modRowData}>
<div class="flex flex-col gap-4 py-2">
<For each={modRowData()}>
{row => (
<ModsRow {...row} />
)}
@ -78,32 +111,10 @@ function BrowserMain() {
);
}
// export const modStore: Mod[] = [
// {
// id: '1yIQcc2b',
// name: 'EvergreenHUD',
// description: 'Improves your heads up display.',
// author: 'Polyfrost',
// icon_url: 'https://cdn.modrinth.com/data/1yIQcc2b/icon.png',
// page_url: 'https://modrinth.com/mod/evergreenhud',
// provider: Provider.Modrinth,
// downloads: 281700,
// ratings: 220,
// },
// ];
// const _modRowData: ModsRowProps[] = [
// {
// header: 'Highly Endorsed',
// category: 'endorsed',
// mods: modStore,
// },
// ];
interface ModsRowProps {
header: string;
category: string;
mods: Mod[];
packages: ManagedPackage[];
};
function ModsRow(props: ModsRowProps) {
@ -114,8 +125,8 @@ function ModsRow(props: ModsRowProps) {
<A class="text-fg-secondary hover:text-fg-secondary-hover active:text-fg-secondary-pressed" href={`category?category=${props.category}`}>See all</A>
</div>
<div class="flex flex-row gap-2">
<For each={props.mods}>
<div class="flex flex-row gap-2 max-w-full flex-wrap">
<For each={props.packages}>
{mod => <ModCard {...mod} />}
</For>
</div>

View file

@ -1,32 +1,100 @@
import { type Params, useSearchParams } from '@solidjs/router';
import { Match, Switch } from 'solid-js';
import { Download01Icon } from '@untitled-theme/icons-solid';
import { For, Match, Switch, createSignal, onMount } from 'solid-js';
import { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import Dropdown from '~ui/components/base/Dropdown';
import ModCard from '~ui/components/content/ModCard';
import Modal from '~ui/components/overlay/Modal';
import useCommand, { tryResult } from '~ui/hooks/useCommand';
interface BrowserModParams extends Params {
id: string;
provider: string;
}
// TODO: Refactor the entire browser page and subpages
function BrowserMod() {
const [params] = useSearchParams<BrowserModParams>();
const isInvalid = !params.id || !params.provider;
const [visible, setVisible] = createSignal(false);
const [cluster, setCluster] = createSignal<string>();
const [clusters, setClusters] = createSignal<[string, string][]>();
const [pkg] = useCommand(bridge.commands.getMod, params.id!);
function installTo() {
setVisible(true);
}
function download() {
setVisible(false);
tryResult(bridge.commands.downloadMod, cluster()!, params.id!).then((res) => {
console.log('finished');
}).catch(err => console.error(err));
}
function onChange(selected: number) {
setCluster(clusters()![selected]![1]);
}
onMount(() => {
tryResult(bridge.commands.getClusters).then((res) => {
const list: [string, string][] = res.map(cluster => [cluster.meta.name, cluster.uuid]);
setClusters(list);
setCluster(clusters()![0]![1]);
});
});
return (
<Switch>
<Match when={isInvalid === true}>
{/* TODO */}
<div>uh oh this doesnt exist</div>
</Match>
<Match when={isInvalid === false}>
<div>
{JSON.stringify(params)}
<>
<div class="flex flex-row gap-x-12">
<ModCard {...pkg()!} />
<div class="flex flex-1 flex-col justify-between items-start bg-component-bg rounded-lg p-4 px-6">
<div class="flex w-full flex-row justify-between items-center">
<h1>{pkg()?.title}</h1>
<Button
buttonStyle="primary"
iconLeft={<Download01Icon />}
children="Install to..."
onClick={installTo}
/>
</div>
<p>TO DO</p>
</div>
</Match>
</Switch>
</div>
<Modal.Simple
visible={visible}
setVisible={setVisible}
title="Install To..."
buttons={[
<Button
buttonStyle="secondary"
children="Cancel"
onClick={() => setVisible(false)}
/>,
<Button
buttonStyle="primary"
children="Download"
onClick={download}
/>,
]}
>
<Dropdown onChange={onChange}>
<For each={clusters()}>
{cluster => (
<Dropdown.Row>{cluster[0]}</Dropdown.Row>
)}
</For>
</Dropdown>
</Modal.Simple>
</>
);
}
BrowserMod.getUrl = function (params: BrowserModParams): string {
return `/browser/mod?id=${params.id}&fprovider=${params.provider}`;
return `/browser/mod?id=${params.id}&provider=${params.provider}`;
};
export default BrowserMod;

View file

@ -1,6 +1,6 @@
import { Route, useSearchParams } from '@solidjs/router';
import type { ParentProps } from 'solid-js';
import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, Settings04Icon } from '@untitled-theme/icons-solid';
import { EyeIcon, File06Icon, Globe04Icon, Image03Icon, PackagePlusIcon, Settings04Icon } from '@untitled-theme/icons-solid';
import Sidebar from '../../components/Sidebar';
import AnimatedRoutes from '../../components/AnimatedRoutes';
import ErrorBoundary from '../../components/ErrorBoundary';
@ -56,7 +56,7 @@ function ClusterSidebar() {
Cluster: [
[<EyeIcon />, 'Overview', '/'],
// TODO: Better way of checking mods
// (cluster!.meta.loader === 'vanilla' ? [<PackagePlusIcon />, 'Mods', '/mods'] : undefined),
(cluster!.meta.loader !== 'vanilla' ? [<PackagePlusIcon />, 'Mods', '/mods'] : undefined),
[<Image03Icon />, 'Screenshots', '/screenshots'],
[<Globe04Icon />, 'Worlds', '/worlds'],
[<File06Icon />, 'Logs', '/logs'],

View file

@ -46,6 +46,7 @@ pub struct ModrinthPackage {
// pub project_type: String,
pub downloads: u32,
#[serde(rename = "icon_url")]
#[serde(default)]
pub icon_url: String,
// pub color: i64,
// #[serde(rename = "thread_id")]
@ -64,9 +65,11 @@ pub struct ModrinthPackage {
// pub queued: String,
pub followers: u32,
// pub license: License,
#[serde(default)]
pub versions: Vec<String>,
#[serde(rename = "game_versions")]
pub game_versions: Vec<String>,
#[serde(default)]
pub loaders: Vec<String>,
// pub gallery: Vec<Gallery>,
}