feat: Experiment with search - should be refactored later on to provide a unified search across the entire app

This commit is contained in:
LynithDev 2024-05-18 22:04:21 +02:00
parent 9f9be695ad
commit 429fc1fc53
11 changed files with 191 additions and 98 deletions

View file

@ -17,6 +17,7 @@
"dependencies": {
"@fontsource/poppins": "^5.0.14",
"@iconify-json/logos": "^1.1.42",
"@leeoniya/ufuzzy": "^1.0.14",
"@onelauncher/client": "workspace:*",
"@rspc/client": "^0.2.0",
"@rspc/solid": "^0.2.0",

View file

@ -41,7 +41,7 @@ function Navbar() {
let notificationButton!: HTMLButtonElement;
return (
<div class="flex flex-row *:flex-1 items-center min-h-[60px] h-15 mx-2">
<div class="flex flex-row *:flex-1 items-center min-h-[60px] h-15">
<div>
<div onClick={() => open(WEBSITE)} class="flex items-center justify-center active:scale-90 transition-transform w-min">
<PolyfrostFull />

View file

@ -1,21 +1,14 @@
import { OverlayScrollbarsComponent } from 'overlayscrollbars-solid';
import type { ParentProps } from 'solid-js';
type ScrollableContainerProps = ParentProps & {
title: string;
};
function ScrollableContainer(props: ScrollableContainerProps) {
function ScrollableContainer(props: ParentProps) {
return (
<div class="flex flex-col flex-1">
<h1>{props.title}</h1>
<div class="flex flex-col flex-1 rounded-lg overflow-hidden w-[calc(100%+14px)]">
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar relative flex-1">
<div class="flex flex-col absolute w-[calc(100%-14px)] gap-2">
{props.children}
</div>
</OverlayScrollbarsComponent>
</div>
<div class="flex flex-col flex-1 overflow-hidden w-[calc(100%+14px)]">
<OverlayScrollbarsComponent class="os-hide-horizontal-scrollbar relative flex-1">
<div class="flex flex-col absolute w-[calc(100%-14px)] gap-1">
{props.children}
</div>
</OverlayScrollbarsComponent>
</div>
);
}

View file

@ -1,6 +1,6 @@
import { useLocation, useNavigate } from '@solidjs/router';
import type { JSX, ParentProps } from 'solid-js';
import { For, createEffect } from 'solid-js';
import { For, createEffect, splitProps } from 'solid-js';
type SidebarProps = ParentProps & {
state: {
@ -67,4 +67,13 @@ function Sidebar(props: SidebarProps) {
);
}
Sidebar.Page = function (props: JSX.HTMLAttributes<HTMLDivElement>) {
const [split, rest] = splitProps(props, ['class', 'children']);
return (
<div class={`flex flex-col flex-1 gap-y-1 ${split.class || ''}`} {...rest}>
{split.children}
</div>
);
};
export default Sidebar;

View file

@ -1,29 +1,103 @@
import { open } from '@tauri-apps/plugin-shell';
import { ArrowRightIcon, Edit02Icon, LinkExternal01Icon, Trash03Icon } from '@untitled-theme/icons-solid';
import { For } from 'solid-js';
import { ArrowRightIcon, ChevronDownIcon, ChevronUpIcon, Edit02Icon, SearchMdIcon, Trash03Icon } from '@untitled-theme/icons-solid';
import { For, Index, Match, Switch, createEffect, createSignal } from 'solid-js';
import * as uuid from 'uuid';
import uFuzzy from '@leeoniya/ufuzzy';
import Button from '~ui/components/base/Button';
import TextField from '~ui/components/base/TextField';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
const mods = Array<ModEntryProps>(15).fill({
id: uuid.v4(),
name: 'Mod Name',
author: 'Author Name',
version: '1.0.0',
description: 'This is a mod description',
provider: 'curseforge',
thumbnail: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
});
const randomModNames = [
'Sodium',
'Nvidium',
'Iris Shaders',
'Create',
'Fabric API',
'Create: Estrogen',
'OneConfig',
'PolySprint',
'PolyEffects',
];
const mods: ModEntryProps[] = [];
for (let i = 0; i < randomModNames.length; i++) {
mods.push({
id: uuid.v4(),
name: randomModNames[i] || 'Unknown',
author: 'Author Name',
version: '1.0.0',
description: 'This is a mod description',
provider: 'curseforge',
thumbnail: 'https://cdn.modrinth.com/data/AANobbMI/icon.png',
});
}
function ClusterMods() {
// Search state:
// `null` - No search query, show all mods
// `[]` - Search query, but no results, show "no results" message
// `[...]` - Search query, with results, show results
const [indexes, setIndexes] = createSignal<number[] | null>(null);
// State:
// true - A to Z
// false - Z to A
const [sortingName, setSortingName] = createSignal<boolean>(true);
const uf = new uFuzzy();
const modsSearchable = mods
.map(mod => mod.name);
function search(value: string) {
if (value === '' || value === undefined) {
setIndexes(null);
return;
}
const result = uf.search(modsSearchable, value);
setIndexes(result[0] ?? []);
}
return (
<ScrollableContainer title="Mods">
<For each={mods}>
{mod => (
<ModEntry {...mod} />
)}
</For>
</ScrollableContainer>
<Sidebar.Page>
<div class="flex flex-row justify-between w-full">
<h1>Mods</h1>
<div class="flex flex-row justify-end items-center gap-x-2">
<Button
buttonStyle="secondary"
iconLeft={sortingName() ? <ChevronUpIcon /> : <ChevronDownIcon />}
onClick={() => setSortingName(!sortingName())}
children="Name"
/>
<TextField iconLeft={<SearchMdIcon />} placeholder="Search..." onInput={e => search(e.target.value)} />
</div>
</div>
<ScrollableContainer>
<Switch>
<Match when={indexes() === null}>
<For each={mods}>
{mod => (
<ModEntry {...mod} />
)}
</For>
</Match>
<Match when={indexes()!.length > 0}>
<For each={indexes()}>
{(index) => {
const mod = mods[index];
return !mod ? <></> : <ModEntry {...mod} />;
}}
</For>
</Match>
<Match when={indexes()!.length === 0}>
<p class="text-2lg text-center my-4">No mods were found</p>
</Match>
</Switch>
</ScrollableContainer>
</Sidebar.Page>
);
}
@ -49,9 +123,9 @@ function ModEntry(props: ModEntryProps) {
<div class="flex flex-col flex-1">
<div class="flex flex-row justify-between items-center">
<div class="flex flex-col items-start justify-center">
<div class="flex flex-row items-center">
<div class="flex flex-col items-start justify-center gap-y-2">
<h4>{props.name}</h4>
<span class="flex flex-row items-center text-xs font-600 text-fg-secondary/50 ml-2">
<span class="h-2 flex flex-row items-center justify-start text-xs font-600 text-fg-secondary/50">
{props.version}
{/* TODO: Add version checker */}
{/* <Show when={props.version.includes()}> */}
@ -60,15 +134,6 @@ function ModEntry(props: ModEntryProps) {
{/* </Show> */}
</span>
</div>
<span class="flex flex-row text-xs -mb-1 gap-1 items-center text-fg-secondary opacity-50">
by
{/* TODO: Implement prompt */}
<a onClick={() => open('https://google.com/')} class="text-fg-primary hover:opacity-70 flex flex-row gap-1 items-center">
{props.author}
<LinkExternal01Icon class="w-3" />
</a>
</span>
</div>
<div class="flex flex-row gap-2 items-end justify-center">

View file

@ -7,6 +7,7 @@ import LoaderIcon from '../../../components/game/LoaderIcon';
import Button from '../../../components/base/Button';
import { launchCluster } from '../../../../bridge/game';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
function ClusterOverview() {
const [params] = useSearchParams();
@ -15,11 +16,14 @@ function ClusterOverview() {
throw new Error('Cluster doesn\'t exist');
return (
<ScrollableContainer title="Overview">
<Show when={!cluster.loading}>
<Banner {...cluster()!} />
</Show>
</ScrollableContainer>
<Sidebar.Page>
<h1>Overview</h1>
<ScrollableContainer>
<Show when={!cluster.loading}>
<Banner {...cluster()!} />
</Show>
</ScrollableContainer>
</Sidebar.Page>
);
}

View file

@ -1,5 +1,6 @@
import { For } from 'solid-js';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Sidebar from '~ui/components/Sidebar';
const list = Array<Screenshot>(19).fill({
date_taken: Math.floor(Date.now() / 1000),
@ -14,15 +15,18 @@ interface Screenshot {
function ClusterScreenshots() {
return (
<ScrollableContainer title="Screenshots">
<div class="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full gap-2">
<For each={list}>
{screenshot => (
<ScreenshotEntry {...screenshot} />
)}
</For>
</div>
</ScrollableContainer>
<Sidebar.Page>
<h1>Screenshots</h1>
<ScrollableContainer>
<div class="grid grid-cols-[repeat(auto-fill,minmax(350px,1fr))] w-full gap-2">
<For each={list}>
{screenshot => (
<ScreenshotEntry {...screenshot} />
)}
</For>
</div>
</ScrollableContainer>
</Sidebar.Page>
);
}

View file

@ -4,37 +4,41 @@ import ScrollableContainer from '~ui/components/ScrollableContainer';
import Button from '~ui/components/base/Button';
import Toggle from '~ui/components/base/Toggle';
import appSettings from '~ui/state/appSettings';
import Sidebar from '~ui/components/Sidebar';
function SettingsAppearance() {
const { settings, setSettings } = appSettings;
return (
<ScrollableContainer title="Appearance">
<div class="flex flex-row items-center flex-1">
<p>theme placeholder</p>
</div>
<Sidebar.Page>
<h1>Appearance</h1>
<ScrollableContainer>
<div class="flex flex-row items-center flex-1">
<p>theme placeholder</p>
</div>
<SettingsRow
title="Accent Color"
description="The main color used across the launcher. This doesn't edit your theme."
icon={<PaintPourIcon />}
>
<Button iconLeft={<ColorsIcon />}>#ff0000</Button>
</SettingsRow>
<SettingsRow
title="Accent Color"
description="The main color used across the launcher. This doesn't edit your theme."
icon={<PaintPourIcon />}
>
<Button iconLeft={<ColorsIcon />}>#ff0000</Button>
</SettingsRow>
<SettingsRow
title="Animations"
description="Toggle all animations in the launcher."
icon={<Speedometer04Icon />}
>
<Toggle
defaultChecked={settings.reducedMotion}
onChecked={(checked) => {
setSettings('reducedMotion', checked);
}}
/>
</SettingsRow>
</ScrollableContainer>
<SettingsRow
title="Animations"
description="Toggle all animations in the launcher."
icon={<Speedometer04Icon />}
>
<Toggle
defaultChecked={settings.reducedMotion}
onChecked={(checked) => {
setSettings('reducedMotion', checked);
}}
/>
</SettingsRow>
</ScrollableContainer>
</Sidebar.Page>
);
}

View file

@ -3,23 +3,27 @@ import SettingsRow from '../components';
import ScrollableContainer from '~ui/components/ScrollableContainer';
import Toggle from '~ui/components/base/Toggle';
import appSettings from '~ui/state/appSettings';
import Sidebar from '~ui/components/Sidebar';
function SettingsGeneral() {
return (
<ScrollableContainer title="General">
<SettingsRow
title='Show Close Dialog'
description='Show a confirmation dialog when closing the launcher.'
icon={<XIcon />}
>
<Toggle
defaultChecked={appSettings.settings.closeDialog}
onChecked={(checked) => {
appSettings.setSettings('closeDialog', checked);
}}
/>
</SettingsRow>
</ScrollableContainer>
<Sidebar.Page>
<h1>General</h1>
<ScrollableContainer>
<SettingsRow
title="Show Close Dialog"
description="Show a confirmation dialog when closing the launcher."
icon={<XIcon />}
>
<Toggle
defaultChecked={appSettings.settings.closeDialog}
onChecked={(checked) => {
appSettings.setSettings('closeDialog', checked);
}}
/>
</SettingsRow>
</ScrollableContainer>
</Sidebar.Page>
);
}

View file

@ -28,6 +28,7 @@ export default petal({
rules: {
'petal/consistent-list-newline': 'off',
'no-console': 'off',
'new-cap': 'off',
},
},
]);

8
pnpm-lock.yaml generated
View file

@ -80,6 +80,9 @@ importers:
'@iconify-json/logos':
specifier: ^1.1.42
version: 1.1.42
'@leeoniya/ufuzzy':
specifier: ^1.0.14
version: 1.0.14
'@onelauncher/client':
specifier: workspace:*
version: link:../client
@ -684,6 +687,9 @@ packages:
resolution: {integrity: sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==}
engines: {node: '>=10'}
'@leeoniya/ufuzzy@1.0.14':
resolution: {integrity: sha512-/xF4baYuCQMo+L/fMSUrZnibcu0BquEGnbxfVPiZhs/NbJeKj4c/UmFpQzW9Us0w45ui/yYW3vyaqawhNYsTzA==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -3929,6 +3935,8 @@ snapshots:
string-argv: 0.3.2
type-detect: 4.0.8
'@leeoniya/ufuzzy@1.0.14': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5