Add Shuffle Play Option on List Page and Menus

This commit is contained in:
saltrafael 2021-08-24 10:14:28 -03:00 committed by zeppi
parent 3376986c26
commit 6ec25b0f71
10 changed files with 216 additions and 62 deletions

View file

@ -30,6 +30,8 @@ import { doToast } from 'redux/actions/notifications';
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectListShuffle } from 'redux/selectors/content';
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
import ClaimPreview from './view'; import ClaimPreview from './view';
import fs from 'fs'; import fs from 'fs';
@ -42,6 +44,9 @@ const select = (state, props) => {
const contentSigningChannel = contentClaim && contentClaim.signing_channel; const contentSigningChannel = contentClaim && contentClaim.signing_channel;
const contentPermanentUri = contentClaim && contentClaim.permanent_url; const contentPermanentUri = contentClaim && contentClaim.permanent_url;
const contentChannelUri = (contentSigningChannel && contentSigningChannel.permanent_url) || contentPermanentUri; const contentChannelUri = (contentSigningChannel && contentSigningChannel.permanent_url) || contentPermanentUri;
const shuffleList = selectListShuffle(state);
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
const playNextUri = shuffle && shuffle[0];
return { return {
claim, claim,
@ -69,6 +74,7 @@ const select = (state, props) => {
editedCollection: makeSelectEditedCollectionForId(collectionId)(state), editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
isAuthenticated: Boolean(selectUserVerifiedEmail(state)), isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
resolvedList, resolvedList,
playNextUri,
}; };
}; };
@ -96,6 +102,8 @@ const perform = (dispatch) => ({
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)), doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)), doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })), fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })),
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
}); });
export default connect(select, perform)(ClaimPreview); export default connect(select, perform)(ClaimPreview);

View file

@ -58,6 +58,9 @@ type Props = {
isAuthenticated: boolean, isAuthenticated: boolean,
fetchCollectionItems: (string) => void, fetchCollectionItems: (string) => void,
resolvedList: boolean, resolvedList: boolean,
playNextUri: string,
doSetPlayingUri: (string) => void,
doToggleShuffleList: (string) => void,
}; };
function ClaimMenuList(props: Props) { function ClaimMenuList(props: Props) {
@ -97,7 +100,11 @@ function ClaimMenuList(props: Props) {
isAuthenticated, isAuthenticated,
fetchCollectionItems, fetchCollectionItems,
resolvedList, resolvedList,
playNextUri,
doSetPlayingUri,
doToggleShuffleList,
} = props; } = props;
const [doShuffle, setDoShuffle] = React.useState(false);
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@'); const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
const isChannel = !incognitoClaim && !contentSigningChannel; const isChannel = !incognitoClaim && !contentSigningChannel;
const { channelName } = parseURI(contentChannelUri); const { channelName } = parseURI(contentChannelUri);
@ -118,6 +125,20 @@ function ClaimMenuList(props: Props) {
} }
}, [collectionId, fetchCollectionItems]); }, [collectionId, fetchCollectionItems]);
React.useEffect(() => {
if (resolvedList && doShuffle) {
doToggleShuffleList(collectionId);
if (playNextUri) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
setDoShuffle(false);
doSetPlayingUri(playNextUri);
push(navigateUrl);
}
}
}, [playNextUri, doShuffle, resolvedList, doToggleShuffleList, collectionId, doSetPlayingUri, push]);
if (!claim) { if (!claim) {
return null; return null;
} }
@ -268,6 +289,18 @@ function ClaimMenuList(props: Props) {
{__('View List')} {__('View List')}
</a> </a>
</MenuItem> </MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => {
if (!resolvedList) fetchItems();
setDoShuffle(true);
}}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SHUFFLE} />
{__('Shuffle Play')}
</div>
</MenuItem>
{isMyCollection && ( {isMyCollection && (
<> <>
<MenuItem <MenuItem

View file

@ -10,21 +10,33 @@ import {
import { makeSelectCostInfoForUri } from 'lbryinc'; import { makeSelectCostInfoForUri } from 'lbryinc';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { selectListShuffle } from 'redux/selectors/content';
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
import CollectionActions from './view'; import CollectionActions from './view';
const select = (state, props) => ({ const select = (state, props) => {
claim: makeSelectClaimForUri(props.uri)(state), const collectionId = props.collectionId;
claimIsMine: makeSelectClaimIsMine(props.uri)(state), const shuffleList = selectListShuffle(state);
costInfo: makeSelectCostInfoForUri(props.uri)(state), const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
myChannels: selectMyChannelClaims(state), const playNextUri = shuffle && shuffle[0];
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state), return {
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)), claim: makeSelectClaimForUri(props.uri)(state),
}); claimIsMine: makeSelectClaimIsMine(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state),
myChannels: selectMyChannelClaims(state),
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
playNextUri,
};
};
const perform = (dispatch) => ({ const perform = (dispatch) => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)), doToast: (options) => dispatch(doToast(options)),
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
}); });
export default connect(select, perform)(CollectionActions); export default connect(select, perform)(CollectionActions);

View file

@ -11,6 +11,8 @@ import { useHistory } from 'react-router';
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view'; import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
import classnames from 'classnames'; import classnames from 'classnames';
import { ENABLE_FILE_REACTIONS } from 'config'; import { ENABLE_FILE_REACTIONS } from 'config';
import { COLLECTIONS_CONSTS } from 'lbry-redux';
import { formatLbryUrlForWeb } from 'util/url';
type Props = { type Props = {
uri: string, uri: string,
@ -24,6 +26,10 @@ type Props = {
showInfo: boolean, showInfo: boolean,
setShowInfo: (boolean) => void, setShowInfo: (boolean) => void,
collectionHasEdits: boolean, collectionHasEdits: boolean,
doToggleShuffleList: (string) => void,
playNextUri: string,
doSetPlayingUri: (string) => void,
isBuiltin: boolean,
}; };
function CollectionActions(props: Props) { function CollectionActions(props: Props) {
@ -37,61 +43,92 @@ function CollectionActions(props: Props) {
showInfo, showInfo,
setShowInfo, setShowInfo,
collectionHasEdits, collectionHasEdits,
doToggleShuffleList,
playNextUri,
doSetPlayingUri,
isBuiltin,
} = props; } = props;
const [doShuffle, setDoShuffle] = React.useState(false);
const { push } = useHistory(); const { push } = useHistory();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const webShareable = true; // collections have cost? const webShareable = true; // collections have cost?
React.useEffect(() => {
if (playNextUri && doShuffle) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
setDoShuffle(false);
doSetPlayingUri(playNextUri);
push(navigateUrl);
}
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]);
const lhsSection = ( const lhsSection = (
<> <>
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />} <Button
{uri && <ClaimSupportButton uri={uri} fileAction />} className="button--file-action"
{/* TODO Add ClaimRepostButton component */} icon={ICONS.SHUFFLE}
{uri && ( label={__('Shuffle Play')}
<Button title={__('Shuffle Play')}
className="button--file-action" onClick={() => {
icon={ICONS.SHARE} doToggleShuffleList(collectionId);
label={__('Share')} setDoShuffle(true);
title={__('Share')} }}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })} />
/> {!isBuiltin && (
<>
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
{uri && <ClaimSupportButton uri={uri} fileAction />}
{/* TODO Add ClaimRepostButton component */}
{uri && (
<Button
className="button--file-action"
icon={ICONS.SHARE}
label={__('Share')}
title={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
/>
)}
</>
)} )}
</> </>
); );
const rhsSection = ( const rhsSection = (
<> <>
{isMyCollection && ( {!isBuiltin &&
<Button (isMyCollection ? (
title={uri ? __('Update') : __('Publish')} <>
label={uri ? __('Update') : __('Publish')} <Button
className={classnames('button--file-action')} title={uri ? __('Update') : __('Publish')}
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)} label={uri ? __('Update') : __('Publish')}
icon={ICONS.PUBLISH} className={classnames('button--file-action')}
iconColor={collectionHasEdits && 'red'} onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
iconSize={18} icon={ICONS.PUBLISH}
disabled={claimIsPending} iconColor={collectionHasEdits && 'red'}
/> iconSize={18}
)} disabled={claimIsPending}
{isMyCollection && ( />
<Button <Button
className={classnames('button--file-action')} className={classnames('button--file-action')}
title={__('Delete List')} title={__('Delete List')}
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })} onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
icon={ICONS.DELETE} icon={ICONS.DELETE}
iconSize={18} iconSize={18}
description={__('Delete List')} description={__('Delete List')}
disabled={claimIsPending} disabled={claimIsPending}
/> />
)} </>
{!isMyCollection && ( ) : (
<Button <Button
title={__('Report content')} title={__('Report content')}
className="button--file-action" className="button--file-action"
icon={ICONS.REPORT} icon={ICONS.REPORT}
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`} navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
/> />
)} ))}
</> </>
); );

View file

@ -18,7 +18,7 @@ type Props = {
loop: boolean, loop: boolean,
shuffle: boolean, shuffle: boolean,
doToggleLoopList: (string, boolean) => void, doToggleLoopList: (string, boolean) => void,
doToggleShuffleList: (string, boolean) => void, doToggleShuffleList: (string, string, boolean) => void,
createUnpublishedCollection: (string, Array<any>, ?string) => void, createUnpublishedCollection: (string, Array<any>, ?string) => void,
}; };
@ -57,7 +57,7 @@ export default function CollectionContent(props: Props) {
icon={ICONS.SHUFFLE} icon={ICONS.SHUFFLE}
iconColor={shuffle && 'blue'} iconColor={shuffle && 'blue'}
className="button--file-action" className="button--file-action"
onClick={() => doToggleShuffleList(id, !shuffle)} onClick={() => doToggleShuffleList(url, id, !shuffle)}
/> />
</span> </span>
</> </>

View file

@ -1,11 +1,19 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux'; import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { selectListShuffle } from 'redux/selectors/content';
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
import CollectionMenuList from './view'; import CollectionMenuList from './view';
const select = (state, props) => { const select = (state, props) => {
const collectionId = props.collectionId;
const shuffleList = selectListShuffle(state);
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
const playNextUri = shuffle && shuffle[0];
return { return {
collectionName: makeSelectNameForCollectionId(props.collectionId)(state), collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
playNextUri,
}; };
}; };
@ -13,4 +21,6 @@ export default connect(select, {
doCollectionEdit, doCollectionEdit,
doOpenModal, doOpenModal,
doCollectionDelete, doCollectionDelete,
doSetPlayingUri,
doToggleShuffleList,
})(CollectionMenuList); })(CollectionMenuList);

View file

@ -7,19 +7,44 @@ import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { formatLbryUrlForWeb } from 'util/url';
import { COLLECTIONS_CONSTS } from 'lbry-redux';
type Props = { type Props = {
inline?: boolean, inline?: boolean,
doOpenModal: (string, {}) => void, doOpenModal: (string, {}) => void,
collectionName?: string, collectionName?: string,
collectionId: string, collectionId: string,
playNextUri: string,
doSetPlayingUri: ({ uri: ?string }) => void,
doToggleShuffleList: (string, string, boolean, boolean) => void,
}; };
function CollectionMenuList(props: Props) { function CollectionMenuList(props: Props) {
const { inline = false, collectionId, collectionName, doOpenModal } = props; const {
inline = false,
collectionId,
collectionName,
doOpenModal,
playNextUri,
doSetPlayingUri,
doToggleShuffleList,
} = props;
const [doShuffle, setDoShuffle] = React.useState(false);
const { push } = useHistory(); const { push } = useHistory();
React.useEffect(() => {
if (playNextUri && doShuffle) {
const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
setDoShuffle(false);
doSetPlayingUri({ uri: playNextUri });
push(navigateUrl);
}
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]);
return ( return (
<Menu> <Menu>
<MenuButton <MenuButton
@ -40,6 +65,18 @@ function CollectionMenuList(props: Props) {
{__('View List')} {__('View List')}
</a> </a>
</MenuItem> </MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => {
doToggleShuffleList('', collectionId, true, true);
setDoShuffle(true);
}}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SHUFFLE} />
{__('Shuffle Play')}
</div>
</MenuItem>
<MenuItem <MenuItem
className="comment__menu-option" className="comment__menu-option"
onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)} onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}

View file

@ -112,18 +112,26 @@ export default function CollectionPage(props: Props) {
title={ title={
<span> <span>
<Icon <Icon
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) || icon={
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK} (collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
className="icon--margin-right" /> (collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
ICONS.STACK
}
className="icon--margin-right"
/>
{claim ? claim.value.title || claim.name : collection && collection.name} {claim ? claim.value.title || claim.name : collection && collection.name}
</span> </span>
} }
titleActions={titleActions} titleActions={titleActions}
subtitle={subTitle} subtitle={subTitle}
body={ body={
!isBuiltin && ( <CollectionActions
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} /> uri={uri}
) collectionId={collectionId}
setShowInfo={setShowInfo}
showInfo={showInfo}
isBuiltin={isBuiltin}
/>
} }
actions={ actions={
showInfo && showInfo &&

View file

@ -298,17 +298,25 @@ export function doToggleLoopList(collectionId: string, loop: boolean, hideToast:
}; };
} }
export function doToggleShuffleList(collectionId: string, shuffle: boolean, hideToast: boolean) { export function doToggleShuffleList(currentUri: string, collectionId: string, shuffle: boolean, hideToast: boolean) {
return (dispatch: Dispatch, getState: () => any) => { return (dispatch: Dispatch, getState: () => any) => {
if (shuffle) { if (shuffle) {
const state = getState(); const state = getState();
const urls = makeSelectUrlsForCollectionId(collectionId)(state); const urls = makeSelectUrlsForCollectionId(collectionId)(state);
const newUrls = urls let newUrls = urls
.map((item) => ({ item, sort: Math.random() })) .map((item) => ({ item, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort) .sort((a, b) => a.sort - b.sort)
.map(({ item }) => item); .map(({ item }) => item);
// the currently playing URI should be first in list or else
// can get in strange position where it might be in the middle or last
// and the shuffled list ends before scrolling through all entries
if (currentUri && currentUri !== '') {
newUrls.splice(newUrls.indexOf(currentUri), 1);
newUrls.splice(0, 0, currentUri);
}
dispatch({ dispatch({
type: ACTIONS.TOGGLE_SHUFFLE_LIST, type: ACTIONS.TOGGLE_SHUFFLE_LIST,
data: { collectionId, newUrls }, data: { collectionId, newUrls },

View file

@ -314,6 +314,7 @@ $thumbnailWidthSmall: 1rem;
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
.menu__link { .menu__link {
color: var(--color-text);
padding: 0; padding: 0;
} }
} }