feat: Finished Curseforge implementation?
This commit is contained in:
parent
518c7ec8b9
commit
a1f8df7f49
8 changed files with 298 additions and 170 deletions
|
|
@ -149,6 +149,7 @@ pub async fn download_provider_package(
|
||||||
package_version,
|
package_version,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
package::add_package(
|
package::add_package(
|
||||||
&cluster.cluster_path(),
|
&cluster.cluster_path(),
|
||||||
pkg_path,
|
pkg_path,
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, SearchResult } from '@onelauncher/client/bindings';
|
import type { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, SearchResult } from '@onelauncher/client/bindings';
|
||||||
import type { ModalProps } from '~ui/components/overlay/Modal';
|
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import { bridge } from '~imports';
|
import { bridge } from '~imports';
|
||||||
import Button from '~ui/components/base/Button';
|
import { createModal } from '~ui/components/overlay/Modal';
|
||||||
import Dropdown from '~ui/components/base/Dropdown';
|
|
||||||
import Modal, { createModal } from '~ui/components/overlay/Modal';
|
|
||||||
import BrowserPackage from '~ui/pages/browser/BrowserPackage';
|
import BrowserPackage from '~ui/pages/browser/BrowserPackage';
|
||||||
import { PROVIDERS } from '~utils';
|
import { PROVIDERS } from '~utils';
|
||||||
import { type Accessor, type Context, createContext, createEffect, createSignal, For, on, onMount, type ParentProps, type Setter, Show, untrack, useContext } from 'solid-js';
|
import { type Accessor, type Context, createContext, createEffect, createSignal, on, onMount, type ParentProps, type Setter, useContext } from 'solid-js';
|
||||||
import { useRecentCluster } from './useCluster';
|
import { ChooseClusterModal, useRecentCluster } from './useCluster';
|
||||||
import useCommand, { tryResult } from './useCommand';
|
import { tryResult } from './useCommand';
|
||||||
|
|
||||||
export type ProviderSearchOptions = ProviderSearchQuery & { provider: Providers };
|
export type ProviderSearchOptions = ProviderSearchQuery & { provider: Providers };
|
||||||
export type PopularPackages = Record<Providers, SearchResult[]>;
|
export type PopularPackages = Record<Providers, SearchResult[]>;
|
||||||
|
|
@ -60,8 +57,30 @@ export function BrowserProvider(props: ParentProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const recentCluster = useRecentCluster();
|
const recentCluster = useRecentCluster();
|
||||||
|
|
||||||
|
const currentlySelectedCluster = (list: Cluster[]) => {
|
||||||
|
const cluster = controller.cluster();
|
||||||
|
|
||||||
|
if (cluster === undefined)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (list === undefined)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const index = list.findIndex(c => c.uuid === cluster.uuid) || 0;
|
||||||
|
if (index === -1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return index;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chooseCluster = (cluster: Cluster) => {
|
||||||
|
controller.setCluster(cluster);
|
||||||
|
};
|
||||||
|
|
||||||
const modal = createModal(props => (
|
const modal = createModal(props => (
|
||||||
<ChooseClusterModal
|
<ChooseClusterModal
|
||||||
|
onSelected={chooseCluster}
|
||||||
|
selected={list => currentlySelectedCluster(list) || 0}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
@ -163,73 +182,3 @@ export default function useBrowser() {
|
||||||
|
|
||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChooseClusterModal(props: ModalProps) {
|
|
||||||
const [selected, setSelected] = createSignal<number>(0);
|
|
||||||
const [clusters] = useCommand(() => bridge.commands.getClusters());
|
|
||||||
const controller = useBrowser();
|
|
||||||
|
|
||||||
const currentlySelectedCluster = (list: Cluster[]) => {
|
|
||||||
const cluster = controller.cluster();
|
|
||||||
|
|
||||||
if (cluster === undefined)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
if (list === undefined)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
const index = list.findIndex(c => c.uuid === cluster.uuid) || 0;
|
|
||||||
if (index === -1)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return index;
|
|
||||||
};
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setSelected(currentlySelectedCluster(clusters() || []));
|
|
||||||
});
|
|
||||||
|
|
||||||
function chooseCluster() {
|
|
||||||
const index = untrack(selected);
|
|
||||||
const clusterz = untrack(clusters);
|
|
||||||
if (clusterz !== undefined && index !== undefined)
|
|
||||||
controller.setCluster(clusterz[index]);
|
|
||||||
|
|
||||||
props.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal.Simple
|
|
||||||
{...props}
|
|
||||||
buttons={[
|
|
||||||
<Button
|
|
||||||
buttonStyle="secondary"
|
|
||||||
children="Close"
|
|
||||||
onClick={props.hide}
|
|
||||||
/>,
|
|
||||||
<Button
|
|
||||||
children="Save"
|
|
||||||
onClick={chooseCluster}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
children={(
|
|
||||||
<Show
|
|
||||||
fallback={<div>Loading...</div>}
|
|
||||||
when={clusters !== undefined}
|
|
||||||
>
|
|
||||||
<Dropdown
|
|
||||||
onChange={setSelected}
|
|
||||||
selected={selected}
|
|
||||||
>
|
|
||||||
<For each={clusters()!}>
|
|
||||||
{cluster => (
|
|
||||||
<Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Dropdown>
|
|
||||||
</Show>
|
|
||||||
)}
|
|
||||||
title="Choose a cluster"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import type { Cluster } from '@onelauncher/client/bindings';
|
import type { Cluster } from '@onelauncher/client/bindings';
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import { bridge } from '~imports';
|
import { bridge } from '~imports';
|
||||||
import Modal, { createModal } from '~ui/components/overlay/Modal';
|
import Button from '~ui/components/base/Button';
|
||||||
|
import Dropdown from '~ui/components/base/Dropdown';
|
||||||
|
import Modal, { createModal, type ModalProps } from '~ui/components/overlay/Modal';
|
||||||
import ClusterGame from '~ui/pages/cluster/ClusterGame';
|
import ClusterGame from '~ui/pages/cluster/ClusterGame';
|
||||||
import { type Accessor, type Context, createContext, createEffect, createSignal, type ParentProps, type ResourceReturn, Show, useContext } from 'solid-js';
|
import { type Accessor, type Context, createContext, createEffect, createResource, createSignal, For, Match, onMount, type ParentProps, type ResourceReturn, Show, Switch, untrack, useContext } from 'solid-js';
|
||||||
import useCommand, { tryResult } from './useCommand';
|
import useCommand, { tryResult } from './useCommand';
|
||||||
|
|
||||||
const ClusterContext = createContext() as Context<ResourceReturn<Cluster>>;
|
const ClusterContext = createContext() as Context<ResourceReturn<Cluster>>;
|
||||||
|
|
@ -104,3 +106,82 @@ export function useRecentCluster() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useClusterContext;
|
export default useClusterContext;
|
||||||
|
|
||||||
|
type ChooseClusterModalProps = ModalProps & {
|
||||||
|
clusters?: () => Cluster[];
|
||||||
|
selected?: (clusters: Cluster[]) => number;
|
||||||
|
onSelected?: (cluster: Cluster) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ChooseClusterModal(props: ChooseClusterModalProps) {
|
||||||
|
const [clusters] = createResource(async () => {
|
||||||
|
if (props.clusters !== undefined)
|
||||||
|
return await props.clusters();
|
||||||
|
|
||||||
|
return await tryResult(bridge.commands.getClusters);
|
||||||
|
});
|
||||||
|
const [selected, setSelected] = createSignal<number>(0);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const newSelected = props.selected?.(clusters() || []);
|
||||||
|
if (newSelected !== undefined)
|
||||||
|
setSelected(newSelected);
|
||||||
|
});
|
||||||
|
|
||||||
|
function chooseCluster() {
|
||||||
|
const index = untrack(selected);
|
||||||
|
const clusterz = untrack(clusters);
|
||||||
|
if (clusterz !== undefined && index !== undefined) {
|
||||||
|
const cluster = clusterz[index];
|
||||||
|
if (cluster !== undefined)
|
||||||
|
props.onSelected?.(cluster);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal.Simple
|
||||||
|
{...props}
|
||||||
|
buttons={[
|
||||||
|
<Button
|
||||||
|
buttonStyle="secondary"
|
||||||
|
children="Close"
|
||||||
|
onClick={props.hide}
|
||||||
|
/>,
|
||||||
|
<Button
|
||||||
|
children="Save"
|
||||||
|
onClick={chooseCluster}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
children={(
|
||||||
|
<Switch>
|
||||||
|
<Match when={clusters() !== undefined}>
|
||||||
|
<Dropdown
|
||||||
|
disabled={clusters()?.length === 0}
|
||||||
|
onChange={setSelected}
|
||||||
|
selected={selected}
|
||||||
|
>
|
||||||
|
<Switch>
|
||||||
|
<Match when={(clusters()?.length || 0) > 0}>
|
||||||
|
<For each={clusters()!}>
|
||||||
|
{cluster => (
|
||||||
|
<Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Match>
|
||||||
|
<Match when>
|
||||||
|
<Dropdown.Row>No clusters found</Dropdown.Row>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Dropdown>
|
||||||
|
</Match>
|
||||||
|
<Match when>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
title="Choose a cluster"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,15 @@ import Dropdown from '~ui/components/base/Dropdown';
|
||||||
import Link from '~ui/components/base/Link';
|
import Link from '~ui/components/base/Link';
|
||||||
import Tooltip from '~ui/components/base/Tooltip';
|
import Tooltip from '~ui/components/base/Tooltip';
|
||||||
import Markdown from '~ui/components/content/Markdown';
|
import Markdown from '~ui/components/content/Markdown';
|
||||||
|
import { createModal } from '~ui/components/overlay/Modal';
|
||||||
import Spinner from '~ui/components/Spinner';
|
import Spinner from '~ui/components/Spinner';
|
||||||
import useBrowser from '~ui/hooks/useBrowser';
|
import useBrowser from '~ui/hooks/useBrowser';
|
||||||
|
import { ChooseClusterModal } from '~ui/hooks/useCluster';
|
||||||
import useCommand, { tryResult } from '~ui/hooks/useCommand';
|
import useCommand, { tryResult } from '~ui/hooks/useCommand';
|
||||||
import usePagination from '~ui/hooks/usePagination';
|
import usePagination from '~ui/hooks/usePagination';
|
||||||
import usePromptOpener from '~ui/hooks/usePromptOpener';
|
import usePromptOpener from '~ui/hooks/usePromptOpener';
|
||||||
import { abbreviateNumber, formatAsRelative } from '~utils';
|
import { abbreviateNumber, formatAsRelative } from '~utils';
|
||||||
import { type Accessor, createContext, createEffect, createResource, createSignal, For, Match, on, type ParentProps, type Resource, Show, Switch, useContext } from 'solid-js';
|
import { type Accessor, createContext, createEffect, createMemo, createResource, createSignal, For, Match, on, type ParentProps, type Resource, Show, Switch, useContext } from 'solid-js';
|
||||||
import { getLicenseUrl, getPackageUrl, upperFirst } from '../../../utils';
|
import { getLicenseUrl, getPackageUrl, upperFirst } from '../../../utils';
|
||||||
|
|
||||||
interface BrowserModParams extends Params {
|
interface BrowserModParams extends Params {
|
||||||
|
|
@ -270,7 +272,7 @@ function InstallButton(props: ManagedPackage) {
|
||||||
return game_version && loader;
|
return game_version && loader;
|
||||||
};
|
};
|
||||||
|
|
||||||
const filtered = () => clusters()?.filter(meetsRequirements);
|
const filtered = createMemo(() => clusters()?.filter(meetsRequirements));
|
||||||
|
|
||||||
const getSelectedCluster = () => filtered()?.[selected()];
|
const getSelectedCluster = () => filtered()?.[selected()];
|
||||||
|
|
||||||
|
|
@ -312,44 +314,44 @@ function InstallButton(props: ManagedPackage) {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="h-12 flex flex-row">
|
<Show when={(filtered()?.length || 0) > 0}>
|
||||||
|
<div class="h-12 flex flex-row">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="primary"
|
buttonStyle="primary"
|
||||||
children={(
|
children={(
|
||||||
<div class="flex flex-1 flex-col items-center justify-center">
|
<div class="flex flex-1 flex-col items-center justify-center">
|
||||||
<p class="text-xs">Download latest to</p>
|
<p class="text-xs">Download latest to</p>
|
||||||
<span class="mt-0.5 h-3.5 max-w-38 overflow-x-hidden text-sm font-bold">{filtered?.()?.[selected?.()]?.meta.name || 'Unknown'}</span>
|
<span class="mt-0.5 h-3.5 max-w-38 overflow-x-hidden text-sm font-bold">{filtered?.()?.[selected?.()]?.meta.name || 'Unknown'}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
class="max-w-full flex-1 rounded-r-none!"
|
|
||||||
disabled={filtered()?.length === 0}
|
|
||||||
iconLeft={<Download01Icon />}
|
|
||||||
onClick={download}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
class="w-8"
|
|
||||||
component={props => (
|
|
||||||
<Button
|
|
||||||
buttonStyle="primary"
|
|
||||||
class="h-full w-full border-l border-white/5 rounded-l-none! px-0!"
|
|
||||||
iconLeft={<ChevronDownIcon />}
|
|
||||||
onClick={() => props.setVisible(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
disabled={filtered()?.length === 0}
|
|
||||||
dropdownClass="w-58! right-0"
|
|
||||||
onChange={setSelected}
|
|
||||||
selected={selected}
|
|
||||||
>
|
|
||||||
<For each={filtered()}>
|
|
||||||
{cluster => (
|
|
||||||
<Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
class="max-w-full flex-1 rounded-r-none!"
|
||||||
</Dropdown>
|
iconLeft={<Download01Icon />}
|
||||||
</div>
|
onClick={download}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
class="w-8"
|
||||||
|
component={props => (
|
||||||
|
<Button
|
||||||
|
buttonStyle="primary"
|
||||||
|
class="h-full w-full border-l border-white/5 rounded-l-none! px-0!"
|
||||||
|
iconLeft={<ChevronDownIcon />}
|
||||||
|
onClick={() => props.setVisible(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
dropdownClass="w-58! right-0"
|
||||||
|
onChange={setSelected}
|
||||||
|
selected={selected}
|
||||||
|
>
|
||||||
|
<For each={filtered()}>
|
||||||
|
{cluster => (
|
||||||
|
<Dropdown.Row>{cluster.meta.name}</Dropdown.Row>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -417,7 +419,7 @@ function BrowserPackageVersions() {
|
||||||
|
|
||||||
const list = getVersionsForPage(page());
|
const list = getVersionsForPage(page());
|
||||||
|
|
||||||
return await tryResult(() => bridge.commands.getProviderPackageVersions(context.pkg().provider, list));
|
return (await tryResult(() => bridge.commands.getProviderPackageVersions(context.pkg().provider, list))).toReversed();
|
||||||
});
|
});
|
||||||
|
|
||||||
let container!: HTMLDivElement;
|
let container!: HTMLDivElement;
|
||||||
|
|
@ -455,7 +457,7 @@ function BrowserPackageVersions() {
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={versions()?.reverse()}>
|
<For each={versions() || []}>
|
||||||
{version => <VersionRow {...version} />}
|
{version => <VersionRow {...version} />}
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -469,7 +471,7 @@ function BrowserPackageVersions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorForType(type: string) {
|
function colorForType(type: string) {
|
||||||
switch (type) {
|
switch (type.toLowerCase()) {
|
||||||
case 'release':
|
case 'release':
|
||||||
return 'bg-code-trace';
|
return 'bg-code-trace';
|
||||||
case 'snapshot':
|
case 'snapshot':
|
||||||
|
|
@ -484,6 +486,59 @@ function colorForType(type: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function VersionRow(props: ManagedVersion) {
|
function VersionRow(props: ManagedVersion) {
|
||||||
|
const [clusters, setClusters] = createSignal<Cluster[]>([]);
|
||||||
|
const context = useContext(BrowserPackageContext);
|
||||||
|
|
||||||
|
async function getClusters() {
|
||||||
|
const clusters = await tryResult(bridge.commands.getClusters);
|
||||||
|
|
||||||
|
return clusters.filter((cluster) => {
|
||||||
|
const game_version = props.game_versions.includes(cluster.meta.mc_version);
|
||||||
|
const loader = props.loaders.includes(cluster.meta.loader || 'vanilla');
|
||||||
|
|
||||||
|
return game_version && loader;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = createModal((props) => {
|
||||||
|
return (
|
||||||
|
<ChooseClusterModal
|
||||||
|
clusters={clusters}
|
||||||
|
onSelected={downloadPackage}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function downloadPrompted() {
|
||||||
|
const clusterz = await getClusters();
|
||||||
|
setClusters(clusterz);
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadPackage(cluster: Cluster) {
|
||||||
|
if (cluster === undefined)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line solid/reactivity -- lint issue
|
||||||
|
await tryResult(() => bridge.commands.downloadProviderPackage(
|
||||||
|
context!.pkg()!.provider,
|
||||||
|
context!.pkg()!.id,
|
||||||
|
cluster.uuid,
|
||||||
|
cluster.meta.mc_version,
|
||||||
|
cluster.meta.loader || null,
|
||||||
|
props.id,
|
||||||
|
));
|
||||||
|
|
||||||
|
await bridge.commands.syncClusterPackages(cluster.path || '');
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr class="my-2 bg-page-elevated px-4 [&>td]:py-4">
|
<tr class="my-2 bg-page-elevated px-4 [&>td]:py-4">
|
||||||
<td class="rounded-l-lg px-4">
|
<td class="rounded-l-lg px-4">
|
||||||
|
|
@ -533,6 +588,7 @@ function VersionRow(props: ManagedVersion) {
|
||||||
<Button
|
<Button
|
||||||
buttonStyle="iconSecondary"
|
buttonStyle="iconSecondary"
|
||||||
children={<Download01Icon />}
|
children={<Download01Icon />}
|
||||||
|
onClick={downloadPrompted}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import Button from '~ui/components/base/Button';
|
||||||
import TextField from '~ui/components/base/TextField';
|
import TextField from '~ui/components/base/TextField';
|
||||||
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||||
import Sidebar from '~ui/components/Sidebar';
|
import Sidebar from '~ui/components/Sidebar';
|
||||||
import Spinner from '~ui/components/Spinner';
|
|
||||||
import useBrowser from '~ui/hooks/useBrowser';
|
import useBrowser from '~ui/hooks/useBrowser';
|
||||||
import useClusterContext from '~ui/hooks/useCluster';
|
import useClusterContext from '~ui/hooks/useCluster';
|
||||||
import useCommand, { tryResult } from '~ui/hooks/useCommand';
|
import useCommand, { tryResult } from '~ui/hooks/useCommand';
|
||||||
|
|
@ -39,19 +38,10 @@ function ClusterMods() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatted;
|
return formatted;
|
||||||
|
}, {
|
||||||
|
initialValue: new Map<Providers, ManagedPackage[]>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function getManagedData(mod: Package): ManagedPackage | undefined {
|
|
||||||
const meta = mod.meta;
|
|
||||||
if (meta.type === 'managed') {
|
|
||||||
const provider = managedMods()?.get(meta.provider);
|
|
||||||
if (provider && provider.length > 0)
|
|
||||||
return provider.find(pkg => pkg.id === meta.package_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mods to display, should be a clone of the mods array but sorted
|
// Mods to display, should be a clone of the mods array but sorted
|
||||||
const [displayedMods, setDisplayedMods] = createSignal<Package[]>([]);
|
const [displayedMods, setDisplayedMods] = createSignal<Package[]>([]);
|
||||||
|
|
||||||
|
|
@ -125,24 +115,19 @@ function ClusterMods() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollableContainer>
|
<ScrollableContainer>
|
||||||
<Spinner.Suspense>
|
<Switch>
|
||||||
<Show when={managedMods.loading === false}>
|
<Match when={displayedMods().length > 0}>
|
||||||
<Switch>
|
<For each={displayedMods()}>
|
||||||
<Match when={displayedMods().length > 0}>
|
{mod => (
|
||||||
<For each={displayedMods()}>
|
<ModEntry managedMods={managedMods()} pkg={mod} refetch={refetch} />
|
||||||
{(mod) => {
|
)}
|
||||||
const managed = getManagedData(mod);
|
</For>
|
||||||
return <ModEntry managed={managed} pkg={mod} refetch={refetch} />;
|
</Match>
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</Match>
|
|
||||||
|
|
||||||
<Match when={displayedMods().length === 0}>
|
<Match when={displayedMods().length === 0}>
|
||||||
<p class="my-4 text-center text-2lg">No mods were found</p>
|
<p class="my-4 text-center text-2lg">No mods were found</p>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Show>
|
|
||||||
</Spinner.Suspense>
|
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
</Sidebar.Page>
|
</Sidebar.Page>
|
||||||
);
|
);
|
||||||
|
|
@ -153,13 +138,23 @@ export default ClusterMods;
|
||||||
interface ModEntryProps {
|
interface ModEntryProps {
|
||||||
pkg: Package;
|
pkg: Package;
|
||||||
refetch: () => void;
|
refetch: () => void;
|
||||||
managed?: ManagedPackage | undefined;
|
managedMods?: Map<Providers, ManagedPackage[]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ModEntry(props: ModEntryProps) {
|
function ModEntry(props: ModEntryProps) {
|
||||||
const [cluster] = useClusterContext();
|
const [cluster] = useClusterContext();
|
||||||
const browser = useBrowser();
|
const browser = useBrowser();
|
||||||
|
|
||||||
|
const managed = () => {
|
||||||
|
const pkg = props.pkg;
|
||||||
|
if (props.managedMods && pkg.meta.type === 'managed') {
|
||||||
|
const package_id = pkg.meta.package_id;
|
||||||
|
return props.managedMods.get(pkg.meta.provider)?.find(p => p.id === package_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const name = () => {
|
const name = () => {
|
||||||
if (props.pkg.meta.type === 'unknown')
|
if (props.pkg.meta.type === 'unknown')
|
||||||
return props.pkg.file_name;
|
return props.pkg.file_name;
|
||||||
|
|
@ -191,17 +186,19 @@ function ModEntry(props: ModEntryProps) {
|
||||||
|
|
||||||
const icon = () => {
|
const icon = () => {
|
||||||
if (props.pkg.meta.type === 'managed')
|
if (props.pkg.meta.type === 'managed')
|
||||||
return props.managed?.icon_url;
|
return managed()?.icon_url;
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
function onClick(e: MouseEvent) {
|
function onClick(e: MouseEvent) {
|
||||||
if (props.managed) {
|
const mngd = managed();
|
||||||
|
|
||||||
|
if (mngd) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
browser.displayPackage(props.managed.id, props.managed.provider);
|
browser.displayPackage(mngd.id, mngd.provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ pub async fn find_managed_version(
|
||||||
Ok(versions.0
|
Ok(versions.0
|
||||||
.iter()
|
.iter()
|
||||||
.find(|v| {
|
.find(|v| {
|
||||||
|
if package_version.is_some() {
|
||||||
|
return v.id == *package_version.as_ref().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
let check_game_version = game_version
|
let check_game_version = game_version
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|gv| v.game_versions.iter().any(|gv2| *gv2 == *gv));
|
.is_some_and(|gv| v.game_versions.iter().any(|gv2| *gv2 == *gv));
|
||||||
|
|
@ -44,11 +48,7 @@ pub async fn find_managed_version(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|loader| v.loaders.iter().any(|l| *l == *loader));
|
.is_some_and(|loader| v.loaders.iter().any(|l| *l == *loader));
|
||||||
|
|
||||||
let check_package_version = package_version
|
check_game_version && check_loader
|
||||||
.as_ref()
|
|
||||||
.map_or(true, |pv| pv == &v.version_display);
|
|
||||||
|
|
||||||
check_game_version && check_loader && check_package_version
|
|
||||||
})
|
})
|
||||||
.ok_or_else(|| anyhow::anyhow!("no matching version found"))
|
.ok_or_else(|| anyhow::anyhow!("no matching version found"))
|
||||||
.cloned()?)
|
.cloned()?)
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,12 @@ pub struct CurseforgePackage {
|
||||||
pub summary: String,
|
pub summary: String,
|
||||||
// pub status: u32,
|
// pub status: u32,
|
||||||
pub download_count: u64,
|
pub download_count: u64,
|
||||||
|
#[serde(default)]
|
||||||
pub is_featured: bool,
|
pub is_featured: bool,
|
||||||
// pub primary_category_id: u32,
|
// pub primary_category_id: u32,
|
||||||
pub categories: Vec<Category>,
|
pub categories: Vec<Category>,
|
||||||
pub authors: Vec<Author>,
|
pub authors: Vec<Author>,
|
||||||
pub logo: Logo,
|
pub logo: Option<Logo>,
|
||||||
// pub screenshots: Vec<Screenshot>,
|
// pub screenshots: Vec<Screenshot>,
|
||||||
// pub main_file_id: u32,
|
// pub main_file_id: u32,
|
||||||
// pub latest_files: Vec<LatestFile>,
|
// pub latest_files: Vec<LatestFile>,
|
||||||
|
|
@ -122,8 +123,8 @@ pub struct CurseforgePackage {
|
||||||
pub date_created: DateTime<Utc>,
|
pub date_created: DateTime<Utc>,
|
||||||
pub date_modified: DateTime<Utc>,
|
pub date_modified: DateTime<Utc>,
|
||||||
pub date_released: DateTime<Utc>,
|
pub date_released: DateTime<Utc>,
|
||||||
#[serde(default)]
|
// #[serde(default)]
|
||||||
pub allow_mod_distribution: bool,
|
// pub allow_mod_distribution: bool,
|
||||||
// pub game_popularity_rank: u32,
|
// pub game_popularity_rank: u32,
|
||||||
// pub is_available: bool,
|
// pub is_available: bool,
|
||||||
pub thumbs_up_count: u32,
|
pub thumbs_up_count: u32,
|
||||||
|
|
@ -143,7 +144,7 @@ impl From<CurseforgePackage> for ManagedPackage {
|
||||||
versions: vec![],
|
versions: vec![],
|
||||||
game_versions: vec![],
|
game_versions: vec![],
|
||||||
loaders: vec![],
|
loaders: vec![],
|
||||||
icon_url: Some(package.logo.url),
|
icon_url: package.logo.and_then(|l| Some(l.url)),
|
||||||
created: package.date_created,
|
created: package.date_created,
|
||||||
updated: package.date_modified,
|
updated: package.date_modified,
|
||||||
client: crate::store::PackageSide::Unknown,
|
client: crate::store::PackageSide::Unknown,
|
||||||
|
|
@ -189,7 +190,7 @@ impl Into<SearchResult> for CurseforgePackage {
|
||||||
server_side: crate::store::PackageSide::Unknown,
|
server_side: crate::store::PackageSide::Unknown,
|
||||||
project_type: self.package_type.into(),
|
project_type: self.package_type.into(),
|
||||||
downloads: self.download_count,
|
downloads: self.download_count,
|
||||||
icon_url: self.logo.url,
|
icon_url: self.logo.map_or(String::new(), |l| l.url),
|
||||||
categories: vec![], // TODO
|
categories: vec![], // TODO
|
||||||
display_categories: vec![],
|
display_categories: vec![],
|
||||||
versions: vec![],
|
versions: vec![],
|
||||||
|
|
@ -278,6 +279,7 @@ pub struct ModFile {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
pub game_id: u32,
|
pub game_id: u32,
|
||||||
pub mod_id: u32,
|
pub mod_id: u32,
|
||||||
|
#[serde(default)]
|
||||||
pub is_available: bool,
|
pub is_available: bool,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
|
|
@ -591,6 +593,25 @@ pub async fn get(id: u32) -> Result<CurseforgePackage> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_multiple(ids: &[u32]) -> Result<Vec<CurseforgePackage>> {
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
map.insert("modIds".to_string(), ids.to_vec().into());
|
||||||
|
|
||||||
|
let json_body = serde_json::Value::Object(map);
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
serde_json::from_slice::<CFData<_>>(
|
||||||
|
&fetch_advanced(
|
||||||
|
Method::POST,
|
||||||
|
"/v1/mods",
|
||||||
|
None::<fn(&mut Url)>,
|
||||||
|
None,
|
||||||
|
Some(json_body),
|
||||||
|
).await?)?
|
||||||
|
.data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_package_body(url: String) -> Result<String> {
|
pub async fn get_package_body(url: String) -> Result<String> {
|
||||||
Ok(serde_json::from_slice::<CFData<_>>(&fetch(&url).await?)?.data)
|
Ok(serde_json::from_slice::<CFData<_>>(&fetch(&url).await?)?.data)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,13 +108,36 @@ impl Providers {
|
||||||
|
|
||||||
pub async fn get_multiple(&self, slug_or_ids: &[String]) -> Result<Vec<ManagedPackage>> {
|
pub async fn get_multiple(&self, slug_or_ids: &[String]) -> Result<Vec<ManagedPackage>> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
Self::Modrinth => modrinth::get_multiple(slug_or_ids),
|
Self::Modrinth => {
|
||||||
Self::Curseforge => todo!(),
|
if slug_or_ids.len() <= 0 {
|
||||||
}
|
return Ok(vec![]);
|
||||||
.await?
|
}
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
modrinth::get_multiple(slug_or_ids)
|
||||||
.collect())
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
Self::Curseforge => {
|
||||||
|
let parsed_ids = slug_or_ids
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| id.parse::<u32>().ok())
|
||||||
|
.collect::<Vec<u32>>();
|
||||||
|
|
||||||
|
if parsed_ids.len() <= 0 {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed_ids = parsed_ids.as_slice();
|
||||||
|
|
||||||
|
curseforge::get_multiple(parsed_ids)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_versions(
|
pub async fn get_all_versions(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue