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 { 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);

View file

@ -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

View file

@ -10,9 +10,17 @@ 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) => ({
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),
@ -20,11 +28,15 @@ const select = (state, props) => ({
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);

View file

@ -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,12 +43,41 @@ 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 = (
<>
<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 />}
@ -57,11 +92,15 @@ function CollectionActions(props: Props) {
/>
)}
</>
)}
</>
);
const rhsSection = (
<>
{isMyCollection && (
{!isBuiltin &&
(isMyCollection ? (
<>
<Button
title={uri ? __('Update') : __('Publish')}
label={uri ? __('Update') : __('Publish')}
@ -72,8 +111,6 @@ function CollectionActions(props: Props) {
iconSize={18}
disabled={claimIsPending}
/>
)}
{isMyCollection && (
<Button
className={classnames('button--file-action')}
title={__('Delete List')}
@ -83,15 +120,15 @@ function CollectionActions(props: Props) {
description={__('Delete List')}
disabled={claimIsPending}
/>
)}
{!isMyCollection && (
</>
) : (
<Button
title={__('Report content')}
className="button--file-action"
icon={ICONS.REPORT}
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
/>
)}
))}
</>
);

View file

@ -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>
</>

View file

@ -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);

View file

@ -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`)}

View file

@ -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 &&

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) => {
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 },

View file

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