Expanded Playback and List controls (#6921)
* Dont show countdown on Lists * Add Repeat icon * Add Shuffle icon * Add Replay Icon * Add Replay Option to autoplayCountdown * Add Loop Control for Lists * Add Shuffle control for Lists * Improve View List Link and Fetch action * Add Play Button to List page * Add Shuffle Play Option on List Page and Menus * Fix Modal Remove Collection I18n * CSS: Fix Large list titles * Fix List playback on Floating Player * Add Theater Mode to its own class and fix bar text display * Add Play Next VJS component * Add Play Next Button * Add Play Previous VJS Component * Add Play Previous Button * Add Autoplay Next Button * Add separate control for autoplay next in list * Bump redux * Update CHANGELOG.md
This commit is contained in:
parent
061e4ddd55
commit
64cbd4ae8d
41 changed files with 1030 additions and 300 deletions
|
@ -12,6 +12,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Show on content page if a file is part of a playlist already _community pr!_([#6393](https://github.com/lbryio/lbry-desktop/pull/6393))
|
||||
- Add filtering to playlists ([#6905](https://github.com/lbryio/lbry-desktop/pull/6905))
|
||||
- Added direct replying to notifications _community pr!_ ([#6935](https://github.com/lbryio/lbry-desktop/pull/6935))
|
||||
- Added "Replay" option on autoplay countdown ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
- Added "Loop" option on Lists ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
- Added "Shuffle" option on Lists ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
- Added Play Next/Previous buttons (with shortcuts SHIFT+N/SHIFT+P) ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
- Added separate control for autoplay next on video player ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
|
||||
### Changed
|
||||
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))
|
||||
|
@ -23,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Use resolve for OG metadata instead of chainquery _community pr!_ ([#6787](https://github.com/lbryio/lbry-desktop/pull/6787))
|
||||
- Improved clickability of notification links _community pr!_ ([#6711](https://github.com/lbryio/lbry-desktop/pull/6711))
|
||||
- Changing the supported language from Filipino to Tagalog _community pr!_ ([#6951](https://github.com/lbryio/lbry-desktop/pull/6951))
|
||||
- Don't show countdown to next item in list ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
- Changed "View List" popup option to link, so can be opened on a new tab ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
|
||||
### Fixed
|
||||
- App now supports '#' and ':' for claimId separator ([#6496](https://github.com/lbryio/lbry-desktop/pull/6496))
|
||||
|
@ -42,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Fix OG: "Unparsable data structure - Truncated Unicode character" _community pr!_ ([#6839](https://github.com/lbryio/lbry-desktop/pull/6839))
|
||||
- Fix Paid embed warning overlay redirection button now links to odysee _community pr!_ ([#6819](https://github.com/lbryio/lbry-desktop/pull/6819))
|
||||
- Fix comment section redirection to create channel _community pr!_ ([#6557](https://github.com/lbryio/lbry-desktop/pull/6557))
|
||||
- Clicking on the title of a floating player will take you back to the list ([#6921](https://github.com/lbryio/lbry-desktop/pull/6921))
|
||||
|
||||
## [0.51.1] - [2021-06-26]
|
||||
|
||||
|
|
|
@ -152,7 +152,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#dc264ec50ca3208d940521bdac0b61352d913c95",
|
||||
"lbry-redux": "lbryio/lbry-redux#12a2ffc708bed45ba8d5a46620dc3892aaf890f8",
|
||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, SETTINGS, COLLECTIONS_CONSTS, makeSelectNextUrlForCollectionAndUrl } from 'lbry-redux';
|
||||
import { makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { makeSelectIsPlayerFloating, makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doSetPlayingUri, doPlayUri, clearPosition } from 'redux/actions/content';
|
||||
import AutoplayCountdown from './view';
|
||||
import { selectModal } from 'redux/selectors/app';
|
||||
|
||||
|
@ -11,33 +8,9 @@ import { selectModal } from 'redux/selectors/app';
|
|||
AutoplayCountdown does not fetch it's own next content to play, it relies on <RecommendedContent> being rendered.
|
||||
This is dumb but I'm just the guy who noticed -kj
|
||||
*/
|
||||
const select = (state, props) => {
|
||||
const { location } = props;
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||
const select = (state, props) => ({
|
||||
nextRecommendedClaim: makeSelectClaimForUri(props.nextRecommendedUri)(state),
|
||||
modal: selectModal(state),
|
||||
});
|
||||
|
||||
let nextRecommendedUri;
|
||||
if (collectionId) {
|
||||
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, props.uri)(state);
|
||||
} else {
|
||||
nextRecommendedUri = makeSelectNextUnplayedRecommended(props.uri)(state);
|
||||
}
|
||||
|
||||
return {
|
||||
collectionId,
|
||||
nextRecommendedUri,
|
||||
nextRecommendedClaim: makeSelectClaimForUri(nextRecommendedUri)(state),
|
||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
modal: selectModal(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default withRouter(
|
||||
connect(select, {
|
||||
doSetPlayingUri,
|
||||
doPlayUri,
|
||||
clearPosition,
|
||||
})(AutoplayCountdown)
|
||||
);
|
||||
export default withRouter(connect(select, null)(AutoplayCountdown));
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// @flow
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { withRouter } from 'react-router';
|
||||
import debounce from 'util/debounce';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
||||
const CLASSNAME_AUTOPLAY_COUNTDOWN = 'autoplay-countdown';
|
||||
|
||||
|
@ -14,25 +14,19 @@ type Props = {
|
|||
history: { push: (string) => void },
|
||||
nextRecommendedClaim: ?StreamClaim,
|
||||
nextRecommendedUri: string,
|
||||
isFloating: boolean,
|
||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||
doPlayUri: (string) => void,
|
||||
modal: { id: string, modalProps: {} },
|
||||
collectionId?: string,
|
||||
clearPosition: (string) => void,
|
||||
doNavigate: () => void,
|
||||
doReplay: () => void,
|
||||
};
|
||||
|
||||
function AutoplayCountdown(props: Props) {
|
||||
const {
|
||||
nextRecommendedUri,
|
||||
nextRecommendedClaim,
|
||||
doSetPlayingUri,
|
||||
doPlayUri,
|
||||
isFloating,
|
||||
history: { push },
|
||||
modal,
|
||||
collectionId,
|
||||
clearPosition,
|
||||
doNavigate,
|
||||
doReplay,
|
||||
} = props;
|
||||
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
||||
|
||||
|
@ -45,40 +39,6 @@ function AutoplayCountdown(props: Props) {
|
|||
const anyModalPresent = modal !== undefined && modal !== null;
|
||||
const isTimerPaused = timerPaused || anyModalPresent;
|
||||
|
||||
let navigateUrl;
|
||||
if (nextTitle) {
|
||||
navigateUrl = formatLbryUrlForWeb(nextRecommendedUri);
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
}
|
||||
}
|
||||
|
||||
const doPlay = useCallback(
|
||||
(uri) => {
|
||||
if (collectionId) {
|
||||
clearPosition(uri);
|
||||
}
|
||||
doSetPlayingUri({ uri });
|
||||
doPlayUri(uri);
|
||||
},
|
||||
[clearPosition, doSetPlayingUri, doPlayUri]
|
||||
);
|
||||
|
||||
const doNavigate = useCallback(() => {
|
||||
if (!isFloating) {
|
||||
if (navigateUrl) {
|
||||
push(navigateUrl);
|
||||
doPlay(nextRecommendedUri);
|
||||
}
|
||||
} else {
|
||||
if (nextRecommendedUri) {
|
||||
doPlay(nextRecommendedUri);
|
||||
}
|
||||
}
|
||||
}, [navigateUrl, nextRecommendedUri, isFloating, doPlay, push]);
|
||||
|
||||
function shouldPauseAutoplay() {
|
||||
const elm = document.querySelector(`.${CLASSNAME_AUTOPLAY_COUNTDOWN}`);
|
||||
return elm && elm.getBoundingClientRect().top < 0;
|
||||
|
@ -118,7 +78,7 @@ function AutoplayCountdown(props: Props) {
|
|||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [timer, doNavigate, navigateUrl, push, timerCanceled, isTimerPaused, nextRecommendedUri]);
|
||||
}, [timer, doNavigate, push, timerCanceled, isTimerPaused, nextRecommendedUri]);
|
||||
|
||||
if (timerCanceled || !nextRecommendedUri) {
|
||||
return null;
|
||||
|
@ -149,6 +109,15 @@ function AutoplayCountdown(props: Props) {
|
|||
<Button label={__('Cancel')} button="link" onClick={() => setTimerCanceled(true)} />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
label={__('Replay?')}
|
||||
button="link"
|
||||
iconRight={ICONS.REPLAY}
|
||||
onClick={() => {
|
||||
setTimerCanceled(true);
|
||||
doReplay();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
COLLECTIONS_CONSTS,
|
||||
makeSelectEditedCollectionForId,
|
||||
makeSelectClaimIsMine,
|
||||
doFetchItemsInCollection,
|
||||
makeSelectUrlsForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { doChannelMute, doChannelUnmute } from 'redux/actions/blocked';
|
||||
|
@ -28,16 +30,23 @@ 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';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri, false)(state);
|
||||
const collectionId = props.collectionId;
|
||||
const resolvedList = makeSelectUrlsForCollectionId(collectionId)(state);
|
||||
const repostedClaim = claim && claim.reposted_claim;
|
||||
const contentClaim = repostedClaim || claim;
|
||||
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,
|
||||
|
@ -60,10 +69,12 @@ const select = (state, props) => {
|
|||
isSubscribed: makeSelectIsSubscribed(contentChannelUri, true)(state),
|
||||
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
|
||||
isAdmin: selectHasAdminChannel(state),
|
||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, contentPermanentUri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
|
||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(collectionId, contentPermanentUri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
|
||||
editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
resolvedList,
|
||||
playNextUri,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -90,6 +101,9 @@ const perform = (dispatch) => ({
|
|||
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),
|
||||
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);
|
||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
|||
import classnames from 'classnames';
|
||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { generateShareUrl, generateRssUrl, generateLbryContentUrl } from 'util/url';
|
||||
import { generateShareUrl, generateRssUrl, generateLbryContentUrl, formatLbryUrlForWeb } from 'util/url';
|
||||
import { useHistory } from 'react-router';
|
||||
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
|
@ -56,6 +56,11 @@ type Props = {
|
|||
isChannelPage: boolean,
|
||||
editedCollection: Collection,
|
||||
isAuthenticated: boolean,
|
||||
playNextUri: string,
|
||||
resolvedList: boolean,
|
||||
fetchCollectionItems: (string) => void,
|
||||
doSetPlayingUri: (string) => void,
|
||||
doToggleShuffleList: (string) => void,
|
||||
};
|
||||
|
||||
function ClaimMenuList(props: Props) {
|
||||
|
@ -93,7 +98,13 @@ function ClaimMenuList(props: Props) {
|
|||
isChannelPage = false,
|
||||
editedCollection,
|
||||
isAuthenticated,
|
||||
playNextUri,
|
||||
resolvedList,
|
||||
fetchCollectionItems,
|
||||
doSetPlayingUri,
|
||||
doToggleShuffleList,
|
||||
} = props;
|
||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||
const isChannel = !incognitoClaim && !contentSigningChannel;
|
||||
const { channelName } = parseURI(contentChannelUri);
|
||||
|
@ -107,6 +118,27 @@ function ClaimMenuList(props: Props) {
|
|||
: __('Follow');
|
||||
|
||||
const { push, replace } = useHistory();
|
||||
|
||||
const fetchItems = React.useCallback(() => {
|
||||
if (collectionId) {
|
||||
fetchCollectionItems(collectionId);
|
||||
}
|
||||
}, [collectionId, fetchCollectionItems]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (doShuffle && resolvedList) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, [collectionId, doSetPlayingUri, doShuffle, doToggleShuffleList, playNextUri, push, resolvedList]);
|
||||
|
||||
if (!claim) {
|
||||
return null;
|
||||
}
|
||||
|
@ -246,9 +278,21 @@ function ClaimMenuList(props: Props) {
|
|||
{collectionId && isCollectionClaim ? (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||
<div className="menu__link">
|
||||
<a className="menu__link" href={`/$/${PAGES.LIST}/${collectionId}`}>
|
||||
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||
{__('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 && (
|
||||
|
|
|
@ -10,21 +10,50 @@ import {
|
|||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doPlayUri, doSetPlayingUri, doToggleShuffleList, doToggleLoopList } 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) => {
|
||||
let firstItem;
|
||||
const collectionUrls = props.collectionUrls;
|
||||
if (collectionUrls) {
|
||||
// this will help play the first valid claim in a list
|
||||
// in case the first urls have been deleted
|
||||
collectionUrls.map((url) => {
|
||||
const claim = makeSelectClaimForUri(url)(state);
|
||||
if (firstItem === undefined && claim) {
|
||||
firstItem = claim.permanent_url;
|
||||
}
|
||||
});
|
||||
}
|
||||
const collectionId = props.collectionId;
|
||||
const shuffleList = selectListShuffle(state);
|
||||
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||
const playNextUri = shuffle && shuffle[0];
|
||||
const playNextClaim = makeSelectClaimForUri(playNextUri)(state);
|
||||
|
||||
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(collectionId)(state),
|
||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(collectionId)(state)),
|
||||
firstItem,
|
||||
playNextUri,
|
||||
playNextClaim,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doPlayUri: (uri) => dispatch(doPlayUri(uri)),
|
||||
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||
doToggleShuffleList: (collectionId, shuffle) => dispatch(doToggleShuffleList(undefined, collectionId, shuffle, true)),
|
||||
doToggleLoopList: (collectionId, loop) => dispatch(doToggleLoopList(collectionId, loop)),
|
||||
});
|
||||
|
||||
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,14 @@ type Props = {
|
|||
showInfo: boolean,
|
||||
setShowInfo: (boolean) => void,
|
||||
collectionHasEdits: boolean,
|
||||
isBuiltin: boolean,
|
||||
doToggleShuffleList: (string, boolean) => void,
|
||||
doToggleLoopList: (string, boolean) => void,
|
||||
playNextUri: string,
|
||||
playNextClaim: StreamClaim,
|
||||
doPlayUri: (string) => void,
|
||||
doSetPlayingUri: (string) => void,
|
||||
firstItem: string,
|
||||
};
|
||||
|
||||
function CollectionActions(props: Props) {
|
||||
|
@ -37,61 +47,115 @@ function CollectionActions(props: Props) {
|
|||
showInfo,
|
||||
setShowInfo,
|
||||
collectionHasEdits,
|
||||
isBuiltin,
|
||||
doToggleShuffleList,
|
||||
doToggleLoopList,
|
||||
playNextUri,
|
||||
playNextClaim,
|
||||
doPlayUri,
|
||||
doSetPlayingUri,
|
||||
firstItem,
|
||||
} = 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?
|
||||
|
||||
const doPlay = React.useCallback(
|
||||
(uri) => {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
const navigateUrl = formatLbryUrlForWeb(uri) + `?` + collectionParams.toString();
|
||||
push(navigateUrl);
|
||||
doSetPlayingUri(uri);
|
||||
doPlayUri(uri);
|
||||
},
|
||||
[collectionId, push, doSetPlayingUri, doPlayUri]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (playNextClaim && doShuffle) {
|
||||
setDoShuffle(false);
|
||||
doPlay(playNextUri);
|
||||
}
|
||||
}, [doPlay, doShuffle, playNextClaim, playNextUri]);
|
||||
|
||||
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.PLAY}
|
||||
label={__('Play')}
|
||||
title={__('Play')}
|
||||
onClick={() => {
|
||||
doToggleShuffleList(collectionId, false);
|
||||
doToggleLoopList(collectionId, false);
|
||||
doPlay(firstItem);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.SHUFFLE}
|
||||
label={__('Shuffle Play')}
|
||||
title={__('Shuffle Play')}
|
||||
onClick={() => {
|
||||
doToggleShuffleList(collectionId, true);
|
||||
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}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,15 +7,18 @@ import {
|
|||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
} from 'lbry-redux';
|
||||
import {
|
||||
selectPlayingUri,
|
||||
} from 'redux/selectors/content';
|
||||
import { selectPlayingUri, selectListLoop, selectListShuffle } from 'redux/selectors/content';
|
||||
import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content';
|
||||
|
||||
const select = (state, props) => {
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const playingUrl = playingUri && playingUri.uri;
|
||||
const claim = makeSelectClaimForUri(playingUrl)(state);
|
||||
const url = claim && claim.permanent_url;
|
||||
const loopList = selectListLoop(state);
|
||||
const loop = loopList && loopList.collectionId === props.id && loopList.loop;
|
||||
const shuffleList = selectListShuffle(state);
|
||||
const shuffle = shuffleList && shuffleList.collectionId === props.id && shuffleList.newUrls;
|
||||
|
||||
return {
|
||||
url,
|
||||
|
@ -23,7 +26,12 @@ const select = (state, props) => {
|
|||
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
||||
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
||||
isMine: makeSelectClaimIsMine(url)(state),
|
||||
loop,
|
||||
shuffle,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select)(CollectionContent);
|
||||
export default connect(select, {
|
||||
doToggleLoopList,
|
||||
doToggleShuffleList,
|
||||
})(CollectionContent);
|
||||
|
|
|
@ -15,23 +15,52 @@ type Props = {
|
|||
collectionUrls: Array<Claim>,
|
||||
collectionName: string,
|
||||
collection: any,
|
||||
loop: boolean,
|
||||
shuffle: boolean,
|
||||
doToggleLoopList: (string, boolean) => void,
|
||||
doToggleShuffleList: (string, string, boolean) => void,
|
||||
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
||||
};
|
||||
|
||||
export default function CollectionContent(props: Props) {
|
||||
const { collectionUrls, collectionName, id, url } = props;
|
||||
const { collectionUrls, collectionName, id, url, loop, shuffle, doToggleLoopList, doToggleShuffleList } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
isBodyList
|
||||
className="file-page__recommended"
|
||||
className="file-page__recommended-collection"
|
||||
title={
|
||||
<span>
|
||||
<Icon
|
||||
icon={(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
||||
className="icon--margin-right" />
|
||||
{collectionName}
|
||||
</span>
|
||||
<>
|
||||
<span className="file-page__recommended-collection__row">
|
||||
<Icon
|
||||
icon={
|
||||
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||
ICONS.STACK
|
||||
}
|
||||
className="icon--margin-right"
|
||||
/>
|
||||
{collectionName}
|
||||
</span>
|
||||
<span className="file-page__recommended-collection__row">
|
||||
<Button
|
||||
button="alt"
|
||||
title="Loop"
|
||||
icon={ICONS.REPEAT}
|
||||
iconColor={loop && 'blue'}
|
||||
className="button--file-action"
|
||||
onClick={() => doToggleLoopList(id, !loop)}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
title="Shuffle"
|
||||
icon={ICONS.SHUFFLE}
|
||||
iconColor={shuffle && 'blue'}
|
||||
className="button--file-action"
|
||||
onClick={() => doToggleShuffleList(url, id, !shuffle)}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
titleActions={
|
||||
<div className="card__title-actions--link">
|
||||
|
|
|
@ -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
|
||||
|
@ -35,9 +60,21 @@ function CollectionMenuList(props: Props) {
|
|||
{collectionId && collectionName && (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||
<div className="menu__link">
|
||||
<a className="menu__link" href={`/$/${PAGES.LIST}/${collectionId}`}>
|
||||
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||
{__('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
|
||||
|
|
|
@ -2439,4 +2439,28 @@ export const icons = {
|
|||
/>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.PLAY]: buildIcon(<polygon points="5 3 19 12 5 21 5 3" />),
|
||||
[ICONS.REPLAY]: buildIcon(
|
||||
<g>
|
||||
<polyline points="1 4 1 10 7 10" />
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.REPEAT]: buildIcon(
|
||||
<g>
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 0 1 4-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 0 1-4 4H3" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.SHUFFLE]: buildIcon(
|
||||
<g>
|
||||
<polyline points="16 3 21 3 21 8" />
|
||||
<line x1="4" y1="20" x2="21" y2="3" />
|
||||
<polyline points="21 16 21 21 16 21" />
|
||||
<line x1="15" y1="15" x2="21" y2="21" />
|
||||
<line x1="4" y1="4" x2="9" y2="9" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import { makeSelectFileRenderModeForUri, makeSelectFileExtensionForUri } from 'r
|
|||
import FileRender from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const autoplay = props.embedded ? false : makeSelectClientSetting(SETTINGS.AUTOPLAY)(state);
|
||||
const autoplay = props.embedded ? false : makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state);
|
||||
return {
|
||||
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
|
|
|
@ -22,6 +22,7 @@ const select = (state, props) => {
|
|||
const playingUri = selectPlayingUri(state);
|
||||
const primaryUri = selectPrimaryUri(state);
|
||||
const uri = playingUri && playingUri.uri;
|
||||
const collectionId = playingUri && playingUri.collectionId;
|
||||
|
||||
return {
|
||||
uri,
|
||||
|
@ -35,6 +36,7 @@ const select = (state, props) => {
|
|||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
||||
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
||||
collectionId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { onFullscreenChange } from 'util/full-screen';
|
|||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import debounce from 'util/debounce';
|
||||
import { useHistory } from 'react-router';
|
||||
import { isURIEqual } from 'lbry-redux';
|
||||
import { isURIEqual, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
||||
|
@ -34,6 +34,7 @@ type Props = {
|
|||
primaryUri: ?string,
|
||||
videoTheaterMode: boolean,
|
||||
doFetchRecommendedContent: (string, boolean) => void,
|
||||
collectionId: string,
|
||||
};
|
||||
|
||||
export default function FileRenderFloating(props: Props) {
|
||||
|
@ -51,6 +52,7 @@ export default function FileRenderFloating(props: Props) {
|
|||
primaryUri,
|
||||
videoTheaterMode,
|
||||
doFetchRecommendedContent,
|
||||
collectionId,
|
||||
} = props;
|
||||
const {
|
||||
location: { pathname },
|
||||
|
@ -69,6 +71,13 @@ export default function FileRenderFloating(props: Props) {
|
|||
y: 0,
|
||||
});
|
||||
|
||||
let navigateUrl;
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = uri + `?` + collectionParams.toString();
|
||||
}
|
||||
|
||||
const playingUriSource = playingUri && playingUri.source;
|
||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||
|
@ -303,7 +312,12 @@ export default function FileRenderFloating(props: Props) {
|
|||
{isFloating && (
|
||||
<div className="draggable content__info">
|
||||
<div className="claim-preview__title" title={title || uri}>
|
||||
<Button label={title || uri} navigate={uri} button="link" className="content__floating-link" />
|
||||
<Button
|
||||
label={title || uri}
|
||||
navigate={navigateUrl || uri}
|
||||
button="link"
|
||||
className="content__floating-link"
|
||||
/>
|
||||
</div>
|
||||
<UriIndicator link uri={uri} />
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
makeSelectStreamingUrlForUri,
|
||||
makeSelectClaimWasPurchased,
|
||||
SETTINGS,
|
||||
COLLECTIONS_CONSTS,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
@ -22,27 +23,34 @@ import {
|
|||
import FileRenderInitiator from './view';
|
||||
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||
|
||||
return {
|
||||
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state),
|
||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
collectionId,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
play: (uri) => {
|
||||
play: (uri, collectionId) => {
|
||||
dispatch(doSetPrimaryUri(uri));
|
||||
dispatch(doSetPlayingUri({ uri }));
|
||||
dispatch(doSetPlayingUri({ uri, collectionId }));
|
||||
dispatch(doPlayUri(uri, undefined, undefined, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import FileRenderPlaceholder from 'static/img/fileRenderPlaceholder.png';
|
|||
const SPACE_BAR_KEYCODE = 32;
|
||||
|
||||
type Props = {
|
||||
play: (string) => void,
|
||||
play: (string, string) => void,
|
||||
isLoading: boolean,
|
||||
isPlaying: boolean,
|
||||
fileInfo: FileListItem,
|
||||
|
@ -36,6 +36,7 @@ type Props = {
|
|||
claimWasPurchased: boolean,
|
||||
authenticated: boolean,
|
||||
videoTheaterMode: boolean,
|
||||
collectionId: string,
|
||||
};
|
||||
|
||||
export default function FileRenderInitiator(props: Props) {
|
||||
|
@ -55,6 +56,7 @@ export default function FileRenderInitiator(props: Props) {
|
|||
claimWasPurchased,
|
||||
authenticated,
|
||||
videoTheaterMode,
|
||||
collectionId,
|
||||
} = props;
|
||||
|
||||
// force autoplay if a timestamp is present
|
||||
|
@ -109,9 +111,9 @@ export default function FileRenderInitiator(props: Props) {
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
play(uri);
|
||||
play(uri, collectionId);
|
||||
},
|
||||
[play, uri]
|
||||
[play, uri, collectionId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -11,7 +11,8 @@ import SettingContent from './view';
|
|||
const select = (state) => ({
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
autoplayMedia: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state),
|
||||
autoplayNext: makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state),
|
||||
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||
showNsfw: selectShowMatureContent(state),
|
||||
myChannelUrls: selectMyChannelUrls(state),
|
||||
|
|
|
@ -22,7 +22,8 @@ type Props = {
|
|||
// --- select ---
|
||||
isAuthenticated: boolean,
|
||||
floatingPlayer: boolean,
|
||||
autoplay: boolean,
|
||||
autoplayMedia: boolean,
|
||||
autoplayNext: boolean,
|
||||
hideReposts: ?boolean,
|
||||
showNsfw: boolean,
|
||||
myChannelUrls: ?Array<string>,
|
||||
|
@ -39,7 +40,8 @@ export default function SettingContent(props: Props) {
|
|||
const {
|
||||
isAuthenticated,
|
||||
floatingPlayer,
|
||||
autoplay,
|
||||
autoplayMedia,
|
||||
autoplayNext,
|
||||
hideReposts,
|
||||
showNsfw,
|
||||
myChannelUrls,
|
||||
|
@ -73,12 +75,21 @@ export default function SettingContent(props: Props) {
|
|||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow title={__('Autoplay media files')} subtitle={__(HELP.AUTOPLAY)}>
|
||||
<SettingsRow title={__('Autoplay media files')} subtitle={__(HELP.AUTOPLAY_MEDIA)}>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="autoplay"
|
||||
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
|
||||
checked={autoplay}
|
||||
name="autoplay media"
|
||||
onChange={() => setClientSetting(SETTINGS.AUTOPLAY_MEDIA, !autoplayMedia)}
|
||||
checked={autoplayMedia}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
<SettingsRow title={__('Autoplay next recommended content')} subtitle={__(HELP.AUTOPLAY_NEXT)}>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="autoplay next"
|
||||
onChange={() => setClientSetting(SETTINGS.AUTOPLAY_NEXT, !autoplayNext)}
|
||||
checked={autoplayNext}
|
||||
/>
|
||||
</SettingsRow>
|
||||
|
||||
|
@ -208,7 +219,8 @@ export default function SettingContent(props: Props) {
|
|||
// prettier-ignore
|
||||
const HELP = {
|
||||
FLOATING_PLAYER: 'Keep content playing in the corner when navigating to a different page.',
|
||||
AUTOPLAY: 'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.',
|
||||
AUTOPLAY_MEDIA: 'Autoplay video and audio files when navigating to a file.',
|
||||
AUTOPLAY_NEXT: 'Autoplay video and audio files as the next related item when a file finishes playing.',
|
||||
HIDE_REPOSTS: 'You will not see reposts by people you follow or receive email notifying about them.',
|
||||
SHOW_MATURE: 'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. ',
|
||||
MAX_PURCHASE_PRICE: 'This will prevent you from purchasing any content over a certain cost, as a safety measure.',
|
||||
|
|
|
@ -1,38 +1,69 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, makeSelectFileInfoForUri, makeSelectThumbnailForUri, SETTINGS } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
SETTINGS,
|
||||
COLLECTIONS_CONSTS,
|
||||
makeSelectNextUrlForCollectionAndUrl,
|
||||
makeSelectPreviousUrlForCollectionAndUrl,
|
||||
} from 'lbry-redux';
|
||||
import { doChangeVolume, doChangeMute, doAnalyticsView, doAnalyticsBuffer } from 'redux/actions/app';
|
||||
import { selectVolume, selectMute } from 'redux/selectors/app';
|
||||
import { savePosition, clearPosition } from 'redux/actions/content';
|
||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
||||
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||
import {
|
||||
makeSelectContentPositionForUri,
|
||||
makeSelectIsPlayerFloating,
|
||||
makeSelectNextUnplayedRecommended,
|
||||
selectPlayingUri,
|
||||
} from 'redux/selectors/content';
|
||||
import VideoViewer from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { selectDaemonSettings, makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||
import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
|
||||
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const autoplay = urlParams.get('autoplay');
|
||||
const uri = props.uri;
|
||||
// TODO: eventually this should be received from DB and not local state (https://github.com/lbryio/lbry-desktop/issues/6796)
|
||||
const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(props.uri)(state);
|
||||
const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(uri)(state);
|
||||
const userId = selectUser(state) && selectUser(state).id;
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || (playingUri && playingUri.collectionId);
|
||||
|
||||
let nextRecommendedUri;
|
||||
let previousListUri;
|
||||
if (collectionId) {
|
||||
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||
previousListUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state);
|
||||
} else {
|
||||
nextRecommendedUri = makeSelectNextUnplayedRecommended(uri)(state);
|
||||
}
|
||||
|
||||
return {
|
||||
autoplayIfEmbedded: Boolean(autoplay),
|
||||
autoplaySetting: Boolean(makeSelectClientSetting(SETTINGS.AUTOPLAY)(state)),
|
||||
autoplayMedia: Boolean(makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state)),
|
||||
autoplayNext: Boolean(makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state)),
|
||||
volume: selectVolume(state),
|
||||
muted: selectMute(state),
|
||||
videoPlaybackRate: makeSelectClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE)(state),
|
||||
position: position,
|
||||
hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)),
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
hasFileInfo: Boolean(makeSelectFileInfoForUri(uri)(state)),
|
||||
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
||||
claim: makeSelectClaimForUri(uri)(state),
|
||||
homepageData: selectHomepageData(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
userId: userId,
|
||||
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
|
||||
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||
collectionId,
|
||||
nextRecommendedUri,
|
||||
previousListUri,
|
||||
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -45,7 +76,10 @@ const perform = (dispatch) => ({
|
|||
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
|
||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
|
||||
toggleAutoplayNext: () => dispatch(toggleAutoplayNext()),
|
||||
setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
|
||||
doPlayUri: (uri) => dispatch(doPlayUri(uri)),
|
||||
doSetPlayingUri: (uri, collectionId) => dispatch(doSetPlayingUri({ uri, collectionId })),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(VideoViewer));
|
||||
|
|
33
ui/component/viewers/videoViewer/internal/autoplay-next.js
Normal file
33
ui/component/viewers/videoViewer/internal/autoplay-next.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
// @flow
|
||||
import type { Player } from './videojs';
|
||||
import videojs from 'video.js';
|
||||
|
||||
class AutoplayNextButton extends videojs.getComponent('Button') {
|
||||
constructor(player, options = {}, autoplayNext) {
|
||||
super(player, options, autoplayNext);
|
||||
const title = autoplayNext ? 'Autoplay Next On' : 'Autoplay Next Off';
|
||||
|
||||
this.controlText(title);
|
||||
this.setAttribute('aria-label', title);
|
||||
this.addClass('vjs-button--autoplay-next');
|
||||
this.setAttribute('aria-checked', autoplayNext);
|
||||
}
|
||||
}
|
||||
|
||||
export function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, autoplayNext: boolean) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
const autoplayButton = new AutoplayNextButton(
|
||||
player,
|
||||
{
|
||||
name: 'AutoplayNextButton',
|
||||
text: 'Autoplay Next',
|
||||
clickHandler: () => {
|
||||
toggleAutoplayNext();
|
||||
},
|
||||
},
|
||||
autoplayNext
|
||||
);
|
||||
|
||||
controlBar.addChild(autoplayButton);
|
||||
}
|
25
ui/component/viewers/videoViewer/internal/play-next.js
Normal file
25
ui/component/viewers/videoViewer/internal/play-next.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import type { Player } from './videojs';
|
||||
import videojs from 'video.js';
|
||||
|
||||
class PlayNextButton extends videojs.getComponent('Button') {
|
||||
constructor(player, options = {}) {
|
||||
super(player, options);
|
||||
this.addClass('vjs-button--play-next');
|
||||
this.controlText('Play Next');
|
||||
}
|
||||
}
|
||||
|
||||
export function addPlayNextButton(player: Player, playNextURI: () => void) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
const playNext = new PlayNextButton(player, {
|
||||
name: 'PlayNextButton',
|
||||
text: 'Play Next',
|
||||
clickHandler: () => {
|
||||
playNextURI();
|
||||
},
|
||||
});
|
||||
|
||||
controlBar.addChild(playNext, {}, 1);
|
||||
}
|
25
ui/component/viewers/videoViewer/internal/play-previous.js
Normal file
25
ui/component/viewers/videoViewer/internal/play-previous.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import type { Player } from './videojs';
|
||||
import videojs from 'video.js';
|
||||
|
||||
class PlayPreviousButton extends videojs.getComponent('Button') {
|
||||
constructor(player, options = {}) {
|
||||
super(player, options);
|
||||
this.addClass('vjs-button--play-previous');
|
||||
this.controlText('Play Previous');
|
||||
}
|
||||
}
|
||||
|
||||
export function addPlayPreviousButton(player: Player, playPreviousURI: () => void) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
const playPrevious = new PlayPreviousButton(player, {
|
||||
name: 'PlayPreviousButton',
|
||||
text: 'Play Previous',
|
||||
clickHandler: () => {
|
||||
playPreviousURI();
|
||||
},
|
||||
});
|
||||
|
||||
controlBar.addChild(playPrevious, {}, 0);
|
||||
}
|
|
@ -1,16 +1,25 @@
|
|||
// @flow
|
||||
import type { Player } from './videojs';
|
||||
import videojs from 'video.js';
|
||||
|
||||
class TheaterModeButton extends videojs.getComponent('Button') {
|
||||
constructor(player, options = {}) {
|
||||
super(player, options);
|
||||
this.addClass('vjs-button--theater-mode');
|
||||
this.controlText('Theater Mode');
|
||||
}
|
||||
}
|
||||
|
||||
export function addTheaterModeButton(player: Player, toggleVideoTheaterMode: () => void) {
|
||||
var myButton = player.controlBar.addChild('button', {
|
||||
text: __('Theater mode'),
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
const theaterMode = new TheaterModeButton(player, {
|
||||
name: 'TheaterModeButton',
|
||||
text: 'Theater mode',
|
||||
clickHandler: () => {
|
||||
toggleVideoTheaterMode();
|
||||
},
|
||||
});
|
||||
|
||||
// $FlowFixMe
|
||||
myButton.addClass('vjs-button--theater-mode');
|
||||
// $FlowFixMe
|
||||
myButton.setAttribute('title', __('Theater mode'));
|
||||
controlBar.addChild(theaterMode);
|
||||
}
|
||||
|
|
|
@ -49,16 +49,21 @@ type Props = {
|
|||
source: string,
|
||||
sourceType: string,
|
||||
poster: ?string,
|
||||
onPlayerReady: (Player) => void,
|
||||
onPlayerReady: (Player, any) => void,
|
||||
isAudio: boolean,
|
||||
startMuted: boolean,
|
||||
autoplay: boolean,
|
||||
autoplaySetting: boolean,
|
||||
toggleVideoTheaterMode: () => void,
|
||||
adUrl: ?string,
|
||||
claimId: ?string,
|
||||
userId: ?number,
|
||||
// allowPreRoll: ?boolean,
|
||||
shareTelemetry: boolean,
|
||||
replay: boolean,
|
||||
videoTheaterMode: boolean,
|
||||
playNext: () => void,
|
||||
playPrevious: () => void,
|
||||
};
|
||||
|
||||
// type VideoJSOptions = {
|
||||
|
@ -103,6 +108,9 @@ const SMALL_J_KEYCODE = 74;
|
|||
const SMALL_K_KEYCODE = 75;
|
||||
const SMALL_L_KEYCODE = 76;
|
||||
|
||||
const P_KEYCODE = 80;
|
||||
const N_KEYCODE = 78;
|
||||
|
||||
const FULLSCREEN_KEYCODE = SMALL_F_KEYCODE;
|
||||
const MUTE_KEYCODE = SMALL_M_KEYCODE;
|
||||
const THEATER_MODE_KEYCODE = SMALL_T_KEYCODE;
|
||||
|
@ -185,6 +193,7 @@ properties for this component should be kept to ONLY those that if changed shoul
|
|||
export default React.memo<Props>(function VideoJs(props: Props) {
|
||||
const {
|
||||
autoplay,
|
||||
autoplaySetting,
|
||||
startMuted,
|
||||
source,
|
||||
sourceType,
|
||||
|
@ -197,6 +206,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
userId,
|
||||
// allowPreRoll,
|
||||
shareTelemetry,
|
||||
replay,
|
||||
videoTheaterMode,
|
||||
playNext,
|
||||
playPrevious,
|
||||
} = props;
|
||||
|
||||
const [reload, setReload] = useState('initial');
|
||||
|
@ -298,43 +311,41 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
if (player) {
|
||||
try {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
switch (e.type) {
|
||||
case 'play':
|
||||
controlBar.getChild('PlayToggle').controlText(__('Pause (space)'));
|
||||
break;
|
||||
case 'pause':
|
||||
controlBar.getChild('PlayToggle').controlText(__('Play (space)'));
|
||||
break;
|
||||
case 'volumechange':
|
||||
controlBar
|
||||
.getChild('VolumePanel')
|
||||
.getChild('MuteToggle')
|
||||
.controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)'));
|
||||
break;
|
||||
case 'fullscreenchange':
|
||||
controlBar
|
||||
.getChild('FullscreenToggle')
|
||||
.controlText(player.isFullscreen() ? __('Exit Fullscreen (f)') : __('Fullscreen (f)'));
|
||||
break;
|
||||
case 'loadstart':
|
||||
// --- Do everything ---
|
||||
controlBar.getChild('PlaybackRateMenuButton').controlText(__('Playback Rate (<, >)'));
|
||||
controlBar.getChild('QualityButton').controlText(__('Quality'));
|
||||
resolveCtrlText({ type: 'play' });
|
||||
resolveCtrlText({ type: 'pause' });
|
||||
resolveCtrlText({ type: 'volumechange' });
|
||||
resolveCtrlText({ type: 'fullscreenchange' });
|
||||
// (1) The 'Theater mode' button should probably be changed to a class
|
||||
// so that we can use getChild() with a specific name. There might be
|
||||
// clashes if we add a new button in the future.
|
||||
// (2) We'll have to get 'makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)'
|
||||
// as a prop here so we can say "Theater mode|Default mode" instead of
|
||||
// "Toggle Theater mode".
|
||||
controlBar.getChild('Button').controlText(__('Toggle Theater mode (t)'));
|
||||
break;
|
||||
default:
|
||||
if (isDev) throw Error('Unexpected: ' + e.type);
|
||||
break;
|
||||
switch (e.type) {
|
||||
case 'play':
|
||||
controlBar.getChild('PlayToggle').controlText(__('Pause (space)'));
|
||||
break;
|
||||
case 'pause':
|
||||
controlBar.getChild('PlayToggle').controlText(__('Play (space)'));
|
||||
break;
|
||||
case 'volumechange':
|
||||
controlBar
|
||||
.getChild('VolumePanel')
|
||||
.getChild('MuteToggle')
|
||||
.controlText(player.muted() || player.volume() === 0 ? __('Unmute (m)') : __('Mute (m)'));
|
||||
break;
|
||||
case 'fullscreenchange':
|
||||
controlBar
|
||||
.getChild('FullscreenToggle')
|
||||
.controlText(player.isFullscreen() ? __('Exit Fullscreen (f)') : __('Fullscreen (f)'));
|
||||
break;
|
||||
case 'loadstart':
|
||||
// --- Do everything ---
|
||||
controlBar.getChild('PlaybackRateMenuButton').controlText(__('Playback Rate (<, >)'));
|
||||
controlBar.getChild('QualityButton').controlText(__('Quality'));
|
||||
resolveCtrlText({ type: 'play' });
|
||||
resolveCtrlText({ type: 'pause' });
|
||||
resolveCtrlText({ type: 'volumechange' });
|
||||
resolveCtrlText({ type: 'fullscreenchange' });
|
||||
controlBar
|
||||
.getChild('TheaterModeButton')
|
||||
.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
||||
controlBar.getChild('PlayNextButton').controlText(__('Play Next (SHIFT+N)'));
|
||||
controlBar.getChild('PlayPreviousButton').controlText(__('Play Previous (SHIFT+P)'));
|
||||
break;
|
||||
default:
|
||||
if (isDev) throw Error('Unexpected: ' + e.type);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
// Just fail silently. It'll just be due to hidden ctrls, and if it is
|
||||
|
@ -392,6 +403,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
if (e.altKey || e.ctrlKey || e.metaKey || !e.shiftKey) return;
|
||||
if (e.keyCode === PERIOD_KEYCODE) changePlaybackSpeed(true);
|
||||
if (e.keyCode === COMMA_KEYCODE) changePlaybackSpeed(false);
|
||||
if (e.keyCode === N_KEYCODE) playNext();
|
||||
if (e.keyCode === P_KEYCODE) playPrevious();
|
||||
}
|
||||
|
||||
function handleSingleKeyActions(e: KeyboardEvent) {
|
||||
|
@ -585,7 +598,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
player.children_[0].setAttribute('playsinline', '');
|
||||
|
||||
// I think this is a callback function
|
||||
onPlayerReady(player);
|
||||
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||
onPlayerReady(player, videoNode);
|
||||
});
|
||||
|
||||
// pre-roll ads
|
||||
|
@ -606,7 +620,41 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
return vjs;
|
||||
}
|
||||
|
||||
// This lifecycle hook is only called once (on mount), or when `isAudio` changes.
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (replay && player) {
|
||||
player.play();
|
||||
}
|
||||
}, [replay]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (player) {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
controlBar
|
||||
.getChild('TheaterModeButton')
|
||||
.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
||||
}
|
||||
}, [videoTheaterMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (player) {
|
||||
const touchOverlay = player.getChild('TouchOverlay');
|
||||
const controlBar = player.getChild('controlBar') || touchOverlay.getChild('controlBar');
|
||||
const autoplayButton = controlBar.getChild('AutoplayNextButton');
|
||||
|
||||
if (autoplayButton) {
|
||||
const title = autoplaySetting ? 'Autoplay Next On' : 'Autoplay Next Off';
|
||||
|
||||
autoplayButton.controlText(title);
|
||||
autoplayButton.setAttribute('aria-label', title);
|
||||
autoplayButton.setAttribute('aria-checked', autoplaySetting);
|
||||
}
|
||||
}
|
||||
}, [autoplaySetting]);
|
||||
|
||||
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
|
||||
useEffect(() => {
|
||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||
|
||||
|
@ -635,7 +683,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
window.player = undefined;
|
||||
}
|
||||
};
|
||||
}, [isAudio]);
|
||||
}, [isAudio, source]);
|
||||
|
||||
// Update video player and reload when source URL changes
|
||||
useEffect(() => {
|
||||
|
|
|
@ -16,12 +16,17 @@ import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
|
|||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import { addTheaterModeButton } from './internal/theater-mode';
|
||||
import { addAutoplayNextButton } from './internal/autoplay-next';
|
||||
import { addPlayNextButton } from './internal/play-next';
|
||||
import { addPlayPreviousButton } from './internal/play-previous';
|
||||
import { useGetAds } from 'effects/use-get-ads';
|
||||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getAllIds } from 'util/buildHomepage';
|
||||
import type { HomepageCat } from 'util/buildHomepage';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||
const PLAY_TIMEOUT_LIMIT = 2000;
|
||||
|
@ -38,7 +43,8 @@ type Props = {
|
|||
videoPlaybackRate: number,
|
||||
volume: number,
|
||||
uri: string,
|
||||
autoplaySetting: boolean,
|
||||
autoplayMedia: boolean,
|
||||
autoplayNext: boolean,
|
||||
autoplayIfEmbedded: boolean,
|
||||
desktopPlayStartTime?: number,
|
||||
doAnalyticsView: (string, number) => Promise<any>,
|
||||
|
@ -47,11 +53,19 @@ type Props = {
|
|||
savePosition: (string, number) => void,
|
||||
clearPosition: (string) => void,
|
||||
toggleVideoTheaterMode: () => void,
|
||||
toggleAutoplayNext: () => void,
|
||||
setVideoPlaybackRate: (number) => void,
|
||||
authenticated: boolean,
|
||||
userId: number,
|
||||
homepageData?: { [string]: HomepageCat },
|
||||
shareTelemetry: boolean,
|
||||
isFloating: boolean,
|
||||
doPlayUri: (string) => void,
|
||||
doSetPlayingUri: (string, string) => void,
|
||||
collectionId: string,
|
||||
nextRecommendedUri: string,
|
||||
previousListUri: string,
|
||||
videoTheaterMode: boolean,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -72,7 +86,8 @@ function VideoViewer(props: Props) {
|
|||
uri,
|
||||
muted,
|
||||
volume,
|
||||
autoplaySetting,
|
||||
autoplayMedia,
|
||||
autoplayNext,
|
||||
autoplayIfEmbedded,
|
||||
doAnalyticsView,
|
||||
doAnalyticsBuffer,
|
||||
|
@ -81,22 +96,34 @@ function VideoViewer(props: Props) {
|
|||
clearPosition,
|
||||
desktopPlayStartTime,
|
||||
toggleVideoTheaterMode,
|
||||
toggleAutoplayNext,
|
||||
setVideoPlaybackRate,
|
||||
homepageData,
|
||||
authenticated,
|
||||
userId,
|
||||
shareTelemetry,
|
||||
isFloating,
|
||||
doPlayUri,
|
||||
doSetPlayingUri,
|
||||
collectionId,
|
||||
nextRecommendedUri,
|
||||
previousListUri,
|
||||
videoTheaterMode,
|
||||
} = props;
|
||||
|
||||
const permanentUrl = claim && claim.permanent_url;
|
||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||
const claimId = claim && claim.claim_id;
|
||||
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||
const isAudio = contentType.includes('audio');
|
||||
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
const [doNavigate, setDoNavigate] = useState(false);
|
||||
const [playNextUrl, setPlayNextUrl] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [ended, setEnded] = useState(false);
|
||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
||||
const vjsCallbackDataRef: any = React.useRef();
|
||||
|
@ -108,6 +135,8 @@ function VideoViewer(props: Props) {
|
|||
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
|
||||
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [replay, setReplay] = useState(false);
|
||||
const [videoNode, setVideoNode] = useState();
|
||||
|
||||
// force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true)
|
||||
useEffect(() => {
|
||||
|
@ -152,28 +181,92 @@ function VideoViewer(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
const onEnded = React.useCallback(() => {
|
||||
analytics.videoIsPlaying(false);
|
||||
const doPlay = useCallback(
|
||||
(playUri) => {
|
||||
let navigateUrl = formatLbryUrlForWeb(playUri);
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||
clearPosition(playUri);
|
||||
}
|
||||
if (!isFloating) {
|
||||
push(navigateUrl);
|
||||
}
|
||||
doPlayUri(playUri);
|
||||
doSetPlayingUri(playUri, collectionId);
|
||||
setDoNavigate(false);
|
||||
},
|
||||
[clearPosition, collectionId, doPlayUri, doSetPlayingUri, isFloating, push]
|
||||
);
|
||||
|
||||
if (adUrl) {
|
||||
setAdUrl(null);
|
||||
return;
|
||||
useEffect(() => {
|
||||
if (doNavigate) {
|
||||
if (playNextUrl) {
|
||||
if (permanentUrl !== nextRecommendedUri) {
|
||||
if (nextRecommendedUri) doPlay(nextRecommendedUri);
|
||||
} else {
|
||||
setReplay(true);
|
||||
}
|
||||
} else {
|
||||
if (videoNode) {
|
||||
const currentTime = videoNode.currentTime;
|
||||
|
||||
if (currentTime <= 5) {
|
||||
if (previousListUri && permanentUrl !== previousListUri) doPlay(previousListUri);
|
||||
} else {
|
||||
videoNode.currentTime = 0;
|
||||
}
|
||||
setDoNavigate(false);
|
||||
}
|
||||
}
|
||||
if (!ended) setDoNavigate(false);
|
||||
setEnded(false);
|
||||
setPlayNextUrl(true);
|
||||
}
|
||||
}, [doNavigate, doPlay, ended, nextRecommendedUri, permanentUrl, playNextUrl, previousListUri, videoNode]);
|
||||
|
||||
if (embedded) {
|
||||
setIsEndededEmbed(true);
|
||||
} else if (autoplaySetting) {
|
||||
setShowAutoplayCountdown(true);
|
||||
React.useEffect(() => {
|
||||
if (ended) {
|
||||
analytics.videoIsPlaying(false);
|
||||
|
||||
if (adUrl) {
|
||||
setAdUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (embedded) {
|
||||
setIsEndededEmbed(true);
|
||||
} else if (!collectionId && autoplayNext) {
|
||||
setShowAutoplayCountdown(true);
|
||||
} else if (collectionId) {
|
||||
setDoNavigate(true);
|
||||
}
|
||||
|
||||
clearPosition(uri);
|
||||
}
|
||||
|
||||
clearPosition(uri);
|
||||
}, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl, clearPosition, uri]);
|
||||
}, [
|
||||
embedded,
|
||||
setIsEndededEmbed,
|
||||
autoplayMedia,
|
||||
setShowAutoplayCountdown,
|
||||
adUrl,
|
||||
setAdUrl,
|
||||
clearPosition,
|
||||
uri,
|
||||
ended,
|
||||
collectionId,
|
||||
autoplayNext,
|
||||
]);
|
||||
|
||||
function onPlay(player) {
|
||||
setEnded(false);
|
||||
setIsLoading(false);
|
||||
setIsPlaying(true);
|
||||
setShowAutoplayCountdown(false);
|
||||
setIsEndededEmbed(false);
|
||||
setReplay(false);
|
||||
setDoNavigate(false);
|
||||
analytics.videoIsPlaying(true, player);
|
||||
}
|
||||
|
||||
|
@ -203,12 +296,29 @@ function VideoViewer(props: Props) {
|
|||
playerReadyDependencyList.push(desktopPlayStartTime);
|
||||
}
|
||||
|
||||
const onPlayerReady = useCallback((player: Player) => {
|
||||
const doPlayNext = () => {
|
||||
setPlayNextUrl(true);
|
||||
setDoNavigate(true);
|
||||
};
|
||||
|
||||
const doPlayPrevious = () => {
|
||||
setPlayNextUrl(false);
|
||||
setDoNavigate(true);
|
||||
};
|
||||
|
||||
const onPlayerReady = useCallback((player: Player, videoNode: any) => {
|
||||
if (!embedded) {
|
||||
setVideoNode(videoNode);
|
||||
player.muted(muted);
|
||||
player.volume(volume);
|
||||
player.playbackRate(videoPlaybackRate);
|
||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||
if (collectionId) {
|
||||
addPlayNextButton(player, doPlayNext);
|
||||
addPlayPreviousButton(player, doPlayPrevious);
|
||||
} else {
|
||||
addAutoplayNextButton(player, toggleAutoplayNext, autoplayNext);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldPlay = !embedded || autoplayIfEmbedded;
|
||||
|
@ -244,7 +354,7 @@ function VideoViewer(props: Props) {
|
|||
|
||||
// first play tracking, used for initializing the watchman api
|
||||
player.on('tracking:firstplay', doTrackingFirstPlay);
|
||||
player.on('ended', onEnded);
|
||||
player.on('ended', () => setEnded(true));
|
||||
player.on('play', onPlay);
|
||||
player.on('pause', (event) => onPause(event, player));
|
||||
player.on('dispose', (event) => onDispose(event, player));
|
||||
|
@ -285,7 +395,14 @@ function VideoViewer(props: Props) {
|
|||
})}
|
||||
onContextMenu={stopContextMenu}
|
||||
>
|
||||
{showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
|
||||
{showAutoplayCountdown && (
|
||||
<AutoplayCountdown
|
||||
uri={uri}
|
||||
nextRecommendedUri={nextRecommendedUri}
|
||||
doNavigate={() => setDoNavigate(true)}
|
||||
doReplay={() => setReplay(true)}
|
||||
/>
|
||||
)}
|
||||
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||
|
@ -332,10 +449,15 @@ function VideoViewer(props: Props) {
|
|||
startMuted={autoplayIfEmbedded}
|
||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||
autoplay={!embedded || autoplayIfEmbedded}
|
||||
autoplaySetting={autoplayNext}
|
||||
claimId={claimId}
|
||||
userId={userId}
|
||||
allowPreRoll={!embedded && !authenticated}
|
||||
shareTelemetry={shareTelemetry}
|
||||
replay={replay}
|
||||
videoTheaterMode={videoTheaterMode}
|
||||
playNext={doPlayNext}
|
||||
playPrevious={doPlayPrevious}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -100,6 +100,8 @@ export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
|||
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
||||
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
||||
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
||||
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';
|
||||
export const TOGGLE_SHUFFLE_LIST = 'TOGGLE_SHUFFLE_LIST';
|
||||
|
||||
// Files
|
||||
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
|
||||
|
|
|
@ -170,3 +170,6 @@ export const STAR = 'star';
|
|||
export const MUSIC = 'MusicCategory';
|
||||
export const BADGE_MOD = 'BadgeMod';
|
||||
export const BADGE_STREAMER = 'BadgeStreamer';
|
||||
export const REPLAY = 'Replay';
|
||||
export const REPEAT = 'Repeat';
|
||||
export const SHUFFLE = 'Shuffle';
|
||||
|
|
|
@ -14,7 +14,8 @@ export const THEME = 'theme';
|
|||
export const THEMES = 'themes';
|
||||
export const AUTOMATIC_DARK_MODE_ENABLED = 'automatic_dark_mode_enabled';
|
||||
export const CLOCK_24H = 'clock_24h';
|
||||
export const AUTOPLAY = 'autoplay';
|
||||
export const AUTOPLAY_MEDIA = 'autoplay';
|
||||
export const AUTOPLAY_NEXT = 'autoplay';
|
||||
export const OS_NOTIFICATIONS_ENABLED = 'os_notifications_enabled';
|
||||
export const AUTO_DOWNLOAD = 'auto_download';
|
||||
export const AUTO_LAUNCH = 'auto_launch';
|
||||
|
|
|
@ -36,7 +36,7 @@ function ModalRemoveCollection(props: Props) {
|
|||
</React.Fragment>
|
||||
) : (
|
||||
<I18nMessage tokens={{ title: <cite>{uri && title ? `"${title}"` : `"${collectionName}"`}</cite> }}>
|
||||
Are you sure you'd like to remove "%title%"?
|
||||
Are you sure you'd like to remove %title%?
|
||||
</I18nMessage>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -70,13 +70,12 @@ export default function CollectionPage(props: Props) {
|
|||
|
||||
const urlsReady =
|
||||
collectionUrls && (totalItems === undefined || (totalItems && totalItems === collectionUrls.length));
|
||||
const shouldFetch = !claim && !collection;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (collectionId && !urlsReady && !didTryResolve && shouldFetch) {
|
||||
if (collectionId && !urlsReady && !didTryResolve && !collection) {
|
||||
fetchCollectionItems(collectionId, () => setDidTryResolve(true));
|
||||
}
|
||||
}, [collectionId, urlsReady, didTryResolve, shouldFetch, setDidTryResolve, fetchCollectionItems]);
|
||||
}, [collectionId, urlsReady, didTryResolve, setDidTryResolve, fetchCollectionItems, collection]);
|
||||
|
||||
const pending = (
|
||||
<div className="help card__title--help">
|
||||
|
@ -112,18 +111,27 @@ 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}
|
||||
collectionUrls={collectionUrls}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
showInfo &&
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimWasPurchased,
|
||||
doToast,
|
||||
makeSelectUrlsForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
|
@ -110,16 +112,18 @@ export function doSetPlayingUri({
|
|||
source,
|
||||
pathname,
|
||||
commentId,
|
||||
collectionId,
|
||||
}: {
|
||||
uri: ?string,
|
||||
source?: string,
|
||||
commentId?: string,
|
||||
pathname: string,
|
||||
collectionId: string,
|
||||
}) {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_PLAYING_URI,
|
||||
data: { uri, source, pathname, commentId },
|
||||
data: { uri, source, pathname, commentId, collectionId },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -279,3 +283,58 @@ export const doRecommendationClicked = (claimId: string, index: number) => (disp
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function doToggleLoopList(collectionId: string, loop: boolean, hideToast: boolean) {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.TOGGLE_LOOP_LIST,
|
||||
data: { collectionId, loop },
|
||||
});
|
||||
if (loop && !hideToast) {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __('Loop is on.'),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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 },
|
||||
});
|
||||
if (!hideToast) {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __('Shuffle is on.'),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: ACTIONS.TOGGLE_SHUFFLE_LIST,
|
||||
data: { collectionId, newUrls: false },
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ export const IS_MAC = process.platform === 'darwin';
|
|||
const UPDATE_IS_NIGHT_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
export function doFetchDaemonSettings() {
|
||||
return dispatch => {
|
||||
Lbry.settings_get().then(settings => {
|
||||
return (dispatch) => {
|
||||
Lbry.settings_get().then((settings) => {
|
||||
analytics.toggleInternal(settings.share_usage_data);
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||
|
@ -32,11 +32,11 @@ export function doFetchDaemonSettings() {
|
|||
}
|
||||
|
||||
export function doFindFFmpeg() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED,
|
||||
});
|
||||
return Lbry.ffmpeg_find().then(done => {
|
||||
return Lbry.ffmpeg_find().then((done) => {
|
||||
dispatch(doGetDaemonStatus());
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED,
|
||||
|
@ -46,8 +46,8 @@ export function doFindFFmpeg() {
|
|||
}
|
||||
|
||||
export function doGetDaemonStatus() {
|
||||
return dispatch => {
|
||||
return Lbry.status().then(status => {
|
||||
return (dispatch) => {
|
||||
return Lbry.status().then((status) => {
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_STATUS_RECEIVED,
|
||||
data: {
|
||||
|
@ -72,7 +72,7 @@ export function doClearDaemonSetting(key) {
|
|||
key,
|
||||
};
|
||||
// not if syncLocked
|
||||
Lbry.settings_clear(clearKey).then(defaultSettings => {
|
||||
Lbry.settings_clear(clearKey).then((defaultSettings) => {
|
||||
if (SDK_SYNC_KEYS.includes(key)) {
|
||||
dispatch({
|
||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||
|
@ -83,7 +83,7 @@ export function doClearDaemonSetting(key) {
|
|||
dispatch(doWalletReconnect());
|
||||
}
|
||||
});
|
||||
Lbry.settings_get().then(settings => {
|
||||
Lbry.settings_get().then((settings) => {
|
||||
analytics.toggleInternal(settings.share_usage_data);
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||
|
@ -108,7 +108,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
|||
key,
|
||||
value: !value && value !== false ? null : value,
|
||||
};
|
||||
Lbry.settings_set(newSettings).then(newSetting => {
|
||||
Lbry.settings_set(newSettings).then((newSetting) => {
|
||||
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
|
||||
dispatch({
|
||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||
|
@ -121,7 +121,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
|||
// todo: add sdk reloadsettings() (or it happens automagically?)
|
||||
}
|
||||
});
|
||||
Lbry.settings_get().then(settings => {
|
||||
Lbry.settings_get().then((settings) => {
|
||||
analytics.toggleInternal(settings.share_usage_data);
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||
|
@ -170,7 +170,7 @@ export function doUpdateIsNight() {
|
|||
}
|
||||
|
||||
export function doUpdateIsNightAsync() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doUpdateIsNight());
|
||||
|
||||
setInterval(() => dispatch(doUpdateIsNight()), UPDATE_IS_NIGHT_INTERVAL);
|
||||
|
@ -201,8 +201,8 @@ export function doSetDarkTime(value, options) {
|
|||
|
||||
export function doGetWalletSyncPreference() {
|
||||
const SYNC_KEY = 'enable-sync';
|
||||
return dispatch => {
|
||||
return Lbry.preference_get({ key: SYNC_KEY }).then(result => {
|
||||
return (dispatch) => {
|
||||
return Lbry.preference_get({ key: SYNC_KEY }).then((result) => {
|
||||
const enabled = result && result[SYNC_KEY];
|
||||
if (enabled !== null) {
|
||||
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
||||
|
@ -214,8 +214,8 @@ export function doGetWalletSyncPreference() {
|
|||
|
||||
export function doSetWalletSyncPreference(pref) {
|
||||
const SYNC_KEY = 'enable-sync';
|
||||
return dispatch => {
|
||||
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then(result => {
|
||||
return (dispatch) => {
|
||||
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then((result) => {
|
||||
const enabled = result && result[SYNC_KEY];
|
||||
if (enabled !== null) {
|
||||
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
||||
|
@ -226,7 +226,7 @@ export function doSetWalletSyncPreference(pref) {
|
|||
}
|
||||
|
||||
export function doPushSettingsToPrefs() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS,
|
||||
|
@ -274,8 +274,8 @@ export function doFetchLanguage(language) {
|
|||
if (settings.language !== language || (settings.loadedLanguages && !settings.loadedLanguages.includes(language))) {
|
||||
// this should match the behavior/logic in index-web.html
|
||||
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
window.i18n_messages[language] = j;
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
||||
|
@ -284,7 +284,7 @@ export function doFetchLanguage(language) {
|
|||
},
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_FAILURE,
|
||||
});
|
||||
|
@ -324,8 +324,8 @@ export function doSetLanguage(language) {
|
|||
) {
|
||||
// this should match the behavior/logic in index-web.html
|
||||
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
window.i18n_messages[language] = j;
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
||||
|
@ -344,7 +344,7 @@ export function doSetLanguage(language) {
|
|||
});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
window.localStorage.setItem(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE);
|
||||
dispatch(doSetClientSetting(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE));
|
||||
const languageName = SUPPORTED_LANGUAGES[language] ? SUPPORTED_LANGUAGES[language] : language;
|
||||
|
@ -369,7 +369,7 @@ export function doSetAutoLaunch(value) {
|
|||
}
|
||||
|
||||
if (value === undefined) {
|
||||
launcher.isEnabled().then(isEnabled => {
|
||||
launcher.isEnabled().then((isEnabled) => {
|
||||
if (isEnabled) {
|
||||
if (!autoLaunch) {
|
||||
launcher.disable().then(() => {
|
||||
|
@ -385,7 +385,7 @@ export function doSetAutoLaunch(value) {
|
|||
}
|
||||
});
|
||||
} else if (value === true) {
|
||||
launcher.isEnabled().then(function(isEnabled) {
|
||||
launcher.isEnabled().then(function (isEnabled) {
|
||||
if (!isEnabled) {
|
||||
launcher.enable().then(() => {
|
||||
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, true));
|
||||
|
@ -396,7 +396,7 @@ export function doSetAutoLaunch(value) {
|
|||
});
|
||||
} else {
|
||||
// value = false
|
||||
launcher.isEnabled().then(function(isEnabled) {
|
||||
launcher.isEnabled().then(function (isEnabled) {
|
||||
if (isEnabled) {
|
||||
launcher.disable().then(() => {
|
||||
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, false));
|
||||
|
@ -410,7 +410,7 @@ export function doSetAutoLaunch(value) {
|
|||
}
|
||||
|
||||
export function doSetAppToTrayWhenClosed(value) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
window.localStorage.setItem(SETTINGS.TO_TRAY_WHEN_CLOSED, value);
|
||||
dispatch(doSetClientSetting(SETTINGS.TO_TRAY_WHEN_CLOSED, value));
|
||||
};
|
||||
|
@ -424,3 +424,12 @@ export function toggleVideoTheaterMode() {
|
|||
dispatch(doSetClientSetting(SETTINGS.VIDEO_THEATER_MODE, !videoTheaterMode));
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleAutoplayNext() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const autoplayNext = makeSelectClientSetting(SETTINGS.AUTOPLAY_NEXT)(state);
|
||||
|
||||
dispatch(doSetClientSetting(SETTINGS.AUTOPLAY_NEXT, !autoplayNext));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,10 +25,27 @@ reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
|
|||
source: action.data.source,
|
||||
pathname: action.data.pathname,
|
||||
commentId: action.data.commentId,
|
||||
collectionId: action.data.collectionId,
|
||||
primaryUri: state.primaryUri,
|
||||
},
|
||||
});
|
||||
|
||||
reducers[ACTIONS.TOGGLE_LOOP_LIST] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
loopList: {
|
||||
collectionId: action.data.collectionId,
|
||||
loop: action.data.loop,
|
||||
},
|
||||
});
|
||||
|
||||
reducers[ACTIONS.TOGGLE_SHUFFLE_LIST] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
shuffleList: {
|
||||
collectionId: action.data.collectionId,
|
||||
newUrls: action.data.newUrls,
|
||||
},
|
||||
});
|
||||
|
||||
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
||||
const { claimId, outpoint, position } = action.data;
|
||||
return {
|
||||
|
|
|
@ -69,7 +69,7 @@ const defaultState = {
|
|||
|
||||
// Content
|
||||
[SETTINGS.SHOW_MATURE]: false,
|
||||
[SETTINGS.AUTOPLAY]: true,
|
||||
[SETTINGS.AUTOPLAY_MEDIA]: true,
|
||||
[SETTINGS.AUTOPLAY_NEXT]: true,
|
||||
[SETTINGS.FLOATING_PLAYER]: true,
|
||||
[SETTINGS.AUTO_DOWNLOAD]: true,
|
||||
|
|
|
@ -29,6 +29,9 @@ export const selectState = (state: any) => state.content || {};
|
|||
export const selectPlayingUri = createSelector(selectState, (state) => state.playingUri);
|
||||
export const selectPrimaryUri = createSelector(selectState, (state) => state.primaryUri);
|
||||
|
||||
export const selectListLoop = createSelector(selectState, (state) => state.loopList);
|
||||
export const selectListShuffle = createSelector(selectState, (state) => state.shuffleList);
|
||||
|
||||
export const makeSelectIsPlaying = (uri: string) =>
|
||||
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
|
||||
|
||||
|
|
|
@ -132,6 +132,71 @@
|
|||
}
|
||||
}
|
||||
|
||||
.vjs-button--play-next.vjs-button,
|
||||
.vjs-button--play-previous.vjs-button,
|
||||
.vjs-button--autoplay-next.vjs-button {
|
||||
display: block;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
display: block;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-button--play-next.vjs-button {
|
||||
order: 0;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='18' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-skip-forward'%3e%3cpolygon points='5 4 15 12 5 20 5 4'%3e%3c/polygon%3e%3cline x1='19' y1='5' x2='19' y2='19'%3e%3c/line%3e%3c/svg%3e");
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
order: 0;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='18' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-skip-forward'%3e%3cpolygon points='5 4 15 12 5 20 5 4'%3e%3c/polygon%3e%3cline x1='19' y1='5' x2='19' y2='19'%3e%3c/line%3e%3c/svg%3e");
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-button--play-previous.vjs-button {
|
||||
order: 0;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='18' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-skip-back'%3e%3cpolygon points='19 20 9 12 19 4 19 20'%3e%3c/polygon%3e%3cline x1='5' y1='19' x2='5' y2='5'%3e%3c/line%3e%3c/svg%3e");
|
||||
|
||||
&:focus:not(:focus-visible) {
|
||||
order: 0;
|
||||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='18' height='14' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round' class='feather feather-skip-back'%3e%3cpolygon points='19 20 9 12 19 4 19 20'%3e%3c/polygon%3e%3cline x1='5' y1='19' x2='5' y2='5'%3e%3c/line%3e%3c/svg%3e");
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-button--autoplay-next.vjs-button {
|
||||
margin: auto;
|
||||
order: 1;
|
||||
width: 24px;
|
||||
height: 14px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--color-gray-4);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.vjs-button--autoplay-next.vjs-button[aria-checked=true] {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.vjs-button--autoplay-next.vjs-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background: var(--color-white);
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.25);
|
||||
border-radius: var(--border-radius);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.vjs-button--autoplay-next.vjs-button[aria-checked=true]::after {
|
||||
transform: translateX(12px);
|
||||
}
|
||||
|
||||
.button--link {
|
||||
color: var(--color-link);
|
||||
transition: color 0.2s;
|
||||
|
|
|
@ -316,6 +316,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
font-size: var(--font-xsmall);
|
||||
|
||||
.menu__link {
|
||||
color: var(--color-text);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-page__recommended-collection {
|
||||
@extend .file-page__recommended;
|
||||
flex-direction: column;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.file-page__recommended-collection__row {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
max-width: 50rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
flex-direction: column;
|
||||
> :first-child {
|
||||
|
|
|
@ -10134,9 +10134,9 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#dc264ec50ca3208d940521bdac0b61352d913c95:
|
||||
lbry-redux@lbryio/lbry-redux#12a2ffc708bed45ba8d5a46620dc3892aaf890f8:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/dc264ec50ca3208d940521bdac0b61352d913c95"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/12a2ffc708bed45ba8d5a46620dc3892aaf890f8"
|
||||
dependencies:
|
||||
"@ungap/from-entries" "^0.2.1"
|
||||
proxy-polyfill "0.1.6"
|
||||
|
|
Loading…
Reference in a new issue