feat: Category mapping + search results tweaks

This commit is contained in:
LynithDev 2024-09-08 14:07:01 +02:00
parent 02f1aaa429
commit f50d99a2bc
8 changed files with 176 additions and 94 deletions

View file

@ -20,11 +20,10 @@ function Markdown(props: MarkdownProps) {
e.preventDefault();
}}
onClick={(e) => {
e.stopImmediatePropagation();
e.preventDefault();
// TODO: Create a custom renderer or just use something else instead of markdown-it :D
if (e.target.nodeName === 'A') {
e.stopImmediatePropagation();
e.preventDefault();
const target = e.target as HTMLAnchorElement;
if (target.href.startsWith('http'))
promptOpen(target.href);

View file

@ -1,5 +1,6 @@
import type { Providers, SearchResult } from '@onelauncher/client/bindings';
import { ChevronDownIcon, Download01Icon, HeartIcon } from '@untitled-theme/icons-solid';
import type { JSX } from 'solid-js';
import { For, Match, Show, Switch, createSignal } from 'solid-js';
import Button from '../base/Button';
import useBrowser from '~ui/hooks/useBrowser';
@ -9,7 +10,7 @@ import { abbreviateNumber } from '~utils';
interface SearchResultsContainerProps {
provider: Providers;
results: SearchResult[];
header: string;
header: string | JSX.Element;
category: string;
collapsable?: boolean;
}
@ -22,7 +23,15 @@ function SearchResultsContainer(props: SearchResultsContainerProps) {
return (
<div class="relative flex flex-1 flex-col gap-3">
<div class="flex flex-1 flex-row justify-between">
<h5 class="ml-2">{props.header}</h5>
<Switch>
<Match when={typeof props.header === 'string'}>
<h5 class="ml-2">{props.header}</h5>
</Match>
<Match when>
{props.header}
</Match>
</Switch>
</div>
<div
@ -32,6 +41,10 @@ function SearchResultsContainer(props: SearchResultsContainerProps) {
}}
>
<Switch>
<Match when={props.results.length === 0}>
<p class="mb-8 text-center text-fg-secondary">No results found</p>
</Match>
<Match when={settings().browser_list_view === 'list'}>
<PackageList {...props} />
</Match>
@ -107,16 +120,29 @@ function PackageItem(props: SearchResult & { provider: Providers; row?: boolean
'w-full h-28': !props.row,
}}
>
<img class="absolute z-0 max-w-none w-7/6 opacity-50 filter-blur-xl" src={props.icon_url || ''} alt={`Icon for ${props.title}`} />
<img
class="relative z-1 aspect-ratio-square rounded-md image-render-auto"
classList={{
'w-2/5': !props.row,
'w-3/4': props.row,
}}
src={props.icon_url || ''}
alt={`Icon for ${props.title}`}
/>
<Show
fallback={(
<div
class="aspect-ratio-square rounded-md bg-gray-05"
classList={{
'w-2/5': !props.row,
'w-3/4': props.row,
}}
/>
)}
when={props.icon_url}
>
<img class="absolute z-0 max-w-none w-7/6 opacity-50 filter-blur-xl" src={props.icon_url!} alt={`Icon for ${props.title}`} />
<img
class="relative z-1 aspect-ratio-square rounded-md image-render-auto"
classList={{
'w-2/5': !props.row,
'w-3/4': props.row,
}}
src={props.icon_url!}
alt={`Icon for ${props.title}`}
/>
</Show>
</div>
<div class="flex flex-1 flex-col gap-2 p-3">
<div class="flex flex-col gap-2">

View file

@ -10,6 +10,8 @@ import Modal, { createModal } from '~ui/components/overlay/Modal';
import Dropdown from '~ui/components/base/Dropdown';
import Button from '~ui/components/base/Button';
export type ProviderSearchOptions = ProviderSearchQuery & { provider: Providers };
interface BrowserControllerType {
cluster: Accessor<Cluster | undefined>;
setCluster: Setter<Cluster | undefined>;
@ -20,8 +22,8 @@ interface BrowserControllerType {
displayClusterSelector: () => void;
search: () => void;
searchQuery: Accessor<ProviderSearchQuery>;
setSearchQuery: Setter<ProviderSearchQuery>;
searchQuery: Accessor<ProviderSearchOptions>;
setSearchQuery: Setter<ProviderSearchOptions>;
packageType: Accessor<PackageType>;
setPackageType: Setter<PackageType>;
@ -43,7 +45,8 @@ export function BrowserProvider(props: ParentProps) {
// Active cluster that results should be "focused" on
const [cluster, setCluster] = createSignal<Cluster>();
const [searchOptions, setSearchOptions] = createSignal<ProviderSearchQuery>({
const [searchOptions, setSearchOptions] = createSignal<ProviderSearchOptions>({
provider: 'Modrinth',
query: '',
limit: 20,
offset: 0,
@ -123,7 +126,7 @@ export function BrowserProvider(props: ParentProps) {
createEffect(on(cluster, (cluster) => {
if (cluster !== undefined)
setSearchOptions((prev) => {
const opts: ProviderSearchQuery = {
const opts: ProviderSearchOptions = {
...prev,
game_versions: [cluster.meta.mc_version],
};

View file

@ -86,7 +86,9 @@ function usePagination(initialOptions: PaginationOptions) {
<span>...</span>
</Show>
<PaginationBtn page={totalPages()} />
<Show when={totalPages() > 1}>
<PaginationBtn page={totalPages()} />
</Show>
<Button
buttonStyle="iconSecondary"

View file

@ -77,14 +77,14 @@ function BrowserPackage(props: ParentProps) {
return (
<Spinner.Suspense>
<div class="h-full flex flex-1 flex-row items-start gap-x-4 pb-8">
<div class="h-full flex flex-1 flex-row items-start gap-x-4">
<Show
when={pkg() !== undefined && authors() !== undefined}
children={(
<>
<BrowserSidebar package={pkg()!} authors={authors()!} />
<div class="min-h-full flex flex-1 flex-col items-start gap-y-4">
<div class="min-h-full flex flex-1 flex-col items-start gap-y-4 pb-8">
<div class="flex flex-none flex-row gap-x-1 rounded-lg bg-component-bg p-1">
<For each={links}>
{link => (
@ -200,7 +200,7 @@ function BrowserSidebar(props: { package: ManagedPackage; authors: ManagedUser[]
</div>
<LinkExternal01Icon class="h-4 w-4" />
</div>
<Show when={author.is_organization_user === true}>
<Show when={author.is_organization_user === true && props.authors.length > 1}>
<div class="h-px w-full bg-gray-05" />
</Show>
</>

View file

@ -8,6 +8,8 @@ import Button from '~ui/components/base/Button';
import TextField from '~ui/components/base/TextField';
import useBrowser from '~ui/hooks/useBrowser';
import { browserCategories } from '~utils/browser';
import Dropdown from '~ui/components/base/Dropdown';
import { PROVIDERS } from '~utils';
function BrowserRoutes() {
return (
@ -88,23 +90,19 @@ function BrowserCategories() {
<div class="top-0 grid grid-cols-[1fr_auto] h-fit min-w-50 gap-y-6">
<div />
<div class="flex flex-col gap-y-6">
<For each={browserCategories.byPackageType(browser.packageType())}>
{category => (
<div class="flex flex-col gap-y-2">
<h6 class="my-1">{category.name}</h6>
<For each={category.sub}>
{sub => (
<p
class={`text-md capitalize text-fg-primary hover:text-fg-primary-hover ${isEnabled(sub) ? 'text-opacity-100! hover:text-opacity-90!' : 'text-opacity-60! hover:text-opacity-70!'}`}
onClick={() => toggleCategory(sub)}
>
{sub}
</p>
)}
</For>
</div>
)}
</For>
<div class="flex flex-col gap-y-2">
<h6 class="my-1">Categories</h6>
<For each={browserCategories.byPackageType(browser.packageType())}>
{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!'}`}
onClick={() => toggleCategory(category.id)}
>
{category.display}
</p>
)}
</For>
</div>
</div>
</div>
);
@ -115,7 +113,7 @@ function BrowserSidebar() {
return (
<div class="flex flex-col gap-y-4">
<div class="flex flex-col gap-y-1">
<div class="flex flex-col gap-y-4">
<div class="flex flex-col gap-y-1">
<h6 class="my-1">Active Cluster</h6>
<Button
@ -125,6 +123,24 @@ function BrowserSidebar() {
onClick={controller.displayClusterSelector}
/>
</div>
<div class="flex flex-col gap-y-1">
<h6 class="my-1">Provider</h6>
<Dropdown
selected={() => PROVIDERS.indexOf(controller.searchQuery().provider)}
onChange={index => controller.setSearchQuery(prev => ({
...prev,
provider: PROVIDERS[index] || 'Modrinth',
}))}
>
<For each={PROVIDERS}>
{provider => (
<Dropdown.Row>
{provider}
</Dropdown.Row>
)}
</For>
</Dropdown>
</div>
</div>
</div>
);

View file

@ -36,22 +36,25 @@ function BrowserSearch() {
return (
<BrowserContent>
<pagination.Navigation />
<Spinner.Suspense>
<Show when={results() !== undefined}>
<SearchResultsContainer
category="test"
provider="Modrinth"
results={results()!.results}
header={(
<div class="w-full flex flex-row items-end justify-between">
<h5 class="ml-2">Results</h5>
<pagination.Navigation />
</div>
)}
/>
</Show>
<div class="my-2">
<Spinner.Suspense>
<Show when={results() !== undefined}>
<SearchResultsContainer
category="test"
header="Results"
provider="Modrinth"
results={results()!.results}
/>
</Show>
</Spinner.Suspense>
</div>
<pagination.Navigation />
<div class="mt-2">
<pagination.Navigation />
</div>
</Spinner.Suspense>
</BrowserContent>
);
}

View file

@ -1,53 +1,86 @@
import type { BrowserListView, PackageType } from '@onelauncher/client/bindings';
import type { BrowserListView, PackageType, Providers } from '@onelauncher/client/bindings';
export const BROWSER_VIEWS: BrowserListView[] = ['grid', 'list', 'preview'] as const;
export interface BrowserSidebarCategory {
name: string;
sub: string[];
export interface CategoryItem {
display: string;
id: string;
}
type ModCategories = [
'adventure',
'library',
'equipment',
'decoration',
'food',
'magic',
'performance',
'storage',
'technology',
'utility',
'worldgen',
];
const modMapping: Record<Providers, { [key in ModCategories[number]]: CategoryItem }> = {
Modrinth: {
adventure: {
display: 'Adventure',
id: 'adventure',
},
library: {
display: 'Library',
id: 'library',
},
decoration: {
display: 'Decoration',
id: 'decoration',
},
equipment: {
display: 'Equipment',
id: 'equipment',
},
food: {
display: 'Food',
id: 'food',
},
magic: {
display: 'Magic',
id: 'magic',
},
performance: {
display: 'Performance',
id: 'optimization',
},
storage: {
display: 'Storage',
id: 'storage',
},
technology: {
display: 'Technology',
id: 'technology',
},
utility: {
display: 'Utility',
id: 'utility',
},
worldgen: {
display: 'World Generation',
id: 'worldgen',
},
},
};
export const browserCategories = {
byPackageType(packageType: PackageType): BrowserSidebarCategory[] {
byPackageType(packageType: PackageType): CategoryItem[] {
switch (packageType) {
case 'mod':
return this.mod();
case 'datapack':
return this.datapack();
case 'resourcepack':
return this.resourcepack();
case 'shaderpack':
return this.shaderpack();
}
return [];
},
mod(): BrowserSidebarCategory[] {
return [
{
name: 'Categories',
sub: [
'adventure',
'magic',
'technology',
'utility',
'worldgen',
'miscellaneous',
],
},
];
},
datapack(): BrowserSidebarCategory[] {
return [];
},
resourcepack(): BrowserSidebarCategory[] {
return [];
},
shaderpack(): BrowserSidebarCategory[] {
return [];
mod(provider: Providers = 'Modrinth'): CategoryItem[] {
return Object.values(modMapping[provider]);
},
};