feat: Fix curseforge search and tweak browser frontend
This commit is contained in:
parent
0fb2017924
commit
c991319463
7 changed files with 131 additions and 101 deletions
|
|
@ -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
|
||||
{' '}
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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?)?;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue