Add Shuffle Play Option on List Page and Menus
This commit is contained in:
parent
3376986c26
commit
6ec25b0f71
10 changed files with 216 additions and 62 deletions
|
@ -30,6 +30,8 @@ import { doToast } from 'redux/actions/notifications';
|
|||
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||
import ClaimPreview from './view';
|
||||
import fs from 'fs';
|
||||
|
||||
|
@ -42,6 +44,9 @@ const select = (state, props) => {
|
|||
const contentSigningChannel = contentClaim && contentClaim.signing_channel;
|
||||
const contentPermanentUri = contentClaim && contentClaim.permanent_url;
|
||||
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 {
|
||||
claim,
|
||||
|
@ -69,6 +74,7 @@ const select = (state, props) => {
|
|||
editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
resolvedList,
|
||||
playNextUri,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -96,6 +102,8 @@ const perform = (dispatch) => ({
|
|||
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
|
||||
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
|
||||
fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })),
|
||||
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ClaimPreview);
|
||||
|
|
|
@ -58,6 +58,9 @@ type Props = {
|
|||
isAuthenticated: boolean,
|
||||
fetchCollectionItems: (string) => void,
|
||||
resolvedList: boolean,
|
||||
playNextUri: string,
|
||||
doSetPlayingUri: (string) => void,
|
||||
doToggleShuffleList: (string) => void,
|
||||
};
|
||||
|
||||
function ClaimMenuList(props: Props) {
|
||||
|
@ -97,7 +100,11 @@ function ClaimMenuList(props: Props) {
|
|||
isAuthenticated,
|
||||
fetchCollectionItems,
|
||||
resolvedList,
|
||||
playNextUri,
|
||||
doSetPlayingUri,
|
||||
doToggleShuffleList,
|
||||
} = props;
|
||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||
const isChannel = !incognitoClaim && !contentSigningChannel;
|
||||
const { channelName } = parseURI(contentChannelUri);
|
||||
|
@ -118,6 +125,20 @@ function ClaimMenuList(props: Props) {
|
|||
}
|
||||
}, [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) {
|
||||
return null;
|
||||
}
|
||||
|
@ -268,6 +289,18 @@ function ClaimMenuList(props: Props) {
|
|||
{__('View List')}
|
||||
</a>
|
||||
</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 && (
|
||||
<>
|
||||
<MenuItem
|
||||
|
|
|
@ -10,21 +10,33 @@ import {
|
|||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||
import CollectionActions from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
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)),
|
||||
});
|
||||
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 {
|
||||
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) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionActions);
|
||||
|
|
|
@ -11,6 +11,8 @@ import { useHistory } from 'react-router';
|
|||
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
|
||||
import classnames from 'classnames';
|
||||
import { ENABLE_FILE_REACTIONS } from 'config';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -24,6 +26,10 @@ type Props = {
|
|||
showInfo: boolean,
|
||||
setShowInfo: (boolean) => void,
|
||||
collectionHasEdits: boolean,
|
||||
doToggleShuffleList: (string) => void,
|
||||
playNextUri: string,
|
||||
doSetPlayingUri: (string) => void,
|
||||
isBuiltin: boolean,
|
||||
};
|
||||
|
||||
function CollectionActions(props: Props) {
|
||||
|
@ -37,61 +43,92 @@ function CollectionActions(props: Props) {
|
|||
showInfo,
|
||||
setShowInfo,
|
||||
collectionHasEdits,
|
||||
doToggleShuffleList,
|
||||
playNextUri,
|
||||
doSetPlayingUri,
|
||||
isBuiltin,
|
||||
} = props;
|
||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
const { push } = useHistory();
|
||||
const isMobile = useIsMobile();
|
||||
const claimId = claim && claim.claim_id;
|
||||
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 = (
|
||||
<>
|
||||
{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 })}
|
||||
/>
|
||||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.SHUFFLE}
|
||||
label={__('Shuffle Play')}
|
||||
title={__('Shuffle Play')}
|
||||
onClick={() => {
|
||||
doToggleShuffleList(collectionId);
|
||||
setDoShuffle(true);
|
||||
}}
|
||||
/>
|
||||
{!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 = (
|
||||
<>
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
title={uri ? __('Update') : __('Publish')}
|
||||
label={uri ? __('Update') : __('Publish')}
|
||||
className={classnames('button--file-action')}
|
||||
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
||||
icon={ICONS.PUBLISH}
|
||||
iconColor={collectionHasEdits && 'red'}
|
||||
iconSize={18}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
)}
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
className={classnames('button--file-action')}
|
||||
title={__('Delete List')}
|
||||
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
||||
icon={ICONS.DELETE}
|
||||
iconSize={18}
|
||||
description={__('Delete List')}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
)}
|
||||
{!isMyCollection && (
|
||||
<Button
|
||||
title={__('Report content')}
|
||||
className="button--file-action"
|
||||
icon={ICONS.REPORT}
|
||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||
/>
|
||||
)}
|
||||
{!isBuiltin &&
|
||||
(isMyCollection ? (
|
||||
<>
|
||||
<Button
|
||||
title={uri ? __('Update') : __('Publish')}
|
||||
label={uri ? __('Update') : __('Publish')}
|
||||
className={classnames('button--file-action')}
|
||||
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
||||
icon={ICONS.PUBLISH}
|
||||
iconColor={collectionHasEdits && 'red'}
|
||||
iconSize={18}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
<Button
|
||||
className={classnames('button--file-action')}
|
||||
title={__('Delete List')}
|
||||
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
||||
icon={ICONS.DELETE}
|
||||
iconSize={18}
|
||||
description={__('Delete List')}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
title={__('Report content')}
|
||||
className="button--file-action"
|
||||
icon={ICONS.REPORT}
|
||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ type Props = {
|
|||
loop: boolean,
|
||||
shuffle: boolean,
|
||||
doToggleLoopList: (string, boolean) => void,
|
||||
doToggleShuffleList: (string, boolean) => void,
|
||||
doToggleShuffleList: (string, string, boolean) => void,
|
||||
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
||||
};
|
||||
|
||||
|
@ -57,7 +57,7 @@ export default function CollectionContent(props: Props) {
|
|||
icon={ICONS.SHUFFLE}
|
||||
iconColor={shuffle && 'blue'}
|
||||
className="button--file-action"
|
||||
onClick={() => doToggleShuffleList(id, !shuffle)}
|
||||
onClick={() => doToggleShuffleList(url, id, !shuffle)}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||
import CollectionMenuList from './view';
|
||||
|
||||
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 {
|
||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||
playNextUri,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -13,4 +21,6 @@ export default connect(select, {
|
|||
doCollectionEdit,
|
||||
doOpenModal,
|
||||
doCollectionDelete,
|
||||
doSetPlayingUri,
|
||||
doToggleShuffleList,
|
||||
})(CollectionMenuList);
|
||||
|
|
|
@ -7,19 +7,44 @@ import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
|||
import Icon from 'component/common/icon';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { useHistory } from 'react-router';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
inline?: boolean,
|
||||
doOpenModal: (string, {}) => void,
|
||||
collectionName?: string,
|
||||
collectionId: string,
|
||||
playNextUri: string,
|
||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||
doToggleShuffleList: (string, string, boolean, boolean) => void,
|
||||
};
|
||||
|
||||
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();
|
||||
|
||||
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 (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
|
@ -40,6 +65,18 @@ function CollectionMenuList(props: Props) {
|
|||
{__('View List')}
|
||||
</a>
|
||||
</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
|
||||
className="comment__menu-option"
|
||||
onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}
|
||||
|
|
|
@ -112,18 +112,26 @@ export default function CollectionPage(props: Props) {
|
|||
title={
|
||||
<span>
|
||||
<Icon
|
||||
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
||||
className="icon--margin-right" />
|
||||
icon={
|
||||
(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||
ICONS.STACK
|
||||
}
|
||||
className="icon--margin-right"
|
||||
/>
|
||||
{claim ? claim.value.title || claim.name : collection && collection.name}
|
||||
</span>
|
||||
}
|
||||
titleActions={titleActions}
|
||||
subtitle={subTitle}
|
||||
body={
|
||||
!isBuiltin && (
|
||||
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} />
|
||||
)
|
||||
<CollectionActions
|
||||
uri={uri}
|
||||
collectionId={collectionId}
|
||||
setShowInfo={setShowInfo}
|
||||
showInfo={showInfo}
|
||||
isBuiltin={isBuiltin}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
showInfo &&
|
||||
|
|
|
@ -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) => {
|
||||
if (shuffle) {
|
||||
const state = getState();
|
||||
const urls = makeSelectUrlsForCollectionId(collectionId)(state);
|
||||
|
||||
const newUrls = urls
|
||||
let newUrls = urls
|
||||
.map((item) => ({ item, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.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({
|
||||
type: ACTIONS.TOGGLE_SHUFFLE_LIST,
|
||||
data: { collectionId, newUrls },
|
||||
|
|
|
@ -314,6 +314,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
font-size: var(--font-xsmall);
|
||||
|
||||
.menu__link {
|
||||
color: var(--color-text);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue