feat: Browser Search sorta works, gotta implement filtering

This commit is contained in:
LynithDev 2024-09-04 22:03:37 +02:00
parent e1c96dc4d1
commit 5012f2c3a6
10 changed files with 159 additions and 32 deletions

View file

@ -51,25 +51,29 @@ pub async fn get_provider_package_version(
Ok(provider.get_version(&version).await?)
}
#[specta::specta]
#[tauri::command]
pub async fn search_provider_packages(
provider: Providers,
#[derive(specta::Type, serde::Deserialize, serde::Serialize)]
pub struct ProviderSearchQuery {
query: Option<String>,
limit: Option<u8>,
game_versions: Option<Vec<String>>,
categories: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
package_types: Option<Vec<PackageType>>,
open_source: Option<bool>,
) -> Result<ProviderSearchResults, String> {
}
#[specta::specta]
#[tauri::command]
pub async fn search_provider_packages(provider: Providers, query: ProviderSearchQuery) -> Result<ProviderSearchResults, String> {
Ok(provider
.search(
query,
limit,
game_versions,
categories,
loaders,
open_source,
query.query,
query.limit,
query.game_versions,
query.categories,
query.loaders,
query.package_types,
query.open_source,
)
.await?)
}

View file

@ -12,7 +12,7 @@ type TextFieldProps = {
} & JSX.InputHTMLAttributes<HTMLInputElement>;
function TextField(props: TextFieldProps) {
const [split, rest] = splitProps(props, ['iconLeft', 'iconRight', 'inputFilter', 'onValidInput', 'onValidSubmit', 'labelClass']);
const [split, rest] = splitProps(props, ['iconLeft', 'iconRight', 'inputFilter', 'onValidInput', 'onValidSubmit', 'labelClass', 'onInput', 'onChange']);
const [isValid, setIsValid] = createSignal(true);
const id = createUniqueId();
@ -30,9 +30,19 @@ function TextField(props: TextFieldProps) {
split.onValidInput(value);
}
function onSubmit() {
function onInput(e: Event) {
validate();
// @ts-expect-error -- I do not care about the event types anymore
split.onInput?.(e);
}
function onChange(e: Event) {
if (isValid() && split.onValidSubmit)
split.onValidSubmit(ref.value);
// @ts-expect-error -- I do not care about the event types anymore
split.onChange?.(e);
}
onMount(() => {
@ -46,8 +56,8 @@ function TextField(props: TextFieldProps) {
id={id}
type="text"
ref={ref}
onInput={validate}
onChange={onSubmit}
onInput={onInput}
onChange={onChange}
{...rest}
/>
{split.iconRight && <span class={styles.icon}>{split.iconRight}</span>}

View file

@ -1,6 +1,6 @@
import { useNavigate } from '@solidjs/router';
import { type Accessor, type Context, For, type ParentProps, type Setter, Show, createContext, createEffect, createSignal, onMount, untrack, useContext } from 'solid-js';
import type { Cluster, ManagedPackage, PackageType, ProviderSearchResults, Providers } from '@onelauncher/client/bindings';
import type { Cluster, ManagedPackage, PackageType, ProviderSearchQuery, ProviderSearchResults, Providers } from '@onelauncher/client/bindings';
import useCommand, { tryResult } from './useCommand';
import { useRecentCluster } from './useCluster';
import { bridge } from '~imports';
@ -19,6 +19,10 @@ interface BrowserControllerType {
displayCategory: (category: string) => void;
displayClusterSelector: () => void;
search: () => void;
searchQuery: Accessor<ProviderSearchQuery>;
setSearchQuery: Setter<ProviderSearchQuery>;
packageType: Accessor<PackageType>;
setPackageType: Setter<PackageType>;
@ -33,11 +37,24 @@ export function BrowserProvider(props: ParentProps) {
const [mainPageCache, setMainPageCache] = createSignal<ProviderSearchResults>();
const [featured, setFeatured] = createSignal<ManagedPackage | undefined>();
// Used for the current "Browser Mode". It'll only show packages of the selected type
const [packageType, setPackageType] = createSignal<PackageType>('mod');
// Active cluster that results should be "focused" on
const [cluster, setCluster] = createSignal<Cluster>();
const recentCluster = useRecentCluster();
const [searchOptions, setSearchOptions] = createSignal<ProviderSearchQuery>({
query: '',
limit: 20,
categories: null,
game_versions: null,
loaders: null,
package_types: ['mod'],
open_source: null,
});
const navigate = useNavigate();
const recentCluster = useRecentCluster();
const modal = createModal(props => (
<ChooseClusterModal
@ -65,11 +82,18 @@ export function BrowserProvider(props: ParentProps) {
modal.show();
},
search: () => {
navigate('/browser/search');
},
searchQuery: searchOptions,
setSearchQuery: setSearchOptions,
packageType,
setPackageType,
async refreshCache() {
const res = await tryResult(() => bridge.commands.searchProviderPackages('Modrinth', null, 10, null, null, null, null));
const opts = untrack(searchOptions);
const res = await tryResult(() => bridge.commands.searchProviderPackages('Modrinth', opts));
setMainPageCache(res);

View file

@ -33,6 +33,12 @@ interface FeaturedProps {
}
function Featured(props: FeaturedProps) {
const browser = useBrowser();
const open = () => {
browser.displayPackage(props.package.id, props.package.provider);
};
return (
<div class="flex flex-col gap-y-1">
<h5 class="ml-2">Featured</h5>
@ -55,7 +61,7 @@ function Featured(props: FeaturedProps) {
buttonStyle="ghost"
iconRight={<ChevronRightIcon />}
children="View Package"
onClick={() => { }}
onClick={open}
/>
</div>
</div>

View file

@ -1,10 +1,11 @@
import { Route } from '@solidjs/router';
import { For, type ParentProps } from 'solid-js';
import { For, type JSX, type ParentProps } from 'solid-js';
import { SearchMdIcon, Settings01Icon } from '@untitled-theme/icons-solid';
import type { PackageType } from '@onelauncher/client/bindings';
import BrowserMain from './BrowserMain';
import BrowserCategory from './BrowserCategory';
import BrowserPackage from './BrowserPackage';
import BrowserSearch from './BrowserSearch';
import Button from '~ui/components/base/Button';
import TextField from '~ui/components/base/TextField';
import useBrowser from '~ui/hooks/useBrowser';
@ -16,6 +17,7 @@ function BrowserRoutes() {
<Route path="/" component={BrowserMain} />
<Route path="/category" component={BrowserCategory} />
<Route path="/package" component={BrowserPackage} children={<BrowserPackage.Routes />} />
<Route path="/search" component={BrowserSearch} />
</>
);
}
@ -27,23 +29,31 @@ function BrowserRoot(props: ParentProps) {
}
function BrowserToolbar() {
// const controller = useBrowser();
const browser = useBrowser();
const onKeyPress: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent> = (e) => {
if (e.key === 'Enter') {
const query = e.currentTarget.value;
browser.setSearchQuery(prev => ({
...prev,
query,
}));
browser.search();
}
};
return (
<div class="w-full flex flex-row justify-between bg-page">
<div class="flex flex-row gap-2">
{/* <Button
buttonStyle="secondary"
children={controller.cluster()?.meta.name || 'None'}
iconLeft={<Settings01Icon />}
onClick={controller.displayClusterSelector}
/> */}
</div>
<div class="flex flex-row gap-2" />
<div class="flex flex-row gap-2">
<TextField
iconLeft={<SearchMdIcon />}
value={browser.searchQuery().query || ''}
placeholder="Search for content"
onKeyPress={onKeyPress}
/>
</div>
</div>

View file

@ -0,0 +1,34 @@
import { Show, createEffect, on } from 'solid-js';
import { BrowserContent } from './BrowserRoot';
import { bridge } from '~imports';
import SearchResultsContainer from '~ui/components/content/SearchResults';
import Spinner from '~ui/components/Spinner';
import useBrowser from '~ui/hooks/useBrowser';
import useCommand from '~ui/hooks/useCommand';
function BrowserSearch() {
const browser = useBrowser();
const [results, { refetch }] = useCommand(() => bridge.commands.searchProviderPackages('Modrinth', browser.searchQuery()));
createEffect(on(() => browser.searchQuery().query, () => {
refetch();
}));
return (
<BrowserContent>
<Spinner.Suspense>
<Show when={results() !== undefined}>
<SearchResultsContainer
category="test"
header="Results"
provider="Modrinth"
results={results()!.results}
/>
</Show>
</Spinner.Suspense>
</BrowserContent>
);
}
export default BrowserSearch;

View file

@ -0,0 +1,21 @@
use onelauncher::{data::PackageType, package::content, Result, State};
#[tokio::main]
async fn main() -> Result<()> {
let _ = State::get().await?;
let provider = content::Providers::Modrinth;
let results = provider.search(
Some("chatting".to_string()),
Some(5),
None,
None,
None,
Some(vec![PackageType::Mod]),
None
).await?;
println!("{:#?}", results);
Ok(())
}

View file

@ -5,7 +5,7 @@
use modrinth::{Facet, FacetOperation};
use serde::{Deserialize, Serialize};
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion};
use crate::data::{Loader, ManagedPackage, ManagedUser, ManagedVersion, PackageType};
use crate::package::content::modrinth::FacetBuilder;
use crate::store::{Author, ProviderSearchResults};
use crate::Result;
@ -47,6 +47,7 @@ impl Providers {
game_versions: Option<Vec<String>>,
categories: Option<Vec<String>>,
loaders: Option<Vec<Loader>>,
package_types: Option<Vec<PackageType>>,
open_source: Option<bool>,
) -> Result<ProviderSearchResults> {
match self {
@ -70,6 +71,16 @@ impl Providers {
}
}
if let Some(package_types) = package_types {
for package_type in package_types {
builder.or(Facet(
"project_types".to_string(),
FacetOperation::Eq,
package_type.get_name().to_string(),
));
}
}
if let Some(loaders) = loaders {
for loader in loaders {
builder.or(Facet(

View file

@ -249,7 +249,7 @@ impl FacetBuilder {
if i != 0 {
builder.push(',');
}
builder.push_str(f.to_string().as_str());
builder.push_str(format!("\"{}\"", f.to_string()).as_str());
}
builder.push(']');
}
@ -322,7 +322,7 @@ where
if facets.is_empty() {
"".to_string()
} else {
format!("&facets={}", facets)
format!("&facets=[{}]", facets)
}
)
.as_str(),

View file

@ -28,6 +28,8 @@ pub enum PackageType {
ResourcePack,
/// Represents a shaderpack file
ShaderPack,
/// Represents a modpack file
ModPack,
}
impl PackageType {
@ -59,6 +61,7 @@ impl PackageType {
"datapacks" => Some(PackageType::DataPack),
"resourcepacks" => Some(PackageType::ResourcePack),
"shaderpacks" => Some(PackageType::ShaderPack),
"modpack" => Some(PackageType::ModPack),
_ => None,
}
}
@ -69,6 +72,7 @@ impl PackageType {
PackageType::DataPack => "datapack",
PackageType::ResourcePack => "resourcepack",
PackageType::ShaderPack => "shaderpack",
PackageType::ModPack => "modpack",
}
}
@ -78,6 +82,7 @@ impl PackageType {
PackageType::DataPack => "datapacks",
PackageType::ResourcePack => "resourcepacks",
PackageType::ShaderPack => "shaderpacks",
PackageType::ModPack => "modpack",
}
}
@ -283,6 +288,7 @@ impl PackageManager {
PackageType::DataPack => &self.datapacks,
PackageType::ResourcePack => &self.resourcepacks,
PackageType::ShaderPack => &self.shaderpacks,
_ => unreachable!(),
}
}
@ -293,6 +299,7 @@ impl PackageManager {
PackageType::DataPack => &mut self.datapacks,
PackageType::ResourcePack => &mut self.resourcepacks,
PackageType::ShaderPack => &mut self.shaderpacks,
_ => unreachable!(),
}
}