Skip to content

Commit

Permalink
Merge pull request #528 from Stremio/feat/search-history
Browse files Browse the repository at this point in the history
feature: search history
  • Loading branch information
tymmesyde authored Jan 3, 2024
2 parents ca4b582 + 932ebde commit 0344999
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 27 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.45.1",
"@stremio/stremio-core-web": "0.46.0",
"@stremio/stremio-icons": "5.0.0-beta.3",
"@stremio/stremio-video": "0.0.26",
"a-color-picker": "1.2.1",
Expand Down
142 changes: 126 additions & 16 deletions src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,100 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useTranslation } = require('react-i18next');
const { default: Icon } = require('@stremio/stremio-icons/react');
const { useRouteFocused } = require('stremio-router');
const Button = require('stremio/common/Button');
const TextInput = require('stremio/common/TextInput');
const useTorrent = require('stremio/common/useTorrent');
const { withCoreSuspender } = require('stremio/common/CoreSuspender');
const useSearchHistory = require('./useSearchHistory');
const useLocalSearch = require('./useLocalSearch');
const styles = require('./styles');
const useBinaryState = require('stremio/common/useBinaryState');

const SearchBar = ({ className, query, active }) => {
const SearchBar = React.memo(({ className, query, active }) => {
const { t } = useTranslation();
const routeFocused = useRouteFocused();
const searchHistory = useSearchHistory();
const localSearch = useLocalSearch();
const { createTorrentFromMagnet } = useTorrent();

const [historyOpen, openHistory, closeHistory, ] = useBinaryState(false);
const [currentQuery, setCurrentQuery] = React.useState(query || '');

const searchInputRef = React.useRef(null);
const containerRef = React.useRef(null);

const searchBarOnClick = React.useCallback(() => {
if (!active) {
window.location = '#/search';
}
}, [active]);

const searchHistoryOnClose = React.useCallback((event) => {
if (historyOpen && containerRef.current && !containerRef.current.contains(event.target)) {
closeHistory();
}
}, [historyOpen]);

React.useEffect(() => {
document.addEventListener('mousedown', searchHistoryOnClose);
return () => {
document.removeEventListener('mousedown', searchHistoryOnClose);
};
}, [searchHistoryOnClose]);

const queryInputOnChange = React.useCallback(() => {
const value = searchInputRef.current.value;
setCurrentQuery(value);
openHistory();
try {
createTorrentFromMagnet(searchInputRef.current.value);
// eslint-disable-next-line no-empty
} catch { }
}, []);
const queryInputOnSubmit = React.useCallback(() => {
if (searchInputRef.current !== null) {
const queryParams = new URLSearchParams([['search', searchInputRef.current.value]]);
window.location = `#/search?${queryParams.toString()}`;
createTorrentFromMagnet(value);
} catch (error) {
console.error('Failed to create torrent from magnet:', error);
}
}, [createTorrentFromMagnet]);

const queryInputOnSubmit = React.useCallback((event) => {
event.preventDefault();
const searchValue = `/search?search=${event.target.value}`;
setCurrentQuery(searchValue);
if (searchInputRef.current && searchValue) {
window.location.hash = searchValue;
closeHistory();
}
}, []);

const queryInputClear = React.useCallback(() => {
searchInputRef.current.value = '';
setCurrentQuery('');
window.location.hash = '/search';
}, []);

const updateLocalSearchDebounced = React.useCallback(debounce((query) => {
localSearch.search(query);
}, 250), []);

React.useEffect(() => {
updateLocalSearchDebounced(currentQuery);
}, [currentQuery]);

React.useEffect(() => {
if (routeFocused && active) {
searchInputRef.current.focus();
}
}, [routeFocused, active, query]);
}, [routeFocused, active]);

React.useEffect(() => {
return () => {
updateLocalSearchDebounced.cancel();
};
}, []);

return (
<label className={classnames(className, styles['search-bar-container'], { 'active': active })} onClick={searchBarOnClick}>
<div className={classnames(className, styles['search-bar-container'], { 'active': active })} onClick={searchBarOnClick} ref={containerRef}>
{
active ?
<TextInput
Expand All @@ -53,18 +109,72 @@ const SearchBar = ({ className, query, active }) => {
tabIndex={-1}
onChange={queryInputOnChange}
onSubmit={queryInputOnSubmit}
onClick={openHistory}
/>
:
<div className={styles['search-input']}>
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div>
}
<Button className={styles['submit-button-container']} tabIndex={-1} onClick={queryInputOnSubmit}>
<Icon className={styles['icon']} name={'search'} />
</Button>
</label>
{
currentQuery.length > 0 ?
<Button className={styles['submit-button-container']} onClick={queryInputClear}>
<Icon className={styles['icon']} name={'close'} />
</Button>
:
<Button className={styles['submit-button-container']}>
<Icon className={styles['icon']} name={'search'} />
</Button>
}
{
historyOpen && (searchHistory?.items?.length || localSearch?.items?.length) ?
<div className={styles['menu-container']}>
{
searchHistory?.items?.length > 0 ?
<div className={styles['items']}>
<div className={styles['title']}>
<div className={styles['label']}>{ t('STREMIO_TV_SEARCH_HISTORY_TITLE') }</div>
<button className={styles['search-history-clear']} onClick={searchHistory.clear}>
{ t('CLEAR_HISTORY') }
</button>
</div>
{
searchHistory.items.slice(0, 8).map(({ query, deepLinks }, index) => (
<Button key={index} className={styles['item']} href={deepLinks.search} onClick={closeHistory}>
{query}
</Button>
))
}
</div>
:
null
}
{
localSearch?.items?.length ?
<div className={styles['items']}>
<div className={styles['title']}>
<div className={styles['label']}>{ t('Recommendations') }</div>
</div>
{
localSearch.items.map(({ query, deepLinks }, index) => (
<Button key={index} className={styles['item']} href={deepLinks.search} onClick={closeHistory}>
{query}
</Button>
))
}
</div>
:
null
}
</div>
:
null
}
</div>
);
};
});

SearchBar.displayName = 'SearchBar';

SearchBar.propTypes = {
className: PropTypes.string,
Expand Down
68 changes: 68 additions & 0 deletions src/common/NavBar/HorizontalNavBar/SearchBar/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
height: var(--search-bar-size);
border-radius: var(--search-bar-size);
background-color: var(--overlay-color);
position: relative;
overflow: visible;

.search-input {
flex: 1;
Expand Down Expand Up @@ -46,4 +48,70 @@
opacity: 0.6;
}
}

.menu-container {
position: absolute;
top: 100%;
left: 0;
width: 100%;
height: auto;
z-index: 10;
padding: 1rem;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
gap: 1.5rem;
background-color: var(--modal-background-color);
border-radius: var(--border-radius);

.label {
font-size: 0.9rem;
color: var(--primary-foreground-color);
}

.title {
display: flex;
justify-content: space-between;
width: 100%;
opacity: 0.8;
padding-bottom: 1rem;

.search-history-clear {
cursor: pointer;
color: var(--primary-foreground-color);
font-size: 0.9rem;

&:hover {
opacity: 0.6;
}
}
}

.items {
width: 100%;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;

.item {
width: 90%;
color: var(--primary-foreground-color);
text-align: left;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
width: 100%;
cursor: pointer;
z-index: 10;

&:hover {
background-color: var(--secondary-background-color);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare const useLocalSearch: () => { items: LocalSearchItem[], search: (query: string) => void };
export = useLocalSearch;
38 changes: 38 additions & 0 deletions src/common/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (C) 2017-2023 Smart code 203358507

const React = require('react');
const { useServices } = require('stremio/services');
const useModelState = require('stremio/common/useModelState');

const useLocalSearch = () => {
const { core } = useServices();

const action = React.useMemo(() => ({
action: 'Load',
args: {
model: 'LocalSearch',
}
}), []);

const { items } = useModelState({ model: 'local_search', action });

const search = React.useCallback((query) => {
core.transport.dispatch({
action: 'Search',
args: {
action: 'Search',
args: {
searchQuery: query,
maxResults: 5
}
},
});
}, []);

return {
items,
search,
};
};

module.exports = useLocalSearch;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare const useSearchHistory: () => { items: SearchHistory, clear: () => void };
export = useSearchHistory;
26 changes: 26 additions & 0 deletions src/common/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (C) 2017-2023 Smart code 203358507

const React = require('react');
const useModelState = require('stremio/common/useModelState');
const { useServices } = require('stremio/services');

const useSearchHistory = () => {
const { core } = useServices();
const { searchHistory: items } = useModelState({ model: 'ctx' });

const clear = React.useCallback(() => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'ClearSearchHistory',
},
});
}, []);

return {
items,
clear,
};
};

module.exports = useSearchHistory;
2 changes: 1 addition & 1 deletion src/routes/Search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ Search.propTypes = {
};

const SearchFallback = ({ queryParams }) => (
<MainNavBars className={styles['search-container']} route={'search'} query={queryParams.get('search')} />
<MainNavBars className={styles['search-container']} route={'search'} query={queryParams.get('search') ?? queryParams.get('query')} />
);

SearchFallback.propTypes = Search.propTypes;
Expand Down
Loading

0 comments on commit 0344999

Please sign in to comment.