feat: Browser Search sorta works, gotta implement filtering
This commit is contained in:
parent
e1c96dc4d1
commit
5012f2c3a6
10 changed files with 159 additions and 32 deletions
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
34
apps/frontend/src/ui/pages/browser/BrowserSearch.tsx
Normal file
34
apps/frontend/src/ui/pages/browser/BrowserSearch.tsx
Normal 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;
|
||||
21
apps/testing/examples/search_packages.rs
Normal file
21
apps/testing/examples/search_packages.rs
Normal 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(())
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue