mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-20 19:02:15 +00:00
Merge pull request #528 from Stremio/feat/search-history
feature: search history
This commit is contained in:
commit
034499942d
12 changed files with 294 additions and 27 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -12,7 +12,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",
|
||||
|
|
@ -3181,9 +3181,9 @@
|
|||
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
|
||||
},
|
||||
"node_modules/@stremio/stremio-core-web": {
|
||||
"version": "0.45.1",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.45.1.tgz",
|
||||
"integrity": "sha512-aoLsi0Mvd/46V5qz/CSdIle+tvHfRYsLsmNolraze469JLwzNXW1g1ZAAkEDayspwIroqd3QasU//GBgTrvDXg==",
|
||||
"version": "0.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.46.0.tgz",
|
||||
"integrity": "sha512-eGc0PfeJB5dYbk1jJz/BJNU+an0/LA8LWAxEDkOq6YtqjvQrsH+djuXL8u0ZDrwxt7YlHhHk6B7W+MBx/gX4pQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.16.0"
|
||||
}
|
||||
|
|
@ -18049,9 +18049,9 @@
|
|||
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
|
||||
},
|
||||
"@stremio/stremio-core-web": {
|
||||
"version": "0.45.1",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.45.1.tgz",
|
||||
"integrity": "sha512-aoLsi0Mvd/46V5qz/CSdIle+tvHfRYsLsmNolraze469JLwzNXW1g1ZAAkEDayspwIroqd3QasU//GBgTrvDXg==",
|
||||
"version": "0.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.46.0.tgz",
|
||||
"integrity": "sha512-eGc0PfeJB5dYbk1jJz/BJNU+an0/LA8LWAxEDkOq6YtqjvQrsH+djuXL8u0ZDrwxt7YlHhHk6B7W+MBx/gX4pQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "7.16.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
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');
|
||||
|
|
@ -10,37 +11,92 @@ 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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/common/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.d.ts
vendored
Normal file
2
src/common/NavBar/HorizontalNavBar/SearchBar/useLocalSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
declare const useLocalSearch: () => { items: LocalSearchItem[], search: (query: string) => void };
|
||||
export = useLocalSearch;
|
||||
|
|
@ -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;
|
||||
2
src/common/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.d.ts
vendored
Normal file
2
src/common/NavBar/HorizontalNavBar/SearchBar/useSearchHistory.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
declare const useSearchHistory: () => { items: SearchHistory, clear: () => void };
|
||||
export = useSearchHistory;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -32,14 +32,15 @@ const useSearch = (queryParams) => {
|
|||
// };
|
||||
// }, [queryParams.get('search')]);
|
||||
const action = React.useMemo(() => {
|
||||
if (queryParams.has('search') && queryParams.get('search').length > 0) {
|
||||
const query = queryParams.get('search') ?? queryParams.get('query');
|
||||
if (query?.length > 0) {
|
||||
return {
|
||||
action: 'Load',
|
||||
args: {
|
||||
model: 'CatalogsWithExtra',
|
||||
args: {
|
||||
extra: [
|
||||
['search', queryParams.get('search')]
|
||||
['search', query]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
src/types/models/Ctx.d.ts
vendored
10
src/types/models/Ctx.d.ts
vendored
|
|
@ -57,7 +57,17 @@ type NotificationItem = {
|
|||
videoReleased: string,
|
||||
}
|
||||
|
||||
type SearchHistoryItem = {
|
||||
query: string,
|
||||
deepLinks: {
|
||||
search: string,
|
||||
},
|
||||
};
|
||||
|
||||
type SearchHistory = SearchHistoryItem[];
|
||||
|
||||
type Ctx = {
|
||||
profile: Profile,
|
||||
notifications: Notifications,
|
||||
searchHistory: SearchHistory,
|
||||
};
|
||||
10
src/types/models/LocalSearch.d.ts
vendored
Normal file
10
src/types/models/LocalSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
type LocalSearchItem = {
|
||||
query: string,
|
||||
deepLinks: {
|
||||
search: string,
|
||||
},
|
||||
};
|
||||
|
||||
type LocalSearch = {
|
||||
items: LocalSearchItem[],
|
||||
};
|
||||
Loading…
Reference in a new issue