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))
|
- 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))
|
- 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 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
|
### Changed
|
||||||
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))
|
- 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))
|
- 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))
|
- 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))
|
- 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
|
### Fixed
|
||||||
- App now supports '#' and ':' for claimId separator ([#6496](https://github.com/lbryio/lbry-desktop/pull/6496))
|
- 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 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 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))
|
- 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]
|
## [0.51.1] - [2021-06-26]
|
||||||
|
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
"imagesloaded": "^4.1.4",
|
"imagesloaded": "^4.1.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"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",
|
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { connect } from 'react-redux';
|
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 { 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 AutoplayCountdown from './view';
|
||||||
import { selectModal } from 'redux/selectors/app';
|
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.
|
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
|
This is dumb but I'm just the guy who noticed -kj
|
||||||
*/
|
*/
|
||||||
const select = (state, props) => {
|
const select = (state, props) => ({
|
||||||
const { location } = props;
|
nextRecommendedClaim: makeSelectClaimForUri(props.nextRecommendedUri)(state),
|
||||||
const { search } = location;
|
|
||||||
const urlParams = new URLSearchParams(search);
|
|
||||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
|
||||||
|
|
||||||
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),
|
modal: selectModal(state),
|
||||||
};
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default withRouter(
|
export default withRouter(connect(select, null)(AutoplayCountdown));
|
||||||
connect(select, {
|
|
||||||
doSetPlayingUri,
|
|
||||||
doPlayUri,
|
|
||||||
clearPosition,
|
|
||||||
})(AutoplayCountdown)
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { useCallback } from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
import * as ICONS from 'constants/icons';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
||||||
const CLASSNAME_AUTOPLAY_COUNTDOWN = 'autoplay-countdown';
|
const CLASSNAME_AUTOPLAY_COUNTDOWN = 'autoplay-countdown';
|
||||||
|
|
||||||
|
@ -14,25 +14,19 @@ type Props = {
|
||||||
history: { push: (string) => void },
|
history: { push: (string) => void },
|
||||||
nextRecommendedClaim: ?StreamClaim,
|
nextRecommendedClaim: ?StreamClaim,
|
||||||
nextRecommendedUri: string,
|
nextRecommendedUri: string,
|
||||||
isFloating: boolean,
|
|
||||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
|
||||||
doPlayUri: (string) => void,
|
|
||||||
modal: { id: string, modalProps: {} },
|
modal: { id: string, modalProps: {} },
|
||||||
collectionId?: string,
|
doNavigate: () => void,
|
||||||
clearPosition: (string) => void,
|
doReplay: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function AutoplayCountdown(props: Props) {
|
function AutoplayCountdown(props: Props) {
|
||||||
const {
|
const {
|
||||||
nextRecommendedUri,
|
nextRecommendedUri,
|
||||||
nextRecommendedClaim,
|
nextRecommendedClaim,
|
||||||
doSetPlayingUri,
|
|
||||||
doPlayUri,
|
|
||||||
isFloating,
|
|
||||||
history: { push },
|
history: { push },
|
||||||
modal,
|
modal,
|
||||||
collectionId,
|
doNavigate,
|
||||||
clearPosition,
|
doReplay,
|
||||||
} = props;
|
} = props;
|
||||||
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
||||||
|
|
||||||
|
@ -45,40 +39,6 @@ function AutoplayCountdown(props: Props) {
|
||||||
const anyModalPresent = modal !== undefined && modal !== null;
|
const anyModalPresent = modal !== undefined && modal !== null;
|
||||||
const isTimerPaused = timerPaused || anyModalPresent;
|
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() {
|
function shouldPauseAutoplay() {
|
||||||
const elm = document.querySelector(`.${CLASSNAME_AUTOPLAY_COUNTDOWN}`);
|
const elm = document.querySelector(`.${CLASSNAME_AUTOPLAY_COUNTDOWN}`);
|
||||||
return elm && elm.getBoundingClientRect().top < 0;
|
return elm && elm.getBoundingClientRect().top < 0;
|
||||||
|
@ -118,7 +78,7 @@ function AutoplayCountdown(props: Props) {
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [timer, doNavigate, navigateUrl, push, timerCanceled, isTimerPaused, nextRecommendedUri]);
|
}, [timer, doNavigate, push, timerCanceled, isTimerPaused, nextRecommendedUri]);
|
||||||
|
|
||||||
if (timerCanceled || !nextRecommendedUri) {
|
if (timerCanceled || !nextRecommendedUri) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -149,6 +109,15 @@ function AutoplayCountdown(props: Props) {
|
||||||
<Button label={__('Cancel')} button="link" onClick={() => setTimerCanceled(true)} />
|
<Button label={__('Cancel')} button="link" onClick={() => setTimerCanceled(true)} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
label={__('Replay?')}
|
||||||
|
button="link"
|
||||||
|
iconRight={ICONS.REPLAY}
|
||||||
|
onClick={() => {
|
||||||
|
setTimerCanceled(true);
|
||||||
|
doReplay();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
COLLECTIONS_CONSTS,
|
COLLECTIONS_CONSTS,
|
||||||
makeSelectEditedCollectionForId,
|
makeSelectEditedCollectionForId,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
|
doFetchItemsInCollection,
|
||||||
|
makeSelectUrlsForCollectionId,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { doChannelMute, doChannelUnmute } from 'redux/actions/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 { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
import { selectListShuffle } from 'redux/selectors/content';
|
||||||
|
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||||
import ClaimPreview from './view';
|
import ClaimPreview from './view';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const claim = makeSelectClaimForUri(props.uri, false)(state);
|
const claim = makeSelectClaimForUri(props.uri, false)(state);
|
||||||
|
const collectionId = props.collectionId;
|
||||||
|
const resolvedList = makeSelectUrlsForCollectionId(collectionId)(state);
|
||||||
const repostedClaim = claim && claim.reposted_claim;
|
const repostedClaim = claim && claim.reposted_claim;
|
||||||
const contentClaim = repostedClaim || claim;
|
const contentClaim = repostedClaim || claim;
|
||||||
const contentSigningChannel = contentClaim && contentClaim.signing_channel;
|
const contentSigningChannel = contentClaim && contentClaim.signing_channel;
|
||||||
const contentPermanentUri = contentClaim && contentClaim.permanent_url;
|
const contentPermanentUri = contentClaim && contentClaim.permanent_url;
|
||||||
const contentChannelUri = (contentSigningChannel && contentSigningChannel.permanent_url) || contentPermanentUri;
|
const contentChannelUri = (contentSigningChannel && contentSigningChannel.permanent_url) || contentPermanentUri;
|
||||||
|
const shuffleList = selectListShuffle(state);
|
||||||
|
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||||
|
const playNextUri = shuffle && shuffle[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claim,
|
claim,
|
||||||
|
@ -60,10 +69,12 @@ const select = (state, props) => {
|
||||||
isSubscribed: makeSelectIsSubscribed(contentChannelUri, true)(state),
|
isSubscribed: makeSelectIsSubscribed(contentChannelUri, true)(state),
|
||||||
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
|
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
|
||||||
isAdmin: selectHasAdminChannel(state),
|
isAdmin: selectHasAdminChannel(state),
|
||||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, contentPermanentUri)(state),
|
claimInCollection: makeSelectCollectionForIdHasClaimUrl(collectionId, contentPermanentUri)(state),
|
||||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
|
||||||
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
|
editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
|
||||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||||
|
resolvedList,
|
||||||
|
playNextUri,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,6 +101,9 @@ const perform = (dispatch) => ({
|
||||||
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),
|
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),
|
||||||
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
|
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
|
||||||
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
|
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
|
||||||
|
fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })),
|
||||||
|
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||||
|
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ClaimPreview);
|
export default connect(select, perform)(ClaimPreview);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
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 { useHistory } from 'react-router';
|
||||||
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
|
@ -56,6 +56,11 @@ type Props = {
|
||||||
isChannelPage: boolean,
|
isChannelPage: boolean,
|
||||||
editedCollection: Collection,
|
editedCollection: Collection,
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
|
playNextUri: string,
|
||||||
|
resolvedList: boolean,
|
||||||
|
fetchCollectionItems: (string) => void,
|
||||||
|
doSetPlayingUri: (string) => void,
|
||||||
|
doToggleShuffleList: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimMenuList(props: Props) {
|
function ClaimMenuList(props: Props) {
|
||||||
|
@ -93,7 +98,13 @@ function ClaimMenuList(props: Props) {
|
||||||
isChannelPage = false,
|
isChannelPage = false,
|
||||||
editedCollection,
|
editedCollection,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
playNextUri,
|
||||||
|
resolvedList,
|
||||||
|
fetchCollectionItems,
|
||||||
|
doSetPlayingUri,
|
||||||
|
doToggleShuffleList,
|
||||||
} = props;
|
} = props;
|
||||||
|
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||||
const isChannel = !incognitoClaim && !contentSigningChannel;
|
const isChannel = !incognitoClaim && !contentSigningChannel;
|
||||||
const { channelName } = parseURI(contentChannelUri);
|
const { channelName } = parseURI(contentChannelUri);
|
||||||
|
@ -107,6 +118,27 @@ function ClaimMenuList(props: Props) {
|
||||||
: __('Follow');
|
: __('Follow');
|
||||||
|
|
||||||
const { push, replace } = useHistory();
|
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) {
|
if (!claim) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -246,9 +278,21 @@ function ClaimMenuList(props: Props) {
|
||||||
{collectionId && isCollectionClaim ? (
|
{collectionId && isCollectionClaim ? (
|
||||||
<>
|
<>
|
||||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
<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} />
|
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||||
{__('View List')}
|
{__('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>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{isMyCollection && (
|
{isMyCollection && (
|
||||||
|
|
|
@ -10,21 +10,50 @@ import {
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { selectListShuffle } from 'redux/selectors/content';
|
||||||
|
import { doPlayUri, doSetPlayingUri, doToggleShuffleList, doToggleLoopList } from 'redux/actions/content';
|
||||||
import CollectionActions from './view';
|
import CollectionActions from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
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),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
|
||||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
|
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(collectionId)(state)),
|
||||||
});
|
firstItem,
|
||||||
|
playNextUri,
|
||||||
|
playNextClaim,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
|
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);
|
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 { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { ENABLE_FILE_REACTIONS } from 'config';
|
import { ENABLE_FILE_REACTIONS } from 'config';
|
||||||
|
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -24,6 +26,14 @@ type Props = {
|
||||||
showInfo: boolean,
|
showInfo: boolean,
|
||||||
setShowInfo: (boolean) => void,
|
setShowInfo: (boolean) => void,
|
||||||
collectionHasEdits: boolean,
|
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) {
|
function CollectionActions(props: Props) {
|
||||||
|
@ -37,12 +47,64 @@ function CollectionActions(props: Props) {
|
||||||
showInfo,
|
showInfo,
|
||||||
setShowInfo,
|
setShowInfo,
|
||||||
collectionHasEdits,
|
collectionHasEdits,
|
||||||
|
isBuiltin,
|
||||||
|
doToggleShuffleList,
|
||||||
|
doToggleLoopList,
|
||||||
|
playNextUri,
|
||||||
|
playNextClaim,
|
||||||
|
doPlayUri,
|
||||||
|
doSetPlayingUri,
|
||||||
|
firstItem,
|
||||||
} = props;
|
} = props;
|
||||||
|
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const webShareable = true; // collections have cost?
|
const webShareable = true; // collections have cost?
|
||||||
|
|
||||||
|
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 = (
|
const lhsSection = (
|
||||||
|
<>
|
||||||
|
<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} />}
|
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
|
||||||
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
||||||
|
@ -57,11 +119,15 @@ function CollectionActions(props: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const rhsSection = (
|
const rhsSection = (
|
||||||
<>
|
<>
|
||||||
{isMyCollection && (
|
{!isBuiltin &&
|
||||||
|
(isMyCollection ? (
|
||||||
|
<>
|
||||||
<Button
|
<Button
|
||||||
title={uri ? __('Update') : __('Publish')}
|
title={uri ? __('Update') : __('Publish')}
|
||||||
label={uri ? __('Update') : __('Publish')}
|
label={uri ? __('Update') : __('Publish')}
|
||||||
|
@ -72,8 +138,6 @@ function CollectionActions(props: Props) {
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
disabled={claimIsPending}
|
disabled={claimIsPending}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{isMyCollection && (
|
|
||||||
<Button
|
<Button
|
||||||
className={classnames('button--file-action')}
|
className={classnames('button--file-action')}
|
||||||
title={__('Delete List')}
|
title={__('Delete List')}
|
||||||
|
@ -83,15 +147,15 @@ function CollectionActions(props: Props) {
|
||||||
description={__('Delete List')}
|
description={__('Delete List')}
|
||||||
disabled={claimIsPending}
|
disabled={claimIsPending}
|
||||||
/>
|
/>
|
||||||
)}
|
</>
|
||||||
{!isMyCollection && (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
title={__('Report content')}
|
title={__('Report content')}
|
||||||
className="button--file-action"
|
className="button--file-action"
|
||||||
icon={ICONS.REPORT}
|
icon={ICONS.REPORT}
|
||||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -7,15 +7,18 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import {
|
import { selectPlayingUri, selectListLoop, selectListShuffle } from 'redux/selectors/content';
|
||||||
selectPlayingUri,
|
import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content';
|
||||||
} from 'redux/selectors/content';
|
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const playingUri = selectPlayingUri(state);
|
const playingUri = selectPlayingUri(state);
|
||||||
const playingUrl = playingUri && playingUri.uri;
|
const playingUrl = playingUri && playingUri.uri;
|
||||||
const claim = makeSelectClaimForUri(playingUrl)(state);
|
const claim = makeSelectClaimForUri(playingUrl)(state);
|
||||||
const url = claim && claim.permanent_url;
|
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 {
|
return {
|
||||||
url,
|
url,
|
||||||
|
@ -23,7 +26,12 @@ const select = (state, props) => {
|
||||||
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
||||||
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
||||||
isMine: makeSelectClaimIsMine(url)(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>,
|
collectionUrls: Array<Claim>,
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
collection: any,
|
collection: any,
|
||||||
|
loop: boolean,
|
||||||
|
shuffle: boolean,
|
||||||
|
doToggleLoopList: (string, boolean) => void,
|
||||||
|
doToggleShuffleList: (string, string, boolean) => void,
|
||||||
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CollectionContent(props: Props) {
|
export default function CollectionContent(props: Props) {
|
||||||
const { collectionUrls, collectionName, id, url } = props;
|
const { collectionUrls, collectionName, id, url, loop, shuffle, doToggleLoopList, doToggleShuffleList } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
isBodyList
|
isBodyList
|
||||||
className="file-page__recommended"
|
className="file-page__recommended-collection"
|
||||||
title={
|
title={
|
||||||
<span>
|
<>
|
||||||
|
<span className="file-page__recommended-collection__row">
|
||||||
<Icon
|
<Icon
|
||||||
icon={(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
icon={
|
||||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
className="icon--margin-right" />
|
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||||
|
ICONS.STACK
|
||||||
|
}
|
||||||
|
className="icon--margin-right"
|
||||||
|
/>
|
||||||
{collectionName}
|
{collectionName}
|
||||||
</span>
|
</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={
|
titleActions={
|
||||||
<div className="card__title-actions--link">
|
<div className="card__title-actions--link">
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux';
|
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { selectListShuffle } from 'redux/selectors/content';
|
||||||
|
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||||
import CollectionMenuList from './view';
|
import CollectionMenuList from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
const collectionId = props.collectionId;
|
||||||
|
const shuffleList = selectListShuffle(state);
|
||||||
|
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||||
|
const playNextUri = shuffle && shuffle[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||||
|
playNextUri,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,4 +21,6 @@ export default connect(select, {
|
||||||
doCollectionEdit,
|
doCollectionEdit,
|
||||||
doOpenModal,
|
doOpenModal,
|
||||||
doCollectionDelete,
|
doCollectionDelete,
|
||||||
|
doSetPlayingUri,
|
||||||
|
doToggleShuffleList,
|
||||||
})(CollectionMenuList);
|
})(CollectionMenuList);
|
||||||
|
|
|
@ -7,19 +7,44 @@ import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
inline?: boolean,
|
inline?: boolean,
|
||||||
doOpenModal: (string, {}) => void,
|
doOpenModal: (string, {}) => void,
|
||||||
collectionName?: string,
|
collectionName?: string,
|
||||||
collectionId: string,
|
collectionId: string,
|
||||||
|
playNextUri: string,
|
||||||
|
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||||
|
doToggleShuffleList: (string, string, boolean, boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CollectionMenuList(props: Props) {
|
function CollectionMenuList(props: Props) {
|
||||||
const { inline = false, collectionId, collectionName, doOpenModal } = props;
|
const {
|
||||||
|
inline = false,
|
||||||
|
collectionId,
|
||||||
|
collectionName,
|
||||||
|
doOpenModal,
|
||||||
|
playNextUri,
|
||||||
|
doSetPlayingUri,
|
||||||
|
doToggleShuffleList,
|
||||||
|
} = props;
|
||||||
|
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||||
|
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (playNextUri && doShuffle) {
|
||||||
|
const collectionParams = new URLSearchParams();
|
||||||
|
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||||
|
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
|
||||||
|
setDoShuffle(false);
|
||||||
|
doSetPlayingUri({ uri: playNextUri });
|
||||||
|
push(navigateUrl);
|
||||||
|
}
|
||||||
|
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
|
@ -35,9 +60,21 @@ function CollectionMenuList(props: Props) {
|
||||||
{collectionId && collectionName && (
|
{collectionId && collectionName && (
|
||||||
<>
|
<>
|
||||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
<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} />
|
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||||
{__('View List')}
|
{__('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>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|
|
@ -2439,4 +2439,28 @@ export const icons = {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</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';
|
import FileRender from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const autoplay = props.embedded ? false : makeSelectClientSetting(SETTINGS.AUTOPLAY)(state);
|
const autoplay = props.embedded ? false : makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state);
|
||||||
return {
|
return {
|
||||||
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
|
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
|
|
@ -22,6 +22,7 @@ const select = (state, props) => {
|
||||||
const playingUri = selectPlayingUri(state);
|
const playingUri = selectPlayingUri(state);
|
||||||
const primaryUri = selectPrimaryUri(state);
|
const primaryUri = selectPrimaryUri(state);
|
||||||
const uri = playingUri && playingUri.uri;
|
const uri = playingUri && playingUri.uri;
|
||||||
|
const collectionId = playingUri && playingUri.collectionId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
|
@ -35,6 +36,7 @@ const select = (state, props) => {
|
||||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
||||||
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(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 { useIsMobile } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
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 IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
||||||
|
@ -34,6 +34,7 @@ type Props = {
|
||||||
primaryUri: ?string,
|
primaryUri: ?string,
|
||||||
videoTheaterMode: boolean,
|
videoTheaterMode: boolean,
|
||||||
doFetchRecommendedContent: (string, boolean) => void,
|
doFetchRecommendedContent: (string, boolean) => void,
|
||||||
|
collectionId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderFloating(props: Props) {
|
export default function FileRenderFloating(props: Props) {
|
||||||
|
@ -51,6 +52,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
primaryUri,
|
primaryUri,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
doFetchRecommendedContent,
|
doFetchRecommendedContent,
|
||||||
|
collectionId,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
const {
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
|
@ -69,6 +71,13 @@ export default function FileRenderFloating(props: Props) {
|
||||||
y: 0,
|
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 playingUriSource = playingUri && playingUri.source;
|
||||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||||
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||||
|
@ -303,7 +312,12 @@ export default function FileRenderFloating(props: Props) {
|
||||||
{isFloating && (
|
{isFloating && (
|
||||||
<div className="draggable content__info">
|
<div className="draggable content__info">
|
||||||
<div className="claim-preview__title" title={title || uri}>
|
<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>
|
</div>
|
||||||
<UriIndicator link uri={uri} />
|
<UriIndicator link uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
makeSelectStreamingUrlForUri,
|
makeSelectStreamingUrlForUri,
|
||||||
makeSelectClaimWasPurchased,
|
makeSelectClaimWasPurchased,
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
|
COLLECTIONS_CONSTS,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
@ -22,7 +23,12 @@ import {
|
||||||
import FileRenderInitiator from './view';
|
import FileRenderInitiator from './view';
|
||||||
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
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),
|
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||||
|
@ -30,19 +36,21 @@ const select = (state, props) => ({
|
||||||
playingUri: selectPlayingUri(state),
|
playingUri: selectPlayingUri(state),
|
||||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY_MEDIA)(state),
|
||||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
});
|
collectionId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
play: (uri) => {
|
play: (uri, collectionId) => {
|
||||||
dispatch(doSetPrimaryUri(uri));
|
dispatch(doSetPrimaryUri(uri));
|
||||||
dispatch(doSetPlayingUri({ uri }));
|
dispatch(doSetPlayingUri({ uri, collectionId }));
|
||||||
dispatch(doPlayUri(uri, undefined, undefined, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
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;
|
const SPACE_BAR_KEYCODE = 32;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
play: (string) => void,
|
play: (string, string) => void,
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
isPlaying: boolean,
|
isPlaying: boolean,
|
||||||
fileInfo: FileListItem,
|
fileInfo: FileListItem,
|
||||||
|
@ -36,6 +36,7 @@ type Props = {
|
||||||
claimWasPurchased: boolean,
|
claimWasPurchased: boolean,
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
videoTheaterMode: boolean,
|
videoTheaterMode: boolean,
|
||||||
|
collectionId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderInitiator(props: Props) {
|
export default function FileRenderInitiator(props: Props) {
|
||||||
|
@ -55,6 +56,7 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
claimWasPurchased,
|
claimWasPurchased,
|
||||||
authenticated,
|
authenticated,
|
||||||
videoTheaterMode,
|
videoTheaterMode,
|
||||||
|
collectionId,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// force autoplay if a timestamp is present
|
// force autoplay if a timestamp is present
|
||||||
|
@ -109,9 +111,9 @@ export default function FileRenderInitiator(props: Props) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
play(uri);
|
play(uri, collectionId);
|
||||||
},
|
},
|
||||||
[play, uri]
|
[play, uri, collectionId]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -11,7 +11,8 @@ import SettingContent from './view';
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
floatingPlayer: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(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),
|
hideReposts: makeSelectClientSetting(SETTINGS.HIDE_REPOSTS)(state),
|
||||||
showNsfw: selectShowMatureContent(state),
|
showNsfw: selectShowMatureContent(state),
|
||||||
myChannelUrls: selectMyChannelUrls(state),
|
myChannelUrls: selectMyChannelUrls(state),
|
||||||
|
|
|
@ -22,7 +22,8 @@ type Props = {
|
||||||
// --- select ---
|
// --- select ---
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
floatingPlayer: boolean,
|
floatingPlayer: boolean,
|
||||||
autoplay: boolean,
|
autoplayMedia: boolean,
|
||||||
|
autoplayNext: boolean,
|
||||||
hideReposts: ?boolean,
|
hideReposts: ?boolean,
|
||||||
showNsfw: boolean,
|
showNsfw: boolean,
|
||||||
myChannelUrls: ?Array<string>,
|
myChannelUrls: ?Array<string>,
|
||||||
|
@ -39,7 +40,8 @@ export default function SettingContent(props: Props) {
|
||||||
const {
|
const {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
floatingPlayer,
|
floatingPlayer,
|
||||||
autoplay,
|
autoplayMedia,
|
||||||
|
autoplayNext,
|
||||||
hideReposts,
|
hideReposts,
|
||||||
showNsfw,
|
showNsfw,
|
||||||
myChannelUrls,
|
myChannelUrls,
|
||||||
|
@ -73,12 +75,21 @@ export default function SettingContent(props: Props) {
|
||||||
/>
|
/>
|
||||||
</SettingsRow>
|
</SettingsRow>
|
||||||
|
|
||||||
<SettingsRow title={__('Autoplay media files')} subtitle={__(HELP.AUTOPLAY)}>
|
<SettingsRow title={__('Autoplay media files')} subtitle={__(HELP.AUTOPLAY_MEDIA)}>
|
||||||
<FormField
|
<FormField
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="autoplay"
|
name="autoplay media"
|
||||||
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
|
onChange={() => setClientSetting(SETTINGS.AUTOPLAY_MEDIA, !autoplayMedia)}
|
||||||
checked={autoplay}
|
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>
|
</SettingsRow>
|
||||||
|
|
||||||
|
@ -208,7 +219,8 @@ export default function SettingContent(props: Props) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const HELP = {
|
const HELP = {
|
||||||
FLOATING_PLAYER: 'Keep content playing in the corner when navigating to a different page.',
|
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.',
|
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. ',
|
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.',
|
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 { 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 { doChangeVolume, doChangeMute, doAnalyticsView, doAnalyticsBuffer } from 'redux/actions/app';
|
||||||
import { selectVolume, selectMute } from 'redux/selectors/app';
|
import { selectVolume, selectMute } from 'redux/selectors/app';
|
||||||
import { savePosition, clearPosition } from 'redux/actions/content';
|
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
import {
|
||||||
|
makeSelectContentPositionForUri,
|
||||||
|
makeSelectIsPlayerFloating,
|
||||||
|
makeSelectNextUnplayedRecommended,
|
||||||
|
selectPlayingUri,
|
||||||
|
} from 'redux/selectors/content';
|
||||||
import VideoViewer from './view';
|
import VideoViewer from './view';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||||
import { selectDaemonSettings, makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
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';
|
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { search } = props.location;
|
const { search } = props.location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const autoplay = urlParams.get('autoplay');
|
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)
|
// 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 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 {
|
return {
|
||||||
autoplayIfEmbedded: Boolean(autoplay),
|
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),
|
volume: selectVolume(state),
|
||||||
muted: selectMute(state),
|
muted: selectMute(state),
|
||||||
videoPlaybackRate: makeSelectClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE)(state),
|
videoPlaybackRate: makeSelectClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE)(state),
|
||||||
position: position,
|
position: position,
|
||||||
hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)),
|
hasFileInfo: Boolean(makeSelectFileInfoForUri(uri)(state)),
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(uri)(state),
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
userId: userId,
|
userId: userId,
|
||||||
shareTelemetry: IS_WEB || selectDaemonSettings(state).share_usage_data,
|
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)),
|
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
|
||||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||||
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
|
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
|
||||||
|
toggleAutoplayNext: () => dispatch(toggleAutoplayNext()),
|
||||||
setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
|
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));
|
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
|
// @flow
|
||||||
import type { Player } from './videojs';
|
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) {
|
export function addTheaterModeButton(player: Player, toggleVideoTheaterMode: () => void) {
|
||||||
var myButton = player.controlBar.addChild('button', {
|
const controlBar = player.getChild('controlBar');
|
||||||
text: __('Theater mode'),
|
|
||||||
|
const theaterMode = new TheaterModeButton(player, {
|
||||||
|
name: 'TheaterModeButton',
|
||||||
|
text: 'Theater mode',
|
||||||
clickHandler: () => {
|
clickHandler: () => {
|
||||||
toggleVideoTheaterMode();
|
toggleVideoTheaterMode();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// $FlowFixMe
|
controlBar.addChild(theaterMode);
|
||||||
myButton.addClass('vjs-button--theater-mode');
|
|
||||||
// $FlowFixMe
|
|
||||||
myButton.setAttribute('title', __('Theater mode'));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,16 +49,21 @@ type Props = {
|
||||||
source: string,
|
source: string,
|
||||||
sourceType: string,
|
sourceType: string,
|
||||||
poster: ?string,
|
poster: ?string,
|
||||||
onPlayerReady: (Player) => void,
|
onPlayerReady: (Player, any) => void,
|
||||||
isAudio: boolean,
|
isAudio: boolean,
|
||||||
startMuted: boolean,
|
startMuted: boolean,
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
|
autoplaySetting: boolean,
|
||||||
toggleVideoTheaterMode: () => void,
|
toggleVideoTheaterMode: () => void,
|
||||||
adUrl: ?string,
|
adUrl: ?string,
|
||||||
claimId: ?string,
|
claimId: ?string,
|
||||||
userId: ?number,
|
userId: ?number,
|
||||||
// allowPreRoll: ?boolean,
|
// allowPreRoll: ?boolean,
|
||||||
shareTelemetry: boolean,
|
shareTelemetry: boolean,
|
||||||
|
replay: boolean,
|
||||||
|
videoTheaterMode: boolean,
|
||||||
|
playNext: () => void,
|
||||||
|
playPrevious: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type VideoJSOptions = {
|
// type VideoJSOptions = {
|
||||||
|
@ -103,6 +108,9 @@ const SMALL_J_KEYCODE = 74;
|
||||||
const SMALL_K_KEYCODE = 75;
|
const SMALL_K_KEYCODE = 75;
|
||||||
const SMALL_L_KEYCODE = 76;
|
const SMALL_L_KEYCODE = 76;
|
||||||
|
|
||||||
|
const P_KEYCODE = 80;
|
||||||
|
const N_KEYCODE = 78;
|
||||||
|
|
||||||
const FULLSCREEN_KEYCODE = SMALL_F_KEYCODE;
|
const FULLSCREEN_KEYCODE = SMALL_F_KEYCODE;
|
||||||
const MUTE_KEYCODE = SMALL_M_KEYCODE;
|
const MUTE_KEYCODE = SMALL_M_KEYCODE;
|
||||||
const THEATER_MODE_KEYCODE = SMALL_T_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) {
|
export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
const {
|
const {
|
||||||
autoplay,
|
autoplay,
|
||||||
|
autoplaySetting,
|
||||||
startMuted,
|
startMuted,
|
||||||
source,
|
source,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
@ -197,6 +206,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
userId,
|
userId,
|
||||||
// allowPreRoll,
|
// allowPreRoll,
|
||||||
shareTelemetry,
|
shareTelemetry,
|
||||||
|
replay,
|
||||||
|
videoTheaterMode,
|
||||||
|
playNext,
|
||||||
|
playPrevious,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [reload, setReload] = useState('initial');
|
const [reload, setReload] = useState('initial');
|
||||||
|
@ -324,13 +337,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
resolveCtrlText({ type: 'pause' });
|
resolveCtrlText({ type: 'pause' });
|
||||||
resolveCtrlText({ type: 'volumechange' });
|
resolveCtrlText({ type: 'volumechange' });
|
||||||
resolveCtrlText({ type: 'fullscreenchange' });
|
resolveCtrlText({ type: 'fullscreenchange' });
|
||||||
// (1) The 'Theater mode' button should probably be changed to a class
|
controlBar
|
||||||
// so that we can use getChild() with a specific name. There might be
|
.getChild('TheaterModeButton')
|
||||||
// clashes if we add a new button in the future.
|
.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
||||||
// (2) We'll have to get 'makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)'
|
controlBar.getChild('PlayNextButton').controlText(__('Play Next (SHIFT+N)'));
|
||||||
// as a prop here so we can say "Theater mode|Default mode" instead of
|
controlBar.getChild('PlayPreviousButton').controlText(__('Play Previous (SHIFT+P)'));
|
||||||
// "Toggle Theater mode".
|
|
||||||
controlBar.getChild('Button').controlText(__('Toggle Theater mode (t)'));
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (isDev) throw Error('Unexpected: ' + e.type);
|
if (isDev) throw Error('Unexpected: ' + e.type);
|
||||||
|
@ -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.altKey || e.ctrlKey || e.metaKey || !e.shiftKey) return;
|
||||||
if (e.keyCode === PERIOD_KEYCODE) changePlaybackSpeed(true);
|
if (e.keyCode === PERIOD_KEYCODE) changePlaybackSpeed(true);
|
||||||
if (e.keyCode === COMMA_KEYCODE) changePlaybackSpeed(false);
|
if (e.keyCode === COMMA_KEYCODE) changePlaybackSpeed(false);
|
||||||
|
if (e.keyCode === N_KEYCODE) playNext();
|
||||||
|
if (e.keyCode === P_KEYCODE) playPrevious();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSingleKeyActions(e: KeyboardEvent) {
|
function handleSingleKeyActions(e: KeyboardEvent) {
|
||||||
|
@ -585,7 +598,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
player.children_[0].setAttribute('playsinline', '');
|
player.children_[0].setAttribute('playsinline', '');
|
||||||
|
|
||||||
// I think this is a callback function
|
// I think this is a callback function
|
||||||
onPlayerReady(player);
|
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||||
|
onPlayerReady(player, videoNode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// pre-roll ads
|
// pre-roll ads
|
||||||
|
@ -606,7 +620,41 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
return vjs;
|
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(() => {
|
useEffect(() => {
|
||||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||||
|
|
||||||
|
@ -635,7 +683,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
window.player = undefined;
|
window.player = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isAudio]);
|
}, [isAudio, source]);
|
||||||
|
|
||||||
// Update video player and reload when source URL changes
|
// Update video player and reload when source URL changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -16,12 +16,17 @@ import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
|
||||||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
import { addTheaterModeButton } from './internal/theater-mode';
|
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 { useGetAds } from 'effects/use-get-ads';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { getAllIds } from 'util/buildHomepage';
|
import { getAllIds } from 'util/buildHomepage';
|
||||||
import type { HomepageCat } 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_ERROR = 'play_timeout_error';
|
||||||
const PLAY_TIMEOUT_LIMIT = 2000;
|
const PLAY_TIMEOUT_LIMIT = 2000;
|
||||||
|
@ -38,7 +43,8 @@ type Props = {
|
||||||
videoPlaybackRate: number,
|
videoPlaybackRate: number,
|
||||||
volume: number,
|
volume: number,
|
||||||
uri: string,
|
uri: string,
|
||||||
autoplaySetting: boolean,
|
autoplayMedia: boolean,
|
||||||
|
autoplayNext: boolean,
|
||||||
autoplayIfEmbedded: boolean,
|
autoplayIfEmbedded: boolean,
|
||||||
desktopPlayStartTime?: number,
|
desktopPlayStartTime?: number,
|
||||||
doAnalyticsView: (string, number) => Promise<any>,
|
doAnalyticsView: (string, number) => Promise<any>,
|
||||||
|
@ -47,11 +53,19 @@ type Props = {
|
||||||
savePosition: (string, number) => void,
|
savePosition: (string, number) => void,
|
||||||
clearPosition: (string) => void,
|
clearPosition: (string) => void,
|
||||||
toggleVideoTheaterMode: () => void,
|
toggleVideoTheaterMode: () => void,
|
||||||
|
toggleAutoplayNext: () => void,
|
||||||
setVideoPlaybackRate: (number) => void,
|
setVideoPlaybackRate: (number) => void,
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
userId: number,
|
userId: number,
|
||||||
homepageData?: { [string]: HomepageCat },
|
homepageData?: { [string]: HomepageCat },
|
||||||
shareTelemetry: boolean,
|
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,
|
uri,
|
||||||
muted,
|
muted,
|
||||||
volume,
|
volume,
|
||||||
autoplaySetting,
|
autoplayMedia,
|
||||||
|
autoplayNext,
|
||||||
autoplayIfEmbedded,
|
autoplayIfEmbedded,
|
||||||
doAnalyticsView,
|
doAnalyticsView,
|
||||||
doAnalyticsBuffer,
|
doAnalyticsBuffer,
|
||||||
|
@ -81,22 +96,34 @@ function VideoViewer(props: Props) {
|
||||||
clearPosition,
|
clearPosition,
|
||||||
desktopPlayStartTime,
|
desktopPlayStartTime,
|
||||||
toggleVideoTheaterMode,
|
toggleVideoTheaterMode,
|
||||||
|
toggleAutoplayNext,
|
||||||
setVideoPlaybackRate,
|
setVideoPlaybackRate,
|
||||||
homepageData,
|
homepageData,
|
||||||
authenticated,
|
authenticated,
|
||||||
userId,
|
userId,
|
||||||
shareTelemetry,
|
shareTelemetry,
|
||||||
|
isFloating,
|
||||||
|
doPlayUri,
|
||||||
|
doSetPlayingUri,
|
||||||
|
collectionId,
|
||||||
|
nextRecommendedUri,
|
||||||
|
previousListUri,
|
||||||
|
videoTheaterMode,
|
||||||
} = props;
|
} = props;
|
||||||
|
const permanentUrl = claim && claim.permanent_url;
|
||||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||||
const isAudio = contentType.includes('audio');
|
const isAudio = contentType.includes('audio');
|
||||||
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||||
const {
|
const {
|
||||||
|
push,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
const [doNavigate, setDoNavigate] = useState(false);
|
||||||
|
const [playNextUrl, setPlayNextUrl] = useState(true);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [ended, setEnded] = useState(false);
|
||||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||||
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
||||||
const vjsCallbackDataRef: any = React.useRef();
|
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
|
/* 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 */
|
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
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)
|
// force everything to recent when URI changes, can cause weird corner cases otherwise (e.g. navigate while autoplay is true)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -152,7 +181,53 @@ function VideoViewer(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEnded = React.useCallback(() => {
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (ended) {
|
||||||
analytics.videoIsPlaying(false);
|
analytics.videoIsPlaying(false);
|
||||||
|
|
||||||
if (adUrl) {
|
if (adUrl) {
|
||||||
|
@ -162,18 +237,36 @@ function VideoViewer(props: Props) {
|
||||||
|
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
setIsEndededEmbed(true);
|
setIsEndededEmbed(true);
|
||||||
} else if (autoplaySetting) {
|
} else if (!collectionId && autoplayNext) {
|
||||||
setShowAutoplayCountdown(true);
|
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) {
|
function onPlay(player) {
|
||||||
|
setEnded(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
setShowAutoplayCountdown(false);
|
setShowAutoplayCountdown(false);
|
||||||
setIsEndededEmbed(false);
|
setIsEndededEmbed(false);
|
||||||
|
setReplay(false);
|
||||||
|
setDoNavigate(false);
|
||||||
analytics.videoIsPlaying(true, player);
|
analytics.videoIsPlaying(true, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,12 +296,29 @@ function VideoViewer(props: Props) {
|
||||||
playerReadyDependencyList.push(desktopPlayStartTime);
|
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) {
|
if (!embedded) {
|
||||||
|
setVideoNode(videoNode);
|
||||||
player.muted(muted);
|
player.muted(muted);
|
||||||
player.volume(volume);
|
player.volume(volume);
|
||||||
player.playbackRate(videoPlaybackRate);
|
player.playbackRate(videoPlaybackRate);
|
||||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||||
|
if (collectionId) {
|
||||||
|
addPlayNextButton(player, doPlayNext);
|
||||||
|
addPlayPreviousButton(player, doPlayPrevious);
|
||||||
|
} else {
|
||||||
|
addAutoplayNextButton(player, toggleAutoplayNext, autoplayNext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldPlay = !embedded || autoplayIfEmbedded;
|
const shouldPlay = !embedded || autoplayIfEmbedded;
|
||||||
|
@ -244,7 +354,7 @@ function VideoViewer(props: Props) {
|
||||||
|
|
||||||
// first play tracking, used for initializing the watchman api
|
// first play tracking, used for initializing the watchman api
|
||||||
player.on('tracking:firstplay', doTrackingFirstPlay);
|
player.on('tracking:firstplay', doTrackingFirstPlay);
|
||||||
player.on('ended', onEnded);
|
player.on('ended', () => setEnded(true));
|
||||||
player.on('play', onPlay);
|
player.on('play', onPlay);
|
||||||
player.on('pause', (event) => onPause(event, player));
|
player.on('pause', (event) => onPause(event, player));
|
||||||
player.on('dispose', (event) => onDispose(event, player));
|
player.on('dispose', (event) => onDispose(event, player));
|
||||||
|
@ -285,7 +395,14 @@ function VideoViewer(props: Props) {
|
||||||
})}
|
})}
|
||||||
onContextMenu={stopContextMenu}
|
onContextMenu={stopContextMenu}
|
||||||
>
|
>
|
||||||
{showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
|
{showAutoplayCountdown && (
|
||||||
|
<AutoplayCountdown
|
||||||
|
uri={uri}
|
||||||
|
nextRecommendedUri={nextRecommendedUri}
|
||||||
|
doNavigate={() => setDoNavigate(true)}
|
||||||
|
doReplay={() => setReplay(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||||
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||||
|
@ -332,10 +449,15 @@ function VideoViewer(props: Props) {
|
||||||
startMuted={autoplayIfEmbedded}
|
startMuted={autoplayIfEmbedded}
|
||||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||||
autoplay={!embedded || autoplayIfEmbedded}
|
autoplay={!embedded || autoplayIfEmbedded}
|
||||||
|
autoplaySetting={autoplayNext}
|
||||||
claimId={claimId}
|
claimId={claimId}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
allowPreRoll={!embedded && !authenticated}
|
allowPreRoll={!embedded && !authenticated}
|
||||||
shareTelemetry={shareTelemetry}
|
shareTelemetry={shareTelemetry}
|
||||||
|
replay={replay}
|
||||||
|
videoTheaterMode={videoTheaterMode}
|
||||||
|
playNext={doPlayNext}
|
||||||
|
playPrevious={doPlayPrevious}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
||||||
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
||||||
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
||||||
|
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';
|
||||||
|
export const TOGGLE_SHUFFLE_LIST = 'TOGGLE_SHUFFLE_LIST';
|
||||||
|
|
||||||
// Files
|
// Files
|
||||||
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
|
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
|
||||||
|
|
|
@ -170,3 +170,6 @@ export const STAR = 'star';
|
||||||
export const MUSIC = 'MusicCategory';
|
export const MUSIC = 'MusicCategory';
|
||||||
export const BADGE_MOD = 'BadgeMod';
|
export const BADGE_MOD = 'BadgeMod';
|
||||||
export const BADGE_STREAMER = 'BadgeStreamer';
|
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 THEMES = 'themes';
|
||||||
export const AUTOMATIC_DARK_MODE_ENABLED = 'automatic_dark_mode_enabled';
|
export const AUTOMATIC_DARK_MODE_ENABLED = 'automatic_dark_mode_enabled';
|
||||||
export const CLOCK_24H = 'clock_24h';
|
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 OS_NOTIFICATIONS_ENABLED = 'os_notifications_enabled';
|
||||||
export const AUTO_DOWNLOAD = 'auto_download';
|
export const AUTO_DOWNLOAD = 'auto_download';
|
||||||
export const AUTO_LAUNCH = 'auto_launch';
|
export const AUTO_LAUNCH = 'auto_launch';
|
||||||
|
|
|
@ -36,7 +36,7 @@ function ModalRemoveCollection(props: Props) {
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
<I18nMessage tokens={{ title: <cite>{uri && title ? `"${title}"` : `"${collectionName}"`}</cite> }}>
|
<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>
|
</I18nMessage>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,13 +70,12 @@ export default function CollectionPage(props: Props) {
|
||||||
|
|
||||||
const urlsReady =
|
const urlsReady =
|
||||||
collectionUrls && (totalItems === undefined || (totalItems && totalItems === collectionUrls.length));
|
collectionUrls && (totalItems === undefined || (totalItems && totalItems === collectionUrls.length));
|
||||||
const shouldFetch = !claim && !collection;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (collectionId && !urlsReady && !didTryResolve && shouldFetch) {
|
if (collectionId && !urlsReady && !didTryResolve && !collection) {
|
||||||
fetchCollectionItems(collectionId, () => setDidTryResolve(true));
|
fetchCollectionItems(collectionId, () => setDidTryResolve(true));
|
||||||
}
|
}
|
||||||
}, [collectionId, urlsReady, didTryResolve, shouldFetch, setDidTryResolve, fetchCollectionItems]);
|
}, [collectionId, urlsReady, didTryResolve, setDidTryResolve, fetchCollectionItems, collection]);
|
||||||
|
|
||||||
const pending = (
|
const pending = (
|
||||||
<div className="help card__title--help">
|
<div className="help card__title--help">
|
||||||
|
@ -112,18 +111,27 @@ export default function CollectionPage(props: Props) {
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Icon
|
<Icon
|
||||||
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
icon={
|
||||||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
className="icon--margin-right" />
|
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||||
|
ICONS.STACK
|
||||||
|
}
|
||||||
|
className="icon--margin-right"
|
||||||
|
/>
|
||||||
{claim ? claim.value.title || claim.name : collection && collection.name}
|
{claim ? claim.value.title || claim.name : collection && collection.name}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
titleActions={titleActions}
|
titleActions={titleActions}
|
||||||
subtitle={subTitle}
|
subtitle={subTitle}
|
||||||
body={
|
body={
|
||||||
!isBuiltin && (
|
<CollectionActions
|
||||||
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} />
|
uri={uri}
|
||||||
)
|
collectionId={collectionId}
|
||||||
|
setShowInfo={setShowInfo}
|
||||||
|
showInfo={showInfo}
|
||||||
|
isBuiltin={isBuiltin}
|
||||||
|
collectionUrls={collectionUrls}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
showInfo &&
|
showInfo &&
|
||||||
|
|
|
@ -16,6 +16,8 @@ import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
makeSelectClaimWasPurchased,
|
makeSelectClaimWasPurchased,
|
||||||
|
doToast,
|
||||||
|
makeSelectUrlsForCollectionId,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
|
@ -110,16 +112,18 @@ export function doSetPlayingUri({
|
||||||
source,
|
source,
|
||||||
pathname,
|
pathname,
|
||||||
commentId,
|
commentId,
|
||||||
|
collectionId,
|
||||||
}: {
|
}: {
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
source?: string,
|
source?: string,
|
||||||
commentId?: string,
|
commentId?: string,
|
||||||
pathname: string,
|
pathname: string,
|
||||||
|
collectionId: string,
|
||||||
}) {
|
}) {
|
||||||
return (dispatch: Dispatch) => {
|
return (dispatch: Dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SET_PLAYING_URI,
|
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;
|
const UPDATE_IS_NIGHT_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
export function doFetchDaemonSettings() {
|
export function doFetchDaemonSettings() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
Lbry.settings_get().then(settings => {
|
Lbry.settings_get().then((settings) => {
|
||||||
analytics.toggleInternal(settings.share_usage_data);
|
analytics.toggleInternal(settings.share_usage_data);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||||
|
@ -32,11 +32,11 @@ export function doFetchDaemonSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doFindFFmpeg() {
|
export function doFindFFmpeg() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED,
|
type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED,
|
||||||
});
|
});
|
||||||
return Lbry.ffmpeg_find().then(done => {
|
return Lbry.ffmpeg_find().then((done) => {
|
||||||
dispatch(doGetDaemonStatus());
|
dispatch(doGetDaemonStatus());
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED,
|
type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED,
|
||||||
|
@ -46,8 +46,8 @@ export function doFindFFmpeg() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doGetDaemonStatus() {
|
export function doGetDaemonStatus() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
return Lbry.status().then(status => {
|
return Lbry.status().then((status) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.DAEMON_STATUS_RECEIVED,
|
type: ACTIONS.DAEMON_STATUS_RECEIVED,
|
||||||
data: {
|
data: {
|
||||||
|
@ -72,7 +72,7 @@ export function doClearDaemonSetting(key) {
|
||||||
key,
|
key,
|
||||||
};
|
};
|
||||||
// not if syncLocked
|
// not if syncLocked
|
||||||
Lbry.settings_clear(clearKey).then(defaultSettings => {
|
Lbry.settings_clear(clearKey).then((defaultSettings) => {
|
||||||
if (SDK_SYNC_KEYS.includes(key)) {
|
if (SDK_SYNC_KEYS.includes(key)) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||||
|
@ -83,7 +83,7 @@ export function doClearDaemonSetting(key) {
|
||||||
dispatch(doWalletReconnect());
|
dispatch(doWalletReconnect());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Lbry.settings_get().then(settings => {
|
Lbry.settings_get().then((settings) => {
|
||||||
analytics.toggleInternal(settings.share_usage_data);
|
analytics.toggleInternal(settings.share_usage_data);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||||
|
@ -108,7 +108,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
||||||
key,
|
key,
|
||||||
value: !value && value !== false ? null : value,
|
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) {
|
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||||
|
@ -121,7 +121,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
||||||
// todo: add sdk reloadsettings() (or it happens automagically?)
|
// todo: add sdk reloadsettings() (or it happens automagically?)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Lbry.settings_get().then(settings => {
|
Lbry.settings_get().then((settings) => {
|
||||||
analytics.toggleInternal(settings.share_usage_data);
|
analytics.toggleInternal(settings.share_usage_data);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||||
|
@ -170,7 +170,7 @@ export function doUpdateIsNight() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doUpdateIsNightAsync() {
|
export function doUpdateIsNightAsync() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
dispatch(doUpdateIsNight());
|
dispatch(doUpdateIsNight());
|
||||||
|
|
||||||
setInterval(() => dispatch(doUpdateIsNight()), UPDATE_IS_NIGHT_INTERVAL);
|
setInterval(() => dispatch(doUpdateIsNight()), UPDATE_IS_NIGHT_INTERVAL);
|
||||||
|
@ -201,8 +201,8 @@ export function doSetDarkTime(value, options) {
|
||||||
|
|
||||||
export function doGetWalletSyncPreference() {
|
export function doGetWalletSyncPreference() {
|
||||||
const SYNC_KEY = 'enable-sync';
|
const SYNC_KEY = 'enable-sync';
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
return Lbry.preference_get({ key: SYNC_KEY }).then(result => {
|
return Lbry.preference_get({ key: SYNC_KEY }).then((result) => {
|
||||||
const enabled = result && result[SYNC_KEY];
|
const enabled = result && result[SYNC_KEY];
|
||||||
if (enabled !== null) {
|
if (enabled !== null) {
|
||||||
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
||||||
|
@ -214,8 +214,8 @@ export function doGetWalletSyncPreference() {
|
||||||
|
|
||||||
export function doSetWalletSyncPreference(pref) {
|
export function doSetWalletSyncPreference(pref) {
|
||||||
const SYNC_KEY = 'enable-sync';
|
const SYNC_KEY = 'enable-sync';
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then(result => {
|
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then((result) => {
|
||||||
const enabled = result && result[SYNC_KEY];
|
const enabled = result && result[SYNC_KEY];
|
||||||
if (enabled !== null) {
|
if (enabled !== null) {
|
||||||
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
||||||
|
@ -226,7 +226,7 @@ export function doSetWalletSyncPreference(pref) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doPushSettingsToPrefs() {
|
export function doPushSettingsToPrefs() {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS,
|
type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS,
|
||||||
|
@ -274,8 +274,8 @@ export function doFetchLanguage(language) {
|
||||||
if (settings.language !== language || (settings.loadedLanguages && !settings.loadedLanguages.includes(language))) {
|
if (settings.language !== language || (settings.loadedLanguages && !settings.loadedLanguages.includes(language))) {
|
||||||
// this should match the behavior/logic in index-web.html
|
// this should match the behavior/logic in index-web.html
|
||||||
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(j => {
|
.then((j) => {
|
||||||
window.i18n_messages[language] = j;
|
window.i18n_messages[language] = j;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
||||||
|
@ -284,7 +284,7 @@ export function doFetchLanguage(language) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_FAILURE,
|
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_FAILURE,
|
||||||
});
|
});
|
||||||
|
@ -324,8 +324,8 @@ export function doSetLanguage(language) {
|
||||||
) {
|
) {
|
||||||
// this should match the behavior/logic in index-web.html
|
// this should match the behavior/logic in index-web.html
|
||||||
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
||||||
.then(r => r.json())
|
.then((r) => r.json())
|
||||||
.then(j => {
|
.then((j) => {
|
||||||
window.i18n_messages[language] = j;
|
window.i18n_messages[language] = j;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
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);
|
window.localStorage.setItem(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE);
|
||||||
dispatch(doSetClientSetting(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE));
|
dispatch(doSetClientSetting(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE));
|
||||||
const languageName = SUPPORTED_LANGUAGES[language] ? SUPPORTED_LANGUAGES[language] : language;
|
const languageName = SUPPORTED_LANGUAGES[language] ? SUPPORTED_LANGUAGES[language] : language;
|
||||||
|
@ -369,7 +369,7 @@ export function doSetAutoLaunch(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
launcher.isEnabled().then(isEnabled => {
|
launcher.isEnabled().then((isEnabled) => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
if (!autoLaunch) {
|
if (!autoLaunch) {
|
||||||
launcher.disable().then(() => {
|
launcher.disable().then(() => {
|
||||||
|
@ -410,7 +410,7 @@ export function doSetAutoLaunch(value) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doSetAppToTrayWhenClosed(value) {
|
export function doSetAppToTrayWhenClosed(value) {
|
||||||
return dispatch => {
|
return (dispatch) => {
|
||||||
window.localStorage.setItem(SETTINGS.TO_TRAY_WHEN_CLOSED, value);
|
window.localStorage.setItem(SETTINGS.TO_TRAY_WHEN_CLOSED, value);
|
||||||
dispatch(doSetClientSetting(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));
|
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,
|
source: action.data.source,
|
||||||
pathname: action.data.pathname,
|
pathname: action.data.pathname,
|
||||||
commentId: action.data.commentId,
|
commentId: action.data.commentId,
|
||||||
|
collectionId: action.data.collectionId,
|
||||||
primaryUri: state.primaryUri,
|
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) => {
|
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
||||||
const { claimId, outpoint, position } = action.data;
|
const { claimId, outpoint, position } = action.data;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -69,7 +69,7 @@ const defaultState = {
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
[SETTINGS.SHOW_MATURE]: false,
|
[SETTINGS.SHOW_MATURE]: false,
|
||||||
[SETTINGS.AUTOPLAY]: true,
|
[SETTINGS.AUTOPLAY_MEDIA]: true,
|
||||||
[SETTINGS.AUTOPLAY_NEXT]: true,
|
[SETTINGS.AUTOPLAY_NEXT]: true,
|
||||||
[SETTINGS.FLOATING_PLAYER]: true,
|
[SETTINGS.FLOATING_PLAYER]: true,
|
||||||
[SETTINGS.AUTO_DOWNLOAD]: 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 selectPlayingUri = createSelector(selectState, (state) => state.playingUri);
|
||||||
export const selectPrimaryUri = createSelector(selectState, (state) => state.primaryUri);
|
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) =>
|
export const makeSelectIsPlaying = (uri: string) =>
|
||||||
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
|
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 {
|
.button--link {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
|
|
|
@ -316,6 +316,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
|
|
||||||
.menu__link {
|
.menu__link {
|
||||||
|
color: var(--color-text);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
@media (max-width: $breakpoint-medium) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
> :first-child {
|
> :first-child {
|
||||||
|
|
|
@ -10134,9 +10134,9 @@ lazy-val@^1.0.4:
|
||||||
yargs "^13.2.2"
|
yargs "^13.2.2"
|
||||||
zstd-codec "^0.1.1"
|
zstd-codec "^0.1.1"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#dc264ec50ca3208d940521bdac0b61352d913c95:
|
lbry-redux@lbryio/lbry-redux#12a2ffc708bed45ba8d5a46620dc3892aaf890f8:
|
||||||
version "0.0.1"
|
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:
|
dependencies:
|
||||||
"@ungap/from-entries" "^0.2.1"
|
"@ungap/from-entries" "^0.2.1"
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
|
|
Loading…
Reference in a new issue