Route to manage current search state
- By using route, it makes it more readily available for the varying component structure - I believe I handn't done this earlier to avoid refactoring, but it wasn't a complex task - Removing link borders, the browser-provided standard underline hover is now much more astheticly pleasing
This commit is contained in:
@ -53,7 +53,7 @@ const Content = () => (
|
||||
<Route path="queue/history" element={<QueueHistory />}/>
|
||||
<Route path="settings/debug" element={<Debug />} />
|
||||
<Route path="settings/*" element={<Settings />} />
|
||||
<Route path="search/" element={<Search />} />
|
||||
<Route path="search" element={<Search />} />
|
||||
<Route path="search/:type/:term" element={<Search />} />
|
||||
<Route path="artist/:uri/*" element={<Artist />} />
|
||||
<Route path="album/:uri/" element={<Album />} />
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { sortItems } from '../util/arrays';
|
||||
import URILink from './URILink';
|
||||
import Icon from './Icon';
|
||||
@ -11,38 +12,21 @@ import { makeSearchResultsSelector, getSortSelector } from '../util/selectors';
|
||||
|
||||
const SearchResults = ({
|
||||
type,
|
||||
query,
|
||||
sortField,
|
||||
sortReverse: sortReverseProp,
|
||||
uri_schemes_priority,
|
||||
all,
|
||||
results: rawResults,
|
||||
}) => {
|
||||
const encodedTerm = encodeURIComponent(query.term);
|
||||
let results = rawResults;
|
||||
let sortReverse = sortReverseProp;
|
||||
|
||||
if (!results) return null;
|
||||
|
||||
let sort_map = null;
|
||||
switch (sortField) {
|
||||
case 'uri':
|
||||
sort_map = uri_schemes_priority;
|
||||
break;
|
||||
case 'followers':
|
||||
// Followers (aka popularlity works in reverse-numerical order)
|
||||
// Ie "more popular" is a bigger number
|
||||
sortReverse = !sortReverse;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const { term } = useParams();
|
||||
const { sortField, sortReverse } = useSelector(
|
||||
(state) => getSortSelector(state, 'search_results'),
|
||||
);
|
||||
const searchResultsSelector = makeSearchResultsSelector(term, type);
|
||||
const rawResults = useSelector(searchResultsSelector);
|
||||
const encodedTerm = encodeURIComponent(term);
|
||||
let results = [...rawResults];
|
||||
|
||||
results = sortItems(
|
||||
results,
|
||||
(type === 'tracks' && sortField === 'followers' ? 'popularity' : sortField),
|
||||
sortReverse,
|
||||
sort_map,
|
||||
);
|
||||
|
||||
const resultsCount = results.length;
|
||||
@ -50,7 +34,7 @@ const SearchResults = ({
|
||||
results = results.slice(0, 6);
|
||||
}
|
||||
|
||||
if (results.length <= 0) return null;
|
||||
if (all && !results.length) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -72,56 +56,59 @@ const SearchResults = ({
|
||||
</URILink>
|
||||
)}
|
||||
</h4>
|
||||
<section className="grid-wrapper">
|
||||
{type === 'artists' && <Grid items={results} show_source_icon mini={all} />}
|
||||
{type === 'albums' && <Grid items={results} show_source_icon mini={all} />}
|
||||
{type === 'playlists' && <Grid items={results} show_source_icon mini={all} />}
|
||||
{type === 'tracks' && (
|
||||
<TrackList
|
||||
source={{
|
||||
uri: `iris:search:${query.type}:${encodedTerm}`,
|
||||
name: 'Search results',
|
||||
type: 'search',
|
||||
}}
|
||||
tracks={results}
|
||||
show_source_icon
|
||||
/>
|
||||
)}
|
||||
{/* <LazyLoadListener enabled={this.props.artists_more && spotify_search_enabled} loadMore={loadMore} /> */}
|
||||
{results.length > 0 && (
|
||||
<section className="grid-wrapper">
|
||||
{type === 'artists' && <Grid items={results} show_source_icon mini={all} />}
|
||||
{type === 'albums' && <Grid items={results} show_source_icon mini={all} />}
|
||||
{type === 'playlists' && <Grid items={results} show_source_icon mini={all} />}
|
||||
{type === 'tracks' && (
|
||||
<TrackList
|
||||
source={{
|
||||
uri: `iris:search:${type}:${encodedTerm}`,
|
||||
name: 'Search results',
|
||||
type: 'search',
|
||||
}}
|
||||
tracks={results}
|
||||
show_source_icon
|
||||
/>
|
||||
)}
|
||||
{/* <LazyLoadListener enabled={this.props.artists_more && spotify_search_enabled} loadMore={loadMore} /> */}
|
||||
|
||||
{resultsCount > results.length && (
|
||||
<Button uri={`iris:search:${type}:${encodedTerm}`} uriType="search" unencoded>
|
||||
<I18n path={`search.${type}.more`} count={resultsCount} />
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
{resultsCount > results.length && (
|
||||
<Button uri={`iris:search:${type}:${encodedTerm}`} uriType="search" unencoded>
|
||||
<I18n path={`search.${type}.more`} count={resultsCount} />
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
query: {
|
||||
term,
|
||||
},
|
||||
type,
|
||||
} = ownProps;
|
||||
const {
|
||||
ui: {
|
||||
uri_schemes_priority = [],
|
||||
},
|
||||
} = state;
|
||||
const searchResultsSelector = makeSearchResultsSelector(term, type);
|
||||
const { sortField, sortReverse } = getSortSelector(state, 'search_results');
|
||||
const AllSearchResults = () => (
|
||||
<>
|
||||
<div className="search-result-sections cf">
|
||||
<section className="search-result-sections__item">
|
||||
<div className="inner">
|
||||
<SearchResults type="artists" all />
|
||||
</div>
|
||||
</section>
|
||||
<section className="search-result-sections__item">
|
||||
<div className="inner">
|
||||
<SearchResults type="albums" all />
|
||||
</div>
|
||||
</section>
|
||||
<section className="search-result-sections__item">
|
||||
<div className="inner">
|
||||
<SearchResults type="playlists" all />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<SearchResults type="tracks" all />
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
results: searchResultsSelector(state),
|
||||
uri_schemes_priority,
|
||||
sortField,
|
||||
sortReverse,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
|
||||
export {
|
||||
SearchResults,
|
||||
AllSearchResults,
|
||||
}
|
||||
@ -1,294 +1,94 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Header from '../components/Header';
|
||||
import Icon from '../components/Icon';
|
||||
import DropdownField from '../components/Fields/DropdownField';
|
||||
import SearchForm from '../components/Fields/SearchForm';
|
||||
import SearchResults from '../components/SearchResults';
|
||||
import * as coreActions from '../services/core/actions';
|
||||
import * as uiActions from '../services/ui/actions';
|
||||
import * as mopidyActions from '../services/mopidy/actions';
|
||||
import * as spotifyActions from '../services/spotify/actions';
|
||||
import { titleCase } from '../util/helpers';
|
||||
import { withRouter } from '../util';
|
||||
import { AllSearchResults, SearchResults } from '../components/SearchResults';
|
||||
import { startSearch } from '../services/core/actions';
|
||||
import {
|
||||
set,
|
||||
hideContextMenu,
|
||||
setWindowTitle,
|
||||
} from '../services/ui/actions';
|
||||
import { i18n } from '../locale';
|
||||
import { getSortSelector } from '../util/selectors';
|
||||
|
||||
class Search extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { term: props.term || '' };
|
||||
}
|
||||
const Search = () => {
|
||||
const { term, type = 'all' } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const lastQuery = useSelector((state) => state.core?.search_results?.query);
|
||||
const { sortField, sortReverse } = useSelector(
|
||||
(state) => getSortSelector(state, 'search_results'),
|
||||
);
|
||||
|
||||
componentDidMount = () => {
|
||||
const {
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
setWindowTitle('Search');
|
||||
|
||||
// Auto-focus on the input field
|
||||
useEffect(() => {
|
||||
dispatch(setWindowTitle('Search'));
|
||||
$(document).find('.search-form input').focus();
|
||||
this.digestUri();
|
||||
}
|
||||
}, []);
|
||||
|
||||
componentDidUpdate = ({ term: prevTerm }) => {
|
||||
const { term: termProp } = this.props;
|
||||
if (prevTerm !== termProp) {
|
||||
this.search();
|
||||
useEffect(() => {
|
||||
if (term && type && term !== lastQuery?.term) {
|
||||
dispatch(setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) })));
|
||||
dispatch(startSearch({ term, type }));
|
||||
}
|
||||
}, [term, type])
|
||||
|
||||
const onSubmit = (nextTerm) => {
|
||||
const encodedTerm = encodeURIComponent(nextTerm);
|
||||
navigate(`/search/${type}/${encodedTerm}`);
|
||||
}
|
||||
|
||||
onSubmit = (term) => {
|
||||
const { navigate, type } = this.props;
|
||||
const encodedTerm = encodeURIComponent(term);
|
||||
const onReset = () => navigate('/search');
|
||||
|
||||
this.setState(
|
||||
{ term },
|
||||
() => {
|
||||
navigate(`/search/${type}/${encodedTerm}`);
|
||||
},
|
||||
);
|
||||
const onSortChange = (value) => {
|
||||
dispatch(set({ uri_schemes_search_enabled: value }));
|
||||
dispatch(hideContextMenu());
|
||||
}
|
||||
|
||||
onReset = () => {
|
||||
const { navigate } = this.props;
|
||||
navigate('/search');
|
||||
}
|
||||
const sortOptions = [
|
||||
{ value: 'followers', label: i18n('common.popularity') },
|
||||
{ value: 'name', label: i18n('common.name') },
|
||||
{ value: 'artist', label: i18n('common.artist') },
|
||||
{ value: 'duration', label: i18n('common.duration') },
|
||||
];
|
||||
|
||||
onSortChange = (value) => {
|
||||
const { uiActions: { hideContextMenu } } = this.props;
|
||||
this.setSort(value);
|
||||
hideContextMenu();
|
||||
}
|
||||
const options = (
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name={i18n('common.sort')}
|
||||
value={sortField}
|
||||
options={sortOptions}
|
||||
selected_icon={sortField ? (sortReverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down') : null}
|
||||
handleChange={onSortChange}
|
||||
valueAsLabel
|
||||
/>
|
||||
);
|
||||
|
||||
onSourceChange = (value) => {
|
||||
const {
|
||||
uiActions: {
|
||||
set,
|
||||
hideContextMenu,
|
||||
},
|
||||
} = this.props;
|
||||
set({ uri_schemes_search_enabled: value });
|
||||
hideContextMenu();
|
||||
}
|
||||
return (
|
||||
<div className="view search-view">
|
||||
<Header options={options}>
|
||||
<Icon name="search" type="material" />
|
||||
</Header>
|
||||
|
||||
onSourceClose = () => {
|
||||
this.search(true);
|
||||
};
|
||||
<SearchForm
|
||||
key={`search_form_${type}_${term}`}
|
||||
term={term}
|
||||
onSubmit={onSubmit}
|
||||
onReset={onReset}
|
||||
/>
|
||||
|
||||
digestUri = () => {
|
||||
const { term } = this.props;
|
||||
if (term) {
|
||||
this.setState({ term }, this.search);
|
||||
} else {
|
||||
this.clearSearch();
|
||||
}
|
||||
}
|
||||
|
||||
clearSearch = () => {
|
||||
const {
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
setWindowTitle(i18n('search.title'));
|
||||
this.setState({ term: '' });
|
||||
}
|
||||
|
||||
search = (force = false) => {
|
||||
const {
|
||||
coreActions: {
|
||||
startSearch,
|
||||
},
|
||||
uiActions: {
|
||||
setWindowTitle,
|
||||
},
|
||||
search_results_query: {
|
||||
type: existingType,
|
||||
term: existingTerm,
|
||||
},
|
||||
type,
|
||||
} = this.props;
|
||||
const { term } = this.state;
|
||||
|
||||
setWindowTitle(i18n('search.title_window', { term: decodeURIComponent(term) }));
|
||||
|
||||
if ((type && term && (force || existingType !== type || existingTerm !== term))) {
|
||||
startSearch({ type, term });
|
||||
}
|
||||
}
|
||||
|
||||
setSort = (value) => {
|
||||
const {
|
||||
sort,
|
||||
sort_reverse,
|
||||
uiActions: {
|
||||
set,
|
||||
},
|
||||
} = this.props;
|
||||
|
||||
let reverse = false;
|
||||
if (sort === value) reverse = !sort_reverse;
|
||||
|
||||
const data = {
|
||||
search_results_sort_reverse: reverse,
|
||||
search_results_sort: value,
|
||||
};
|
||||
set(data);
|
||||
}
|
||||
|
||||
render = () => {
|
||||
const { term } = this.state;
|
||||
const {
|
||||
uri_schemes,
|
||||
sort,
|
||||
sort_reverse,
|
||||
uri_schemes_search_enabled,
|
||||
uiActions,
|
||||
type,
|
||||
} = this.props;
|
||||
|
||||
const sort_options = [
|
||||
{ value: 'followers', label: i18n('common.popularity') },
|
||||
{ value: 'name', label: i18n('common.name') },
|
||||
{ value: 'artist', label: i18n('common.artist') },
|
||||
{ value: 'duration', label: i18n('common.duration') },
|
||||
];
|
||||
|
||||
const provider_options = uri_schemes.map((item) => ({
|
||||
value: item,
|
||||
label: titleCase(item.replace(':', '').replace('+', ' ')),
|
||||
}));
|
||||
|
||||
const options = (
|
||||
<>
|
||||
<DropdownField
|
||||
icon="swap_vert"
|
||||
name={i18n('common.sort')}
|
||||
value={sort}
|
||||
valueAsLabel
|
||||
options={sort_options}
|
||||
selected_icon={sort_reverse ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}
|
||||
handleChange={this.onSortChange}
|
||||
/>
|
||||
<DropdownField
|
||||
icon="cloud"
|
||||
name={i18n('common.sources')}
|
||||
value={uri_schemes_search_enabled}
|
||||
options={provider_options}
|
||||
handleChange={this.onSourceChange}
|
||||
onClose={this.onSourceClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
let searchResults;
|
||||
|
||||
switch (type) {
|
||||
case 'artists':
|
||||
searchResults = <SearchResults type="artists" query={{ term, type: 'artists' }} />;
|
||||
break;
|
||||
case 'albums':
|
||||
searchResults = <SearchResults type="albums" query={{ term, type: 'albums' }} />;
|
||||
break;
|
||||
case 'playlists':
|
||||
searchResults = <SearchResults type="playlists" query={{ term, type: 'playlists' }} />;
|
||||
break;
|
||||
case 'tracks':
|
||||
searchResults = <SearchResults type="tracks" query={{ term, type: 'tracks' }} />
|
||||
break;
|
||||
default:
|
||||
searchResults = (
|
||||
<>
|
||||
<div className="search-result-sections cf">
|
||||
<section className="search-result-sections__item">
|
||||
<div className="inner">
|
||||
<SearchResults type="artists" query={{ term, type: 'artists' }} all />
|
||||
</div>
|
||||
</section>
|
||||
<section className="search-result-sections__item">
|
||||
<div className="inner">
|
||||
<SearchResults type="albums" query={{ term, type: 'albums' }} all />
|
||||
</div>
|
||||
</section>
|
||||
<section className="search-result-sections__item">
|
||||
<div className="inner">
|
||||
<SearchResults type="playlists" query={{ term, type: 'playlists' }} all />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<SearchResults type="tracks" query={{ term, type: 'tracks' }} all />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="view search-view">
|
||||
<Header options={options} uiActions={uiActions}>
|
||||
<Icon name="search" type="material" />
|
||||
</Header>
|
||||
|
||||
<SearchForm
|
||||
key={`search_form_${type}_${term}`}
|
||||
term={term}
|
||||
onSubmit={this.onSubmit}
|
||||
onReset={this.onReset}
|
||||
/>
|
||||
|
||||
<div className="content-wrapper">
|
||||
{searchResults}
|
||||
</div>
|
||||
<div className="content-wrapper">
|
||||
{type != 'all' ? (
|
||||
<SearchResults type={type} />
|
||||
) : (
|
||||
<AllSearchResults />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const {
|
||||
params: {
|
||||
type,
|
||||
term,
|
||||
},
|
||||
navigation,
|
||||
} = ownProps;
|
||||
|
||||
const {
|
||||
mopidy: {
|
||||
uri_schemes = [],
|
||||
},
|
||||
ui: {
|
||||
uri_schemes_search_enabled = [],
|
||||
search_results_sort: sort = 'followers.total',
|
||||
search_results_sort_reverse,
|
||||
},
|
||||
core: {
|
||||
search_results: {
|
||||
query: search_results_query = {},
|
||||
} = {},
|
||||
},
|
||||
} = state;
|
||||
|
||||
return {
|
||||
type: type || 'all',
|
||||
term,
|
||||
navigation,
|
||||
uri_schemes,
|
||||
uri_schemes_search_enabled,
|
||||
sort,
|
||||
sort_reverse: !!search_results_sort_reverse,
|
||||
search_results_query,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
});
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Search));
|
||||
export default Search;
|
||||
|
||||
@ -11,6 +11,10 @@
|
||||
border-bottom: 0 !important;
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@ -111,6 +111,7 @@
|
||||
cursor: pointer;
|
||||
color: colour('white');
|
||||
border: 0 !important;
|
||||
text-decoration: none !important;
|
||||
margin: 0 5px;
|
||||
|
||||
&:hover {
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
border: none !important;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
|
||||
&__inner {
|
||||
|
||||
@ -195,10 +195,8 @@ main {
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.control):not(.action):not(.button) {
|
||||
border-bottom: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
border-color: colour('mid_grey');
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 90px;
|
||||
right: 270px;
|
||||
right: 170px;
|
||||
|
||||
input {
|
||||
@include feature_font();
|
||||
|
||||
Reference in New Issue
Block a user