feat: Fix curseforge search and tweak browser frontend

This commit is contained in:
LynithDev 2024-09-21 14:53:56 +02:00
parent 0fb2017924
commit c991319463
7 changed files with 131 additions and 101 deletions

View file

@ -11,7 +11,6 @@ interface SearchResultsContainerProps {
provider: Providers;
results: SearchResult[];
header: string | JSX.Element;
category: string;
collapsable?: boolean;
}
@ -56,7 +55,7 @@ function SearchResultsContainer(props: SearchResultsContainerProps) {
</div>
<Show when={collapsed() === true}>
<div class="absolute bottom-4 left-1/2 z-2 -translate-x-1/2">
<div class="w-full flex items-center justify-center">
<Button
buttonStyle="secondary"
children="Show more"
@ -146,7 +145,7 @@ function PackageItem(props: SearchResult & { provider: Providers; row?: boolean
</div>
<div class="flex flex-1 flex-col gap-2 p-3">
<div class="flex flex-col gap-2">
<h4 class="text-fg-primary font-medium">{props.title}</h4>
<h4 class="text-fg-primary font-medium line-height-normal">{props.title}</h4>
<p class="text-xs text-fg-secondary">
By
{' '}

View file

@ -1,16 +1,18 @@
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 { bridge } from '~imports';
import Button from '~ui/components/base/Button';
import Dropdown from '~ui/components/base/Dropdown';
import Modal, { createModal } from '~ui/components/overlay/Modal';
import BrowserPackage from '~ui/pages/browser/BrowserPackage';
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 { Cluster, ManagedPackage, PackageType, Providers, ProviderSearchQuery, ProviderSearchResults } from '@onelauncher/client/bindings';
import type { ModalProps } from '~ui/components/overlay/Modal';
import { useRecentCluster } from './useCluster';
import useCommand, { tryResult } from './useCommand';
export type ProviderSearchOptions = ProviderSearchQuery & { provider: Providers };
export type PopularPackages = Record<Providers, SearchResult[]>;
interface BrowserControllerType {
cluster: Accessor<Cluster | undefined>;
@ -18,7 +20,6 @@ interface BrowserControllerType {
displayBrowser: (cluster?: Cluster | undefined) => void;
displayPackage: (id: string, provider: Providers) => void;
displayCategory: (category: string) => void;
displayClusterSelector: () => void;
search: () => void;
@ -28,16 +29,15 @@ interface BrowserControllerType {
packageType: Accessor<PackageType>;
setPackageType: Setter<PackageType>;
refreshCache: () => void;
cache: Accessor<ProviderSearchResults | undefined>;
featured: Accessor<ManagedPackage | undefined>;
popularPackages: Accessor<PopularPackages | undefined>;
featuredPackage: Accessor<ManagedPackage | undefined>;
};
const BrowserContext = createContext() as Context<BrowserControllerType>;
export function BrowserProvider(props: ParentProps) {
const [mainPageCache, setMainPageCache] = createSignal<ProviderSearchResults>();
const [featured, setFeatured] = createSignal<ManagedPackage | undefined>();
const [popularPackages, setPopularPackages] = createSignal<PopularPackages | undefined>();
const [featuredPackage, setFeaturedPackage] = createSignal<ManagedPackage | undefined>();
// Used for the current "Browser Mode". It'll only show packages of the selected type
const [packageType, setPackageType] = createSignal<PackageType>('mod');
@ -48,7 +48,7 @@ export function BrowserProvider(props: ParentProps) {
const [searchOptions, setSearchOptions] = createSignal<ProviderSearchOptions>({
provider: 'Modrinth',
query: '',
limit: 20,
limit: 18, // Even multiples of 3 recommended for grid view (normally 3 columns, 2 columns on very small windows)
offset: 0,
categories: null,
game_versions: null,
@ -78,45 +78,55 @@ export function BrowserProvider(props: ParentProps) {
navigate(BrowserPackage.buildUrl({ id, provider }));
},
displayCategory(_category: string) {
},
displayClusterSelector() {
modal.show();
},
search: () => {
navigate('/browser/search');
},
search: () => navigate('/browser/search'),
searchQuery: searchOptions,
setSearchQuery: setSearchOptions,
packageType,
setPackageType,
async refreshCache() {
const opts = untrack(searchOptions);
const res = await tryResult(() => bridge.commands.searchProviderPackages('Modrinth', opts));
setMainPageCache(res);
// TODO: Better algorithm for selecting a featured package
const firstPackage = res.results[0];
if (firstPackage !== undefined) {
const featuredPackage = await tryResult(() => bridge.commands.getProviderPackage('Modrinth', firstPackage.project_id));
setFeatured(featuredPackage);
}
},
cache: mainPageCache,
featured,
popularPackages,
featuredPackage,
};
onMount(() => {
if (mainPageCache() === undefined)
controller.refreshCache();
onMount(async () => {
const getOpts = (provider: Providers): ProviderSearchOptions => ({
provider,
query: '',
limit: 10,
offset: 0,
categories: null,
game_versions: null,
loaders: null,
package_types: ['mod'],
open_source: null,
});
const response = await Promise.allSettled(
PROVIDERS.map(provider => tryResult(() => bridge.commands.searchProviderPackages(provider, getOpts(provider)))),
);
const popularPackages = response.reduce((acc, res, i) => {
const provider = PROVIDERS[i] as Providers;
if (res.status === 'fulfilled')
acc[provider] = res.value.results;
return acc;
}, {} as PopularPackages);
setPopularPackages(popularPackages);
// TODO: Better algorithm for selecting a featured package
const firstPackage = popularPackages.Modrinth[0] || popularPackages.Curseforge[0];
if (firstPackage !== undefined) {
const featuredPackage = await tryResult(() => bridge.commands.getProviderPackage('Modrinth', firstPackage.project_id));
setFeaturedPackage(featuredPackage);
}
});
createEffect(() => {

View file

@ -1,10 +1,10 @@
import type { ManagedPackage } from '@onelauncher/client/bindings';
import type { ManagedPackage, Providers } from '@onelauncher/client/bindings';
import { ChevronRightIcon } from '@untitled-theme/icons-solid';
import OneConfigLogo from '~assets/logos/oneconfig.svg?component-solid';
import Button from '~ui/components/base/Button';
import SearchResultsContainer from '~ui/components/content/SearchResults';
import useBrowser from '~ui/hooks/useBrowser';
import { onMount, Show } from 'solid-js';
import { For, onMount, Show } from 'solid-js';
import { BrowserContent } from './BrowserRoot';
function BrowserMain() {
@ -20,15 +20,19 @@ function BrowserMain() {
return (
<BrowserContent>
<div class="flex flex-col gap-8">
<Show when={browser.cache() !== undefined && browser.featured() !== undefined}>
<Featured package={browser.featured()!} />
<Show when={browser.popularPackages() !== undefined && browser.featuredPackage() !== undefined}>
<Featured package={browser.featuredPackage()!} />
<SearchResultsContainer
category="modrinth"
collapsable
header="Modrinth"
{...browser.cache()!}
/>
<For each={Object.entries(browser.popularPackages()!)}>
{([provider, results]) => (
<SearchResultsContainer
collapsable
header={provider}
provider={provider as Providers}
results={results}
/>
)}
</For>
</Show>
</div>
</BrowserContent>

View file

@ -96,7 +96,7 @@ function BrowserCategories() {
<For each={browserCategories.byPackageType(browser.packageType(), browser.searchQuery().provider)}>
{category => (
<p
class={`text-md capitalize text-fg-primary hover:text-fg-primary-hover ${isEnabled(category.id) ? 'text-opacity-100! hover:text-opacity-90!' : 'text-opacity-60! hover:text-opacity-70!'}`}
class={`text-md capitalize text-fg-primary hover:text-fg-primary-hover line-height-snug ${isEnabled(category.id) ? 'text-opacity-100! hover:text-opacity-90!' : 'text-opacity-60! hover:text-opacity-70!'}`}
onClick={() => toggleCategory(category.id)}
>
{category.display}
@ -127,10 +127,13 @@ function BrowserSidebar() {
<div class="flex flex-col gap-y-1">
<h6 class="my-1">Provider</h6>
<Dropdown
onChange={index => controller.setSearchQuery(prev => ({
...prev,
provider: PROVIDERS[index] || 'Modrinth',
}))}
onChange={(index) => {
controller.setSearchQuery(prev => ({
...prev,
provider: PROVIDERS[index] || 'Modrinth',
}));
controller.search();
}}
selected={() => PROVIDERS.indexOf(controller.searchQuery().provider)}
>
<For each={PROVIDERS}>

View file

@ -39,14 +39,13 @@ function BrowserSearch() {
<Spinner.Suspense>
<Show when={results() !== undefined}>
<SearchResultsContainer
category="test"
header={(
<div class="w-full flex flex-row items-end justify-between">
<h5 class="ml-2">Results</h5>
<pagination.Navigation />
</div>
)}
provider="Modrinth"
provider={browser.searchQuery().provider}
results={results()!.results}
/>
</Show>

View file

@ -24,52 +24,53 @@ export const browserCategories = {
const modMapping: Record<Providers, CategoryItem[]> = {
Curseforge: [
{ display: 'Server Utility', id: '435' },
{ display: 'Armor, Tools, and Weapons', id: '434' },
{ display: 'Adventure', id: '422' },
{ display: 'API and Library', id: '421' },
{ display: 'Adventure and RPG', id: '422' },
{ display: 'World Gen', id: '406' },
{ display: 'Buildcraft', id: '432' },
{ display: 'Twitch Integration', id: '4671' },
{ display: 'Industrial Craft', id: '429' },
{ display: 'Magic', id: '419' },
{ display: 'Armor and Tools', id: '434' },
{ display: 'Biomes', id: '407' },
{ display: 'Bug Fixes', id: '6821' },
{ display: 'Cosmetic', id: '424' },
{ display: 'KubeJS', id: '5314' },
{ display: 'Storage', id: '420' },
{ display: 'Dimensions', id: '410' },
{ display: 'Education', id: '5299' },
{ display: 'Energy', id: '417' },
{ display: 'Farming', id: '416' },
{ display: 'Food', id: '436' },
{ display: 'Forestry', id: '433' },
{ display: 'Magic', id: '419' },
{ display: 'Miscellaneous', id: '425' },
{ display: 'Mobs', id: '411' },
{ display: 'Ores and Resources', id: '408' },
{ display: 'Create', id: '6484' },
{ display: 'Genetics', id: '418' },
{ display: 'Food', id: '436' },
{ display: 'Automation', id: '4843' },
{ display: 'Performance', id: '6814' },
{ display: 'Farming', id: '416' },
{ display: 'CraftTweaker', id: '4773' },
{ display: 'Structures', id: '409' },
{ display: 'Map and Information', id: '423' },
{ display: 'Bug Fixes', id: '6821' },
{ display: 'Biomes', id: '407' },
{ display: 'Energy', id: '417' },
{ display: 'Addons', id: '426' },
{ display: 'Blood Magic', id: '4485' },
{ display: 'Energy, Fluid, and Item Transport', id: '415' },
{ display: 'Forestry', id: '433' },
{ display: 'Education', id: '5299' },
{ display: 'Technology', id: '412' },
{ display: 'Thermal Expansion', id: '427' },
{ display: 'Dimensions', id: '410' },
{ display: 'Thaumcraft', id: '430' },
{ display: 'Processing', id: '413' },
{ display: 'Tinker\'s Construct', id: '428' },
{ display: 'Miscellaneous', id: '425' },
{ display: 'Skyblock', id: '6145' },
{ display: 'Galacticraft', id: '5232' },
{ display: 'Player Transport', id: '414' },
{ display: 'Applied Energistics 2', id: '4545' },
{ display: 'Integrated Dynamics', id: '6954' },
{ display: 'Redstone', id: '4558' },
{ display: 'Storage', id: '420' },
{ display: 'Structures', id: '409' },
{ display: 'Technology', id: '412' },
{ display: 'Utility & QoL', id: '5191' },
{ display: 'MCreator', id: '4906' },
{ display: 'World Generation', id: '406' },
// { display: 'Map and Information', id: '423' },
// { display: 'Server Utility', id: '435' },
// { display: 'Twitch Integration', id: '4671' },
// { display: 'Create', id: '6484' },
// { display: 'Buildcraft', id: '432' },
// { display: 'Industrial Craft', id: '429' },
// { display: 'KubeJS', id: '5314' },
// { display: 'Genetics', id: '418' },
// { display: 'Blood Magic', id: '4485' },
// { display: 'Automation', id: '4843' },
// { display: 'CraftTweaker', id: '4773' },
// { display: 'Addons', id: '426' },
// { display: 'Energy, Fluid, and Item Transport', id: '415' },
// { display: 'Thermal Expansion', id: '427' },
// { display: 'Thaumcraft', id: '430' },
// { display: 'Processing', id: '413' },
// { display: 'Tinker\'s Construct', id: '428' },
// { display: 'Skyblock', id: '6145' },
// { display: 'Galacticraft', id: '5232' },
// { display: 'Applied Energistics 2', id: '4545' },
// { display: 'Integrated Dynamics', id: '6954' },
// { display: 'MCreator', id: '4906' },
],
Modrinth: [
{ display: 'Adventure', id: 'adventure' },

View file

@ -118,10 +118,10 @@ impl From<CurseforgePackage> for ManagedPackage {
ManagedPackage {
id: package.id.to_string(),
title: package.name,
provider: todo!(),
package_type: todo!(),
description: todo!(),
body: todo!(),
provider: super::Providers::Curseforge,
package_type: package.package_type.into(),
description: package.summary,
body: "".to_string(), // TODO: Description is a new HTTP request
main: todo!(),
versions: todo!(),
game_versions: todo!(),
@ -305,6 +305,7 @@ pub struct LatestEarlyAccessFilesIndex {
}
// https://docs.curseforge.com/rest-api/?shell#tocS_ModsSearchSortField
#[allow(dead_code)]
enum SearchSort {
Featured = 1,
Popularity = 2,
@ -339,7 +340,7 @@ pub async fn fetch_url_builder<F: FnOnce(&mut Url)>(url: &str, url_builder: Opti
http::fetch_advanced(
Method::GET,
dbg!(url.as_str()),
url.as_str(),
None,
None,
Some(headers),
@ -353,6 +354,7 @@ async fn fetch(url: &str) -> Result<Bytes> {
fetch_url_builder(url, None::<fn(&mut Url)>).await
}
#[tracing::instrument(skip_all)]
pub async fn search(
query: Option<String>,
limit: Option<u8>,
@ -377,6 +379,9 @@ pub async fn search(
params.append_pair("gameId", crate::constants::CURSEFORGE_GAME_ID.to_string().as_str());
params.append_pair("pageSize", limit.unwrap_or(10).to_string().as_str());
params.append_pair("index", offset.unwrap_or(0).to_string().as_str());
params.append_pair("sortField", (SearchSort::Popularity as u8).to_string().as_str());
params.append_pair("sortOrder", "desc");
if let Some(query) = query {
params.append_pair("searchFilter", query.as_str());
}
@ -390,9 +395,18 @@ pub async fn search(
params.append_pair("modLoaderTypes", format!("[{}]", loaders.into_iter().map(|l| (CurseforgeLoader::from(l) as u32).to_string()).collect::<Vec<String>>().join(",")).as_str());
}
// TODO: Merge this with categories
if let Some(package_types) = package_types {
params.append_pair("categoryIds", format!("[{}]", package_types.into_iter().map(|p| (CFPackageType::from(p) as u32).to_string()).collect::<Vec<_>>().join(",")).as_str());
if package_types.len() == 0 {
return;
}
if package_types.len() > 1 {
tracing::warn!("curseforge provider does not support multiple package types");
}
let package_type = package_types.first().unwrap();
params.append_pair("classId", (CFPackageType::from(*package_type) as u32).to_string().as_str());
}
}))
).await?)?;