Files
watchparty/src/components/ComboBox/ComboBox.tsx
T
2026-06-21 03:53:06 +03:00

177 lines
4.7 KiB
TypeScript

import React from "react";
import {
debounce,
getMediaPathResults,
getYouTubeResults,
isHttp,
isMagnet,
isYouTube,
} from "../../utils/utils";
import { examples } from "../../utils/examples";
import ChatVideoCard from "../ChatVideoCard/ChatVideoCard";
import { IconLink, IconX } from "@tabler/icons-react";
import {
ActionIcon,
Autocomplete,
Loader,
type AutocompleteProps,
} from "@mantine/core";
type ComboBoxProps = {
roomSetMedia: (value: string) => void;
playlistAdd: (value: string) => void;
roomMedia: string;
getMediaDisplayName: (input: string) => string;
mediaPath: string | undefined;
disabled?: boolean;
};
type ComboBoxState = {
inputMedia?: string;
items: SearchResult[];
loading: boolean;
};
export class ComboBox extends React.Component<ComboBoxProps, ComboBoxState> {
state: ComboBoxState = {
inputMedia: undefined as string | undefined,
items: [] as SearchResult[],
loading: false,
};
setMediaAndClose = (value: string) => {
this.props.roomSetMedia(value);
this.setState({ inputMedia: undefined, items: [] });
};
doSearch = async () => {
const value = this.state.inputMedia;
this.setState({ loading: true });
const query: string = value || "";
let items = examples;
if (
query === "" ||
// Anything that doesn't pass this check we pass to YouTube as a search query
(query && (isHttp(query) || isMagnet(query)))
) {
if (!value && this.props.mediaPath) {
items = await getMediaPathResults(this.props.mediaPath, "");
}
if (query) {
let type: SearchResult["type"] = "file";
if (isYouTube(query)) {
type = "youtube";
}
if (isMagnet(query)) {
type = "magnet";
}
// Create entry from user input
items = [
{
name: query,
type,
url: query,
duration: 0,
},
];
}
} else {
const data = await getYouTubeResults(query);
items = data;
}
this.setState({
loading: false,
items,
});
};
debouncedSearch = debounce(this.doSearch);
onChange = (value: string) => {
this.setState({ inputMedia: value }, this.debouncedSearch);
};
render() {
const { roomMedia: currentMedia, getMediaDisplayName } = this.props;
const renderOption: AutocompleteProps["renderOption"] = ({ option }) => {
const video = this.state.items.find((item) => item.url === option.value);
return (
<div
key={option.value}
onClick={(e) => this.setMediaAndClose(option.value)}
style={{ width: "100%" }}
>
{video && (
<ChatVideoCard
video={video}
index={0}
onPlaylistAdd={this.props.playlistAdd}
/>
)}
</div>
);
};
return (
<Autocomplete
maxDropdownHeight={400}
style={{ width: "100%" }}
disabled={this.props.disabled}
onChange={this.onChange}
onFocus={(e: any) => {
this.setState(
{
// Display the real string value (currentMedia) when focused
inputMedia:
isHttp(currentMedia) || isMagnet(currentMedia)
? currentMedia
: getMediaDisplayName(currentMedia),
},
() => {
if (
!this.state.inputMedia ||
(this.state.inputMedia &&
(isHttp(this.state.inputMedia) ||
isMagnet(this.state.inputMedia)))
) {
this.doSearch();
}
e.target.select();
},
);
}}
onBlur={() => {
this.setState({
inputMedia: undefined,
items: [],
});
}}
onKeyDown={(e: any) => {
if (e.key === "Enter") {
this.setMediaAndClose(this.state.inputMedia ?? "");
e.target.blur();
}
}}
rightSection={
<ActionIcon
color="red"
onClick={(e: any) => this.setMediaAndClose("")}
title="Clear"
>
<IconX />
</ActionIcon>
}
leftSection={this.state.loading ? <Loader size="sm" /> : <IconLink />}
placeholder="Enter video file URL, magnet link, YouTube link, or YouTube search term"
value={
this.state.inputMedia !== undefined
? this.state.inputMedia
: getMediaDisplayName(currentMedia)
}
renderOption={renderOption}
data={this.state.items.map((item) => item.url)}
filter={({ options }) => options}
/>
);
}
}