diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 040367b9..6c3b475e 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -8,7 +8,7 @@ fi if [ ${PIP_PACKAGES:+x} ]; then echo "-- INSTALLING PIP PACKAGES $PIP_PACKAGES --" - python3 -m pip install --no-cache "$PIP_PACKAGES" + python3 -m pip install --no-cache --upgrade "$PIP_PACKAGES" fi exec "$@" diff --git a/src/js/App.js b/src/js/App.js index 11842dd9..d097b168 100755 --- a/src/js/App.js +++ b/src/js/App.js @@ -53,7 +53,7 @@ const Content = () => ( }/> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/src/js/components/ContextMenu/PlaylistSubmenu.js b/src/js/components/ContextMenu/PlaylistSubmenu.js index 10ba22c6..7aa6f43f 100644 --- a/src/js/components/ContextMenu/PlaylistSubmenu.js +++ b/src/js/components/ContextMenu/PlaylistSubmenu.js @@ -3,6 +3,7 @@ import { useDispatch } from 'react-redux'; import { compact } from 'lodash'; import { I18n } from '../../locale'; import Link from '../Link'; +import Loader from '../Loader'; import Icon from '../Icon'; import { encodeUri } from '../../util/format'; import { diff --git a/src/js/components/Fields/OutputControl.js b/src/js/components/Fields/OutputControl.js index 065c8eea..1bee0bb2 100755 --- a/src/js/components/Fields/OutputControl.js +++ b/src/js/components/Fields/OutputControl.js @@ -1,15 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { connect, useSelector, useDispatch } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { find, groupBy, map } from 'lodash'; +import { useSelector, useDispatch } from 'react-redux'; +import { find, groupBy, map, isEmpty } from 'lodash'; import VolumeControl from './VolumeControl'; import MuteControl from './MuteControl'; import Icon from '../Icon'; import Thumbnail from '../Thumbnail'; import LinksSentence from '../LinksSentence'; import DropdownField from './DropdownField'; -import * as coreActions from '../../services/core/actions'; -import * as mopidyActions from '../../services/mopidy/actions'; import * as pusherActions from '../../services/pusher/actions'; import * as snapcastActions from '../../services/snapcast/actions'; import { sortItems, indexToArray } from '../../util/arrays'; @@ -166,18 +163,10 @@ const Group = ({ }; const Outputs = () => { - const snapcastEnabled = useSelector((state) => state.snapcast.enabled); const allGroups = indexToArray(useSelector((state) => state.snapcast.groups || {})); const allStreams = useSelector((state) => state.snapcast.streams || {}); const allServers = indexToArray(useSelector((state) => state.mopidy.servers || {})); const groupsByStream = groupBy(allGroups, 'stream_id'); - if (!snapcastEnabled) { - return ( -

- -

- ); - } return ( @@ -199,12 +188,10 @@ const Outputs = () => { ); } -const Commands = () => { +const Commands = ({ commands }) => { const dispatch = useDispatch(); - const commandsObj = useSelector((state) => state.pusher.commands || {}); - if (!commandsObj) return null; - let items = indexToArray(commandsObj); + let items = indexToArray(commands); if (items.length <= 0) return null; items = sortItems(items, 'sort_order'); @@ -230,6 +217,8 @@ const Commands = () => { }; const OutputControl = ({ force_expanded }) => { + const snapcastEnabled = useSelector((state) => state.snapcast.enabled); + const commands = useSelector((state) => state.pusher.commands); const [expanded, setExpanded] = useState(false); useEffect(() => { @@ -238,9 +227,9 @@ const OutputControl = ({ force_expanded }) => { } }, [force_expanded]); + if (!snapcastEnabled && isEmpty(commands)) return null; + if (expanded) { - const outputs = ; - const commands = ; return ( {!force_expanded &&
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();