feat: Somewhat working Browser UI
This commit is contained in:
parent
170762fc7b
commit
2f80658db9
11 changed files with 198 additions and 93 deletions
|
|
@ -26,6 +26,10 @@ fn main() {
|
|||
"set_settings",
|
||||
// Metadata
|
||||
"get_minecraft_versions",
|
||||
// Package
|
||||
"random_mods",
|
||||
"get_mod",
|
||||
"download_mod",
|
||||
// Other
|
||||
"get_program_info",
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
13
apps/desktop/src/types/global.d.ts
vendored
13
apps/desktop/src/types/global.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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[]>(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue