setExpanded(false)} />}
@@ -251,8 +240,8 @@ const OutputControl = ({ force_expanded }) => {
- {commands}
- {outputs}
+ {!isEmpty(commands) && }
+ {snapcastEnabled && }
);
diff --git a/src/js/components/SearchResults.js b/src/js/components/SearchResults.js
index 663bcd5b..3043a2a3 100755
--- a/src/js/components/SearchResults.js
+++ b/src/js/components/SearchResults.js
@@ -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 (
@@ -72,56 +56,59 @@ const SearchResults = ({
)}
-
- {type === 'artists' && }
- {type === 'albums' && }
- {type === 'playlists' && }
- {type === 'tracks' && (
-
- )}
- {/* */}
+ {results.length > 0 && (
+
+ {type === 'artists' && }
+ {type === 'albums' && }
+ {type === 'playlists' && }
+ {type === 'tracks' && (
+
+ )}
+ {/* */}
- {resultsCount > results.length && (
-
- )}
-
+ {resultsCount > results.length && (
+
+ )}
+
+ )}
);
};
-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 = () => (
+ <>
+
+
+ >
+);
- return {
- results: searchResultsSelector(state),
- uri_schemes_priority,
- sortField,
- sortReverse,
- };
-};
-
-const mapDispatchToProps = () => ({});
-
-export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
+export {
+ SearchResults,
+ AllSearchResults,
+}
\ No newline at end of file
diff --git a/src/js/views/Search.js b/src/js/views/Search.js
index 76c429df..c969cc4a 100755
--- a/src/js/views/Search.js
+++ b/src/js/views/Search.js
@@ -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 = (
+
+ );
- onSourceChange = (value) => {
- const {
- uiActions: {
- set,
- hideContextMenu,
- },
- } = this.props;
- set({ uri_schemes_search_enabled: value });
- hideContextMenu();
- }
+ return (
+
+
- onSourceClose = () => {
- this.search(true);
- };
+
- 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 = (
- <>
-
-
- >
- );
-
- let searchResults;
-
- switch (type) {
- case 'artists':
- searchResults =
;
- break;
- case 'albums':
- searchResults =
;
- break;
- case 'playlists':
- searchResults =
;
- break;
- case 'tracks':
- searchResults =
- break;
- default:
- searchResults = (
- <>
-
-
- >
- );
- }
-
- return (
-
-
-
-
-
-
- {searchResults}
-
+
+ {type != 'all' ? (
+
+ ) : (
+
+ )}
- );
- }
+
+ );
}
-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;
diff --git a/src/scss/components/_context-menu.scss b/src/scss/components/_context-menu.scss
index 6aea7835..d8a801e8 100755
--- a/src/scss/components/_context-menu.scss
+++ b/src/scss/components/_context-menu.scss
@@ -106,7 +106,8 @@
}
&--loader {
- padding-top: 75%;
+ text-align: center;
+ margin-top: 50%;
}
}
diff --git a/src/scss/components/_grid.scss b/src/scss/components/_grid.scss
index ab544217..b7d7d1f6 100755
--- a/src/scss/components/_grid.scss
+++ b/src/scss/components/_grid.scss
@@ -11,6 +11,10 @@
border-bottom: 0 !important;
cursor: pointer;
+ a {
+ text-decoration: none !important;
+ }
+
&__wrapper {
display: inline-block;
}
diff --git a/src/scss/components/_images.scss b/src/scss/components/_images.scss
index 5046c3e4..00f17953 100755
--- a/src/scss/components/_images.scss
+++ b/src/scss/components/_images.scss
@@ -111,6 +111,7 @@
cursor: pointer;
color: colour('white');
border: 0 !important;
+ text-decoration: none !important;
margin: 0 5px;
&:hover {
diff --git a/src/scss/components/_sub-tabs.scss b/src/scss/components/_sub-tabs.scss
index f9338a00..0ee8d748 100755
--- a/src/scss/components/_sub-tabs.scss
+++ b/src/scss/components/_sub-tabs.scss
@@ -17,7 +17,6 @@
padding: 0 5px;
display: block;
box-sizing: border-box;
- border: none !important;
cursor: pointer;
&__inner {
@@ -60,6 +59,8 @@
}
&:hover {
+ text-decoration: none;
+
.menu-item__inner {
background: lighten(colour('dark_grey'), 4%);
diff --git a/src/scss/components/_sub-views.scss b/src/scss/components/_sub-views.scss
index eb463cca..2ac088a3 100755
--- a/src/scss/components/_sub-views.scss
+++ b/src/scss/components/_sub-views.scss
@@ -7,8 +7,6 @@
margin-right: 25px;
font-size: 15px;
font-weight: 500;
- border-bottom: 0;
- padding-bottom: 3px;
cursor: pointer;
@include theme('light') {
@@ -21,25 +19,7 @@
&--active,
&:hover {
- border-bottom-width: 3px !important;
- border-bottom-style: solid;
- padding-bottom: 0px;
- }
-
- &--active {
- border-color: colour('white') !important;
-
- @include theme('light') {
- border-color: colour('darkest_grey') !important;
- }
- }
-
- &:not(.sub-views__option--active):hover {
- border-color: colour('soft_grey') !important;
-
- @include theme('light') {
- border-color: colour('light_grey') !important;
- }
+ text-decoration: underline 0.15em;
}
}
diff --git a/src/scss/global/_core.scss b/src/scss/global/_core.scss
index 58430cdd..03d478c8 100755
--- a/src/scss/global/_core.scss
+++ b/src/scss/global/_core.scss
@@ -194,12 +194,8 @@ main {
text-decoration: none;
cursor: pointer;
- &:not(.control):not(.action):not(.button) {
- border-bottom: 1px solid transparent;
-
- &:hover {
- border-color: colour('mid_grey');
- }
+ &:hover {
+ text-decoration: underline 0.15em;
}
}
@@ -262,17 +258,16 @@ h2 {
a {
color: inherit;
- text-decoration: none;
+ text-decoration: none 2px;
&:hover {
- border-bottom: 2px solid colour('white');
+ text-decoration: underline 2px;
}
}
&.grey-text {
a:hover {
color: colour('mid_grey') !important;
- border-bottom: 2px solid colour('mid_grey');
}
}
}
@@ -342,7 +337,7 @@ h5 {
text-decoration: none;
&:hover {
- text-decoration: underline;
+ text-decoration: underline 0.15em;
}
}
}
diff --git a/src/scss/global/_forms.scss b/src/scss/global/_forms.scss
index 1d01b908..cd688562 100755
--- a/src/scss/global/_forms.scss
+++ b/src/scss/global/_forms.scss
@@ -214,6 +214,10 @@ select {
transform: translate(1px, 1px);
}
+ &:hover {
+ text-decoration: none;
+ }
+
@include theme('light') {
&--default {
border-color: colour('darkest_grey');
diff --git a/src/scss/views/_artist.scss b/src/scss/views/_artist.scss
index 97878906..d771199e 100755
--- a/src/scss/views/_artist.scss
+++ b/src/scss/views/_artist.scss
@@ -66,7 +66,7 @@
.biography-text {
overflow-wrap: break-word;
- white-space: pre-wrap;
+ white-space: pre-wrap;
}
}
}
diff --git a/src/scss/views/_search.scss b/src/scss/views/_search.scss
index d8bfa5fc..102be9f9 100755
--- a/src/scss/views/_search.scss
+++ b/src/scss/views/_search.scss
@@ -5,7 +5,7 @@
position: absolute;
top: 30px;
left: 90px;
- right: 270px;
+ right: 170px;
input {
@include feature_font();