mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 00:30:02 +00:00
Implement SearchBar
This commit is contained in:
@@ -0,0 +1,79 @@
|
|||||||
|
import {Autocomplete, AutocompleteItem} from "@heroui/react";
|
||||||
|
import {CaretRight, MagnifyingGlass} from "@phosphor-icons/react";
|
||||||
|
import {useSnapshot} from "valtio/react";
|
||||||
|
import {gameState} from "Frontend/state/GameState";
|
||||||
|
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||||
|
import {useNavigate} from "react-router";
|
||||||
|
import {Key, KeyboardEvent, useState} from "react";
|
||||||
|
import {GameCover} from "Frontend/components/general/covers/GameCover";
|
||||||
|
|
||||||
|
export default function SearchBar() {
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const state = useSnapshot(gameState);
|
||||||
|
const games = state.games as GameDto[];
|
||||||
|
|
||||||
|
const [selectedId, setSelectedId] = useState<number>();
|
||||||
|
|
||||||
|
function handleKeyDown(event: KeyboardEvent<HTMLInputElement>) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate("/game/" + selectedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelectedId(key: Key | null) {
|
||||||
|
if (key === null) return;
|
||||||
|
setSelectedId(key as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Autocomplete
|
||||||
|
aria-label="Search for games"
|
||||||
|
classNames={{
|
||||||
|
listboxWrapper: "max-h-[320px]",
|
||||||
|
selectorButton: "text-default-500",
|
||||||
|
}}
|
||||||
|
defaultItems={games}
|
||||||
|
inputProps={{
|
||||||
|
classNames: {
|
||||||
|
input: "text-small",
|
||||||
|
inputWrapper: "h-full font-normal text-default-500 bg-default-400/20 dark:bg-default-500/20"
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
listboxProps={{
|
||||||
|
hideSelectedIcon: true,
|
||||||
|
itemClasses: {
|
||||||
|
base: [
|
||||||
|
"text-default-500",
|
||||||
|
"transition-opacity",
|
||||||
|
"data-[hover=true]:text-foreground",
|
||||||
|
"dark:data-[hover=true]:bg-default-50",
|
||||||
|
"data-[pressed=true]:opacity-70",
|
||||||
|
"data-[hover=true]:bg-default-200",
|
||||||
|
"data-[selectable=true]:focus:bg-default-100",
|
||||||
|
"data-[focus-visible=true]:ring-default-500",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
placeholder="Type to search..."
|
||||||
|
startContent={<MagnifyingGlass/>}
|
||||||
|
onSelectionChange={updateSelectedId}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
isVirtualized={true}
|
||||||
|
maxListboxHeight={300}
|
||||||
|
itemHeight={91} // 75px (cover) + 16px (margin top/bottom) = 91px
|
||||||
|
>
|
||||||
|
{(item) => (
|
||||||
|
<AutocompleteItem key={item.id} textValue={item.title} onPress={() => navigate("/game/" + item.id)}>
|
||||||
|
<div className="flex flex-row gap-4 items-center">
|
||||||
|
<GameCover game={item} size={75}/>
|
||||||
|
<div className="flex flex-col flex-1 gap-2">
|
||||||
|
<p><b>{item.title}</b> ({item.release && new Date(item.release).getFullYear()})</p>
|
||||||
|
<p className="text-default-500">{item.developers && [...item.developers].sort().join(" / ")}</p>
|
||||||
|
</div>
|
||||||
|
<CaretRight/>
|
||||||
|
</div>
|
||||||
|
</AutocompleteItem>
|
||||||
|
)}
|
||||||
|
</Autocomplete>
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {Heart} from "@phosphor-icons/react";
|
|||||||
import Confetti, {ConfettiProps} from "react-confetti-boom";
|
import Confetti, {ConfettiProps} from "react-confetti-boom";
|
||||||
import {useTheme} from "next-themes";
|
import {useTheme} from "next-themes";
|
||||||
import {UserPreferenceService} from "Frontend/util/user-preference-service";
|
import {UserPreferenceService} from "Frontend/util/user-preference-service";
|
||||||
|
import SearchBar from "Frontend/components/general/SearchBar";
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -57,9 +58,14 @@ export default function MainLayout() {
|
|||||||
{isExploding ? <Confetti {...confettiProps}/> : <></>}
|
{isExploding ? <Confetti {...confettiProps}/> : <></>}
|
||||||
|
|
||||||
<Navbar maxWidth="full" className="2xl:px-[12.5%]">
|
<Navbar maxWidth="full" className="2xl:px-[12.5%]">
|
||||||
<NavbarBrand as="button" onClick={() => navigate('/')}>
|
<NavbarBrand>
|
||||||
<GameyfinLogo className="h-10 fill-foreground"/>
|
<div className="cursor-pointer" onClick={() => navigate('/')}>
|
||||||
|
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||||
|
</div>
|
||||||
</NavbarBrand>
|
</NavbarBrand>
|
||||||
|
<NavbarContent justify="center" className="flex-1 max-w-96">
|
||||||
|
<SearchBar/>
|
||||||
|
</NavbarContent>
|
||||||
<NavbarContent justify="end">
|
<NavbarContent justify="end">
|
||||||
{auth.state.user?.emailConfirmed === false ?
|
{auth.state.user?.emailConfirmed === false ?
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ class LibraryService(
|
|||||||
// Update only non-null fields
|
// Update only non-null fields
|
||||||
libraryUpdateDto.name?.let { existingLibrary.name = it }
|
libraryUpdateDto.name?.let { existingLibrary.name = it }
|
||||||
libraryUpdateDto.directories?.let {
|
libraryUpdateDto.directories?.let {
|
||||||
existingLibrary.directories = it
|
existingLibrary.directories.clear()
|
||||||
.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
existingLibrary.directories.addAll(
|
||||||
.toMutableList()
|
it.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val updatedLibrary = libraryRepository.save(existingLibrary)
|
val updatedLibrary = libraryRepository.save(existingLibrary)
|
||||||
|
|||||||
Reference in New Issue
Block a user