Compare commits
20 commits
master
...
playback-c
Author | SHA1 | Date | |
---|---|---|---|
|
84c44a2103 | ||
|
809136358b | ||
|
bd64d5c9ea | ||
|
73722a4f00 | ||
|
b26255bf53 | ||
|
ec6f9c8a7f | ||
|
10087891f4 | ||
|
db848fd961 | ||
|
9869980e6b | ||
|
7d4cc58def | ||
|
4ff12294a7 | ||
|
6ec25b0f71 | ||
|
3376986c26 | ||
|
f580f5d536 | ||
|
fe01c4764c | ||
|
7b70db4ea7 | ||
|
bc930ac13b | ||
|
47929419ab | ||
|
f7556e5653 | ||
|
15aee9eb4e |
36 changed files with 970 additions and 208 deletions
|
@ -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#e4d0662100a5f4b28bb1bf3cbc1e51b2eebab5b6",
|
"lbry-redux": "lbryio/lbry-redux#aeb1f533b590c0a9a63954beaa52e92d6f0668e3",
|
||||||
"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,7 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { makeSelectClaimForUri, SETTINGS, COLLECTIONS_CONSTS, makeSelectNextUrlForCollectionAndUrl } from 'lbry-redux';
|
import { makeSelectClaimForUri, SETTINGS, COLLECTIONS_CONSTS, makeSelectNextUrlForCollectionAndUrl } from 'lbry-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { makeSelectIsPlayerFloating, makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
|
import {
|
||||||
|
makeSelectIsPlayerFloating,
|
||||||
|
makeSelectNextUnplayedRecommended,
|
||||||
|
selectPlayingUri,
|
||||||
|
} from 'redux/selectors/content';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
import { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
||||||
import AutoplayCountdown from './view';
|
import AutoplayCountdown from './view';
|
||||||
|
@ -15,7 +19,8 @@ const select = (state, props) => {
|
||||||
const { location } = props;
|
const { location } = props;
|
||||||
const { search } = location;
|
const { search } = location;
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
const playingUri = selectPlayingUri(state);
|
||||||
|
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || (playingUri && playingUri.collectionId);
|
||||||
|
|
||||||
let nextRecommendedUri;
|
let nextRecommendedUri;
|
||||||
if (collectionId) {
|
if (collectionId) {
|
||||||
|
|
|
@ -7,6 +7,8 @@ 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 { 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';
|
||||||
|
|
||||||
|
@ -15,10 +17,11 @@ type Props = {
|
||||||
nextRecommendedClaim: ?StreamClaim,
|
nextRecommendedClaim: ?StreamClaim,
|
||||||
nextRecommendedUri: string,
|
nextRecommendedUri: string,
|
||||||
isFloating: boolean,
|
isFloating: boolean,
|
||||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
doSetPlayingUri: ({ uri: ?string, collectionId: ?string }) => void,
|
||||||
doPlayUri: (string) => void,
|
doPlayUri: (string) => void,
|
||||||
modal: { id: string, modalProps: {} },
|
modal: { id: string, modalProps: {} },
|
||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
|
setReplay: (boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function AutoplayCountdown(props: Props) {
|
function AutoplayCountdown(props: Props) {
|
||||||
|
@ -31,6 +34,7 @@ function AutoplayCountdown(props: Props) {
|
||||||
history: { push },
|
history: { push },
|
||||||
modal,
|
modal,
|
||||||
collectionId,
|
collectionId,
|
||||||
|
setReplay,
|
||||||
} = props;
|
} = props;
|
||||||
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
||||||
|
|
||||||
|
@ -54,19 +58,14 @@ function AutoplayCountdown(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const doNavigate = useCallback(() => {
|
const doNavigate = useCallback(() => {
|
||||||
if (!isFloating) {
|
if (!isFloating && navigateUrl) {
|
||||||
if (navigateUrl) {
|
push(navigateUrl);
|
||||||
push(navigateUrl);
|
|
||||||
doSetPlayingUri({ uri: nextRecommendedUri });
|
|
||||||
doPlayUri(nextRecommendedUri);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (nextRecommendedUri) {
|
|
||||||
doSetPlayingUri({ uri: nextRecommendedUri });
|
|
||||||
doPlayUri(nextRecommendedUri);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [navigateUrl, nextRecommendedUri, isFloating, doSetPlayingUri, doPlayUri, push]);
|
if (nextRecommendedUri) {
|
||||||
|
doSetPlayingUri({ uri: nextRecommendedUri, collectionId });
|
||||||
|
doPlayUri(nextRecommendedUri);
|
||||||
|
}
|
||||||
|
}, [isFloating, navigateUrl, nextRecommendedUri, push, doSetPlayingUri, collectionId, doPlayUri]);
|
||||||
|
|
||||||
function shouldPauseAutoplay() {
|
function shouldPauseAutoplay() {
|
||||||
const elm = document.querySelector(`.${CLASSNAME_AUTOPLAY_COUNTDOWN}`);
|
const elm = document.querySelector(`.${CLASSNAME_AUTOPLAY_COUNTDOWN}`);
|
||||||
|
@ -88,26 +87,30 @@ function AutoplayCountdown(props: Props) {
|
||||||
|
|
||||||
// Update countdown timer.
|
// Update countdown timer.
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let interval;
|
if (collectionId) {
|
||||||
if (!timerCanceled && nextRecommendedUri) {
|
doNavigate();
|
||||||
if (isTimerPaused) {
|
} else {
|
||||||
clearInterval(interval);
|
let interval;
|
||||||
setTimer(countdownTime);
|
if (!timerCanceled && nextRecommendedUri) {
|
||||||
} else {
|
if (isTimerPaused) {
|
||||||
interval = setInterval(() => {
|
clearInterval(interval);
|
||||||
const newTime = timer - 1;
|
setTimer(countdownTime);
|
||||||
if (newTime === 0) {
|
} else {
|
||||||
doNavigate();
|
interval = setInterval(() => {
|
||||||
} else {
|
const newTime = timer - 1;
|
||||||
setTimer(timer - 1);
|
if (newTime === 0) {
|
||||||
}
|
doNavigate();
|
||||||
}, 1000);
|
} else {
|
||||||
|
setTimer(timer - 1);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return () => {
|
}, [timer, doNavigate, navigateUrl, push, timerCanceled, isTimerPaused, nextRecommendedUri, collectionId]);
|
||||||
clearInterval(interval);
|
|
||||||
};
|
|
||||||
}, [timer, doNavigate, navigateUrl, push, timerCanceled, isTimerPaused, nextRecommendedUri]);
|
|
||||||
|
|
||||||
if (timerCanceled || !nextRecommendedUri) {
|
if (timerCanceled || !nextRecommendedUri) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -138,6 +141,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);
|
||||||
|
setReplay(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</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,
|
||||||
|
fetchCollectionItems: (string) => void,
|
||||||
|
resolvedList: boolean,
|
||||||
|
playNextUri: string,
|
||||||
|
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,
|
||||||
|
fetchCollectionItems,
|
||||||
|
resolvedList,
|
||||||
|
playNextUri,
|
||||||
|
doSetPlayingUri,
|
||||||
|
doToggleShuffleList,
|
||||||
} = props;
|
} = props;
|
||||||
|
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||||
const isChannel = !incognitoClaim && !contentSigningChannel;
|
const isChannel = !incognitoClaim && !contentSigningChannel;
|
||||||
const { channelName } = parseURI(contentChannelUri);
|
const { channelName } = parseURI(contentChannelUri);
|
||||||
|
@ -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 (resolvedList && doShuffle) {
|
||||||
|
doToggleShuffleList(collectionId);
|
||||||
|
if (playNextUri) {
|
||||||
|
const collectionParams = new URLSearchParams();
|
||||||
|
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||||
|
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
|
||||||
|
setDoShuffle(false);
|
||||||
|
doSetPlayingUri(playNextUri);
|
||||||
|
push(navigateUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [playNextUri, doShuffle, resolvedList, doToggleShuffleList, collectionId, doSetPlayingUri, push]);
|
||||||
|
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -245,10 +277,28 @@ function ClaimMenuList(props: Props) {
|
||||||
{/* COLLECTION OPERATIONS */}
|
{/* COLLECTION OPERATIONS */}
|
||||||
{collectionId && isCollectionClaim ? (
|
{collectionId && isCollectionClaim ? (
|
||||||
<>
|
<>
|
||||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
<MenuItem
|
||||||
<div className="menu__link">
|
className="comment__menu-option"
|
||||||
|
onSelect={() => {
|
||||||
|
if (!resolvedList) fetchItems();
|
||||||
|
push(`/$/${PAGES.LIST}/${collectionId}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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,33 @@ import {
|
||||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { selectListShuffle } from 'redux/selectors/content';
|
||||||
|
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||||
import CollectionActions from './view';
|
import CollectionActions from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
const collectionId = props.collectionId;
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
const shuffleList = selectListShuffle(state);
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||||
myChannels: selectMyChannelClaims(state),
|
const playNextUri = shuffle && shuffle[0];
|
||||||
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
|
||||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
return {
|
||||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
});
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
|
myChannels: selectMyChannelClaims(state),
|
||||||
|
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
|
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||||
|
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
|
||||||
|
playNextUri,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
|
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||||
|
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CollectionActions);
|
export default connect(select, perform)(CollectionActions);
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { useHistory } from 'react-router';
|
||||||
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
|
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { ENABLE_FILE_REACTIONS } from 'config';
|
import { ENABLE_FILE_REACTIONS } from 'config';
|
||||||
|
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -24,6 +26,10 @@ type Props = {
|
||||||
showInfo: boolean,
|
showInfo: boolean,
|
||||||
setShowInfo: (boolean) => void,
|
setShowInfo: (boolean) => void,
|
||||||
collectionHasEdits: boolean,
|
collectionHasEdits: boolean,
|
||||||
|
doToggleShuffleList: (string) => void,
|
||||||
|
playNextUri: string,
|
||||||
|
doSetPlayingUri: (string) => void,
|
||||||
|
isBuiltin: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function CollectionActions(props: Props) {
|
function CollectionActions(props: Props) {
|
||||||
|
@ -37,61 +43,92 @@ function CollectionActions(props: Props) {
|
||||||
showInfo,
|
showInfo,
|
||||||
setShowInfo,
|
setShowInfo,
|
||||||
collectionHasEdits,
|
collectionHasEdits,
|
||||||
|
doToggleShuffleList,
|
||||||
|
playNextUri,
|
||||||
|
doSetPlayingUri,
|
||||||
|
isBuiltin,
|
||||||
} = props;
|
} = props;
|
||||||
|
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const webShareable = true; // collections have cost?
|
const webShareable = true; // collections have cost?
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (playNextUri && doShuffle) {
|
||||||
|
const collectionParams = new URLSearchParams();
|
||||||
|
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||||
|
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
|
||||||
|
setDoShuffle(false);
|
||||||
|
doSetPlayingUri(playNextUri);
|
||||||
|
push(navigateUrl);
|
||||||
|
}
|
||||||
|
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]);
|
||||||
|
|
||||||
const lhsSection = (
|
const lhsSection = (
|
||||||
<>
|
<>
|
||||||
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
|
<Button
|
||||||
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
className="button--file-action"
|
||||||
{/* TODO Add ClaimRepostButton component */}
|
icon={ICONS.SHUFFLE}
|
||||||
{uri && (
|
label={__('Shuffle Play')}
|
||||||
<Button
|
title={__('Shuffle Play')}
|
||||||
className="button--file-action"
|
onClick={() => {
|
||||||
icon={ICONS.SHARE}
|
doToggleShuffleList(collectionId);
|
||||||
label={__('Share')}
|
setDoShuffle(true);
|
||||||
title={__('Share')}
|
}}
|
||||||
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
|
/>
|
||||||
/>
|
{!isBuiltin && (
|
||||||
|
<>
|
||||||
|
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
|
||||||
|
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
||||||
|
{/* TODO Add ClaimRepostButton component */}
|
||||||
|
{uri && (
|
||||||
|
<Button
|
||||||
|
className="button--file-action"
|
||||||
|
icon={ICONS.SHARE}
|
||||||
|
label={__('Share')}
|
||||||
|
title={__('Share')}
|
||||||
|
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const rhsSection = (
|
const rhsSection = (
|
||||||
<>
|
<>
|
||||||
{isMyCollection && (
|
{!isBuiltin &&
|
||||||
<Button
|
(isMyCollection ? (
|
||||||
title={uri ? __('Update') : __('Publish')}
|
<>
|
||||||
label={uri ? __('Update') : __('Publish')}
|
<Button
|
||||||
className={classnames('button--file-action')}
|
title={uri ? __('Update') : __('Publish')}
|
||||||
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
label={uri ? __('Update') : __('Publish')}
|
||||||
icon={ICONS.PUBLISH}
|
className={classnames('button--file-action')}
|
||||||
iconColor={collectionHasEdits && 'red'}
|
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
||||||
iconSize={18}
|
icon={ICONS.PUBLISH}
|
||||||
disabled={claimIsPending}
|
iconColor={collectionHasEdits && 'red'}
|
||||||
/>
|
iconSize={18}
|
||||||
)}
|
disabled={claimIsPending}
|
||||||
{isMyCollection && (
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={classnames('button--file-action')}
|
className={classnames('button--file-action')}
|
||||||
title={__('Delete List')}
|
title={__('Delete List')}
|
||||||
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
||||||
icon={ICONS.DELETE}
|
icon={ICONS.DELETE}
|
||||||
iconSize={18}
|
iconSize={18}
|
||||||
description={__('Delete List')}
|
description={__('Delete List')}
|
||||||
disabled={claimIsPending}
|
disabled={claimIsPending}
|
||||||
/>
|
/>
|
||||||
)}
|
</>
|
||||||
{!isMyCollection && (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
title={__('Report content')}
|
title={__('Report content')}
|
||||||
className="button--file-action"
|
className="button--file-action"
|
||||||
icon={ICONS.REPORT}
|
icon={ICONS.REPORT}
|
||||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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>
|
<>
|
||||||
<Icon
|
<span className="file-page__recommended-collection__row">
|
||||||
icon={(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
<Icon
|
||||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
icon={
|
||||||
className="icon--margin-right" />
|
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
{collectionName}
|
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||||
</span>
|
ICONS.STACK
|
||||||
|
}
|
||||||
|
className="icon--margin-right"
|
||||||
|
/>
|
||||||
|
{collectionName}
|
||||||
|
</span>
|
||||||
|
<span className="file-page__recommended-collection__row">
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
title="Loop"
|
||||||
|
icon={ICONS.REPEAT}
|
||||||
|
iconColor={loop && 'blue'}
|
||||||
|
className="button--file-action"
|
||||||
|
onClick={() => doToggleLoopList(id, !loop)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
button="alt"
|
||||||
|
title="Shuffle"
|
||||||
|
icon={ICONS.SHUFFLE}
|
||||||
|
iconColor={shuffle && 'blue'}
|
||||||
|
className="button--file-action"
|
||||||
|
onClick={() => doToggleShuffleList(url, id, !shuffle)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
titleActions={
|
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
|
||||||
|
|
|
@ -2401,4 +2401,27 @@ export const icons = {
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
|
[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>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,27 +23,34 @@ 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) => {
|
||||||
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
const { search } = props.location;
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
const urlParams = new URLSearchParams(search);
|
||||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
|
||||||
playingUri: selectPlayingUri(state),
|
return {
|
||||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
playingUri: selectPlayingUri(state),
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||||
});
|
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||||
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||||
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
|
collectionId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
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(() => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ $transition-duration: 300ms;
|
||||||
// Classnames must line up with classes in classes.js
|
// Classnames must line up with classes in classes.js
|
||||||
.ff-container {
|
.ff-container {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
|
||||||
.ff-image {
|
.ff-image {
|
||||||
z-index: $base-zindex;
|
z-index: $base-zindex;
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
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, doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
||||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
import { makeSelectContentPositionForUri, selectPlayingUri, makeSelectIsPlayerFloating } 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) => {
|
||||||
|
@ -18,6 +26,15 @@ const select = (state, props) => {
|
||||||
// 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(props.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 playNextUri;
|
||||||
|
let playPreviousUri;
|
||||||
|
if (collectionId) {
|
||||||
|
playNextUri = makeSelectNextUrlForCollectionAndUrl(collectionId, props.uri)(state);
|
||||||
|
playPreviousUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, props.uri)(state);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
autoplayIfEmbedded: Boolean(autoplay),
|
autoplayIfEmbedded: Boolean(autoplay),
|
||||||
|
@ -33,6 +50,11 @@ const select = (state, props) => {
|
||||||
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,
|
||||||
|
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
||||||
|
isFloating: makeSelectIsPlayerFloating(props.location)(state),
|
||||||
|
collectionId,
|
||||||
|
playNextUri,
|
||||||
|
playPreviousUri,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -45,7 +67,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)),
|
||||||
|
doSetPlayingUri: (uri, collectionId) => dispatch(doSetPlayingUri({ uri, collectionId })),
|
||||||
|
doPlayUri: (uri) => dispatch(doPlayUri(uri)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(VideoViewer));
|
export default withRouter(connect(select, perform)(VideoViewer));
|
||||||
|
|
29
ui/component/viewers/videoViewer/internal/autoplay-next.js
Normal file
29
ui/component/viewers/videoViewer/internal/autoplay-next.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// @flow
|
||||||
|
import type { Player } from './videojs';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
class AutoplayNextButton extends videojs.getComponent('Button') {
|
||||||
|
constructor(player, options = {}, autoplay) {
|
||||||
|
super(player, options, autoplay);
|
||||||
|
this.addClass(autoplay ? 'vjs-button--autoplay-next--active' : 'vjs-button--autoplay-next');
|
||||||
|
this.controlText(autoplay ? 'Autoplay Next On' : 'Autoplay Next Off');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addAutoplayNextButton(player: Player, toggleAutoplayNext: () => void, autoplay: boolean) {
|
||||||
|
const controlBar = player.getChild('controlBar');
|
||||||
|
|
||||||
|
const autoplayButton = new AutoplayNextButton(
|
||||||
|
player,
|
||||||
|
{
|
||||||
|
name: 'AutoplayNextButton',
|
||||||
|
text: __('Autoplay Next'),
|
||||||
|
clickHandler: () => {
|
||||||
|
toggleAutoplayNext();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
autoplay
|
||||||
|
);
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
const theaterMode = new TheaterModeButton(player, {
|
||||||
|
name: 'TheaterModeButton',
|
||||||
text: __('Theater mode'),
|
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,
|
||||||
|
setStartPlayPrevious: (boolean) => void,
|
||||||
|
setStartPlayNext: (boolean) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type VideoJSOptions = {
|
// type VideoJSOptions = {
|
||||||
|
@ -103,6 +108,20 @@ 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 ZERO_KEYCODE = 48;
|
||||||
|
const ONE_KEYCODE = 49;
|
||||||
|
const TWO_KEYCODE = 50;
|
||||||
|
const THREE_KEYCODE = 51;
|
||||||
|
const FOUR_KEYCODE = 52;
|
||||||
|
const FIVE_KEYCODE = 53;
|
||||||
|
const SIX_KEYCODE = 54;
|
||||||
|
const SEVEN_KEYCODE = 55;
|
||||||
|
const EIGHT_KEYCODE = 56;
|
||||||
|
const NINE_KEYCODE = 57;
|
||||||
|
|
||||||
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 +204,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 +217,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
userId,
|
userId,
|
||||||
// allowPreRoll,
|
// allowPreRoll,
|
||||||
shareTelemetry,
|
shareTelemetry,
|
||||||
|
replay,
|
||||||
|
videoTheaterMode,
|
||||||
|
setStartPlayPrevious,
|
||||||
|
setStartPlayNext,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [reload, setReload] = useState('initial');
|
const [reload, setReload] = useState('initial');
|
||||||
|
@ -323,13 +347,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);
|
||||||
|
@ -387,6 +409,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 === P_KEYCODE) setStartPlayPrevious(true);
|
||||||
|
if (e.keyCode === N_KEYCODE) setStartPlayNext(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSingleKeyActions(e: KeyboardEvent) {
|
function handleSingleKeyActions(e: KeyboardEvent) {
|
||||||
|
@ -397,19 +421,37 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
if (e.keyCode === VOLUME_UP_KEYCODE) volumeUp();
|
if (e.keyCode === VOLUME_UP_KEYCODE) volumeUp();
|
||||||
if (e.keyCode === VOLUME_DOWN_KEYCODE) volumeDown();
|
if (e.keyCode === VOLUME_DOWN_KEYCODE) volumeDown();
|
||||||
if (e.keyCode === THEATER_MODE_KEYCODE) toggleTheaterMode();
|
if (e.keyCode === THEATER_MODE_KEYCODE) toggleTheaterMode();
|
||||||
if (e.keyCode === SEEK_FORWARD_KEYCODE) seekVideo(SEEK_STEP);
|
if (e.keyCode === SEEK_FORWARD_KEYCODE) seekVideo(SEEK_STEP, false);
|
||||||
if (e.keyCode === SEEK_BACKWARD_KEYCODE) seekVideo(-SEEK_STEP);
|
if (e.keyCode === SEEK_BACKWARD_KEYCODE) seekVideo(-SEEK_STEP, false);
|
||||||
if (e.keyCode === SEEK_FORWARD_KEYCODE_5) seekVideo(SEEK_STEP_5);
|
if (e.keyCode === SEEK_FORWARD_KEYCODE_5) seekVideo(SEEK_STEP_5, false);
|
||||||
if (e.keyCode === SEEK_BACKWARD_KEYCODE_5) seekVideo(-SEEK_STEP_5);
|
if (e.keyCode === SEEK_BACKWARD_KEYCODE_5) seekVideo(-SEEK_STEP_5, false);
|
||||||
|
if (e.keyCode === ZERO_KEYCODE) seekVideo(0, true);
|
||||||
|
if (e.keyCode === ONE_KEYCODE) seekVideo(1, true);
|
||||||
|
if (e.keyCode === TWO_KEYCODE) seekVideo(2, true);
|
||||||
|
if (e.keyCode === THREE_KEYCODE) seekVideo(3, true);
|
||||||
|
if (e.keyCode === FOUR_KEYCODE) seekVideo(4, true);
|
||||||
|
if (e.keyCode === FIVE_KEYCODE) seekVideo(5, true);
|
||||||
|
if (e.keyCode === SIX_KEYCODE) seekVideo(6, true);
|
||||||
|
if (e.keyCode === SEVEN_KEYCODE) seekVideo(7, true);
|
||||||
|
if (e.keyCode === EIGHT_KEYCODE) seekVideo(8, true);
|
||||||
|
if (e.keyCode === NINE_KEYCODE) seekVideo(9, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function seekVideo(stepSize: number) {
|
function seekVideo(stepSize: number, numberedStep: boolean) {
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||||
if (!videoNode || !player) return;
|
if (!videoNode || !player) return;
|
||||||
const duration = videoNode.duration;
|
const duration = videoNode.duration;
|
||||||
|
const numberedStepSize = duration / 10;
|
||||||
const currentTime = videoNode.currentTime;
|
const currentTime = videoNode.currentTime;
|
||||||
const newDuration = currentTime + stepSize;
|
|
||||||
|
let newDuration;
|
||||||
|
if (numberedStep) {
|
||||||
|
newDuration = numberedStepSize * stepSize;
|
||||||
|
} else {
|
||||||
|
newDuration = currentTime + stepSize;
|
||||||
|
}
|
||||||
|
|
||||||
if (newDuration < 0) {
|
if (newDuration < 0) {
|
||||||
videoNode.currentTime = 0;
|
videoNode.currentTime = 0;
|
||||||
} else if (newDuration > duration) {
|
} else if (newDuration > duration) {
|
||||||
|
@ -417,7 +459,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
} else {
|
} else {
|
||||||
videoNode.currentTime = newDuration;
|
videoNode.currentTime = newDuration;
|
||||||
}
|
}
|
||||||
OVERLAY.showSeekedOverlay(player, Math.abs(stepSize), stepSize > 0);
|
|
||||||
|
if (!numberedStep) {
|
||||||
|
OVERLAY.showSeekedOverlay(player, Math.abs(stepSize), stepSize > 0);
|
||||||
|
}
|
||||||
player.userActive(true);
|
player.userActive(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,7 +625,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
|
||||||
|
@ -601,6 +647,41 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
return vjs;
|
return vjs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 controlBar = player.getChild('controlBar');
|
||||||
|
const autoplayButton = controlBar.getChild('AutoplayNextButton');
|
||||||
|
|
||||||
|
if (autoplaySetting) {
|
||||||
|
autoplayButton.removeClass('vjs-button--autoplay-next');
|
||||||
|
autoplayButton.addClass('vjs-button--autoplay-next--active');
|
||||||
|
autoplayButton.controlText('Autoplay Next On');
|
||||||
|
} else {
|
||||||
|
autoplayButton.removeClass('vjs-button--autoplay-next--active');
|
||||||
|
autoplayButton.addClass('vjs-button--autoplay-next');
|
||||||
|
autoplayButton.controlText('Autoplay Next Off');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [autoplaySetting]);
|
||||||
|
|
||||||
// This lifecycle hook is only called once (on mount), or when `isAudio` changes.
|
// This lifecycle hook is only called once (on mount), or when `isAudio` changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -47,11 +52,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,
|
||||||
|
doSetPlayingUri: (string, string) => void,
|
||||||
|
doPlayUri: (string) => void,
|
||||||
|
playNextUri: string,
|
||||||
|
playPreviousUri: string,
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
userId: number,
|
userId: number,
|
||||||
homepageData?: { [string]: HomepageCat },
|
homepageData?: { [string]: HomepageCat },
|
||||||
shareTelemetry: boolean,
|
shareTelemetry: boolean,
|
||||||
|
videoTheaterMode: boolean,
|
||||||
|
collectionId: string,
|
||||||
|
isFloating: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -81,11 +94,19 @@ function VideoViewer(props: Props) {
|
||||||
clearPosition,
|
clearPosition,
|
||||||
desktopPlayStartTime,
|
desktopPlayStartTime,
|
||||||
toggleVideoTheaterMode,
|
toggleVideoTheaterMode,
|
||||||
|
toggleAutoplayNext,
|
||||||
setVideoPlaybackRate,
|
setVideoPlaybackRate,
|
||||||
|
doSetPlayingUri,
|
||||||
|
doPlayUri,
|
||||||
|
playNextUri,
|
||||||
|
playPreviousUri,
|
||||||
homepageData,
|
homepageData,
|
||||||
authenticated,
|
authenticated,
|
||||||
userId,
|
userId,
|
||||||
shareTelemetry,
|
shareTelemetry,
|
||||||
|
videoTheaterMode,
|
||||||
|
collectionId,
|
||||||
|
isFloating,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||||
|
@ -95,8 +116,10 @@ function VideoViewer(props: Props) {
|
||||||
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||||
const {
|
const {
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
|
push,
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
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 +131,26 @@ 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 [startPlayNext, setStartPlayNext] = useState(false);
|
||||||
|
const [startPlayPrevious, setStartPlayPrevious] = useState(false);
|
||||||
|
const [videoNode, setVideoNode] = useState(false);
|
||||||
|
|
||||||
|
const getNavigateUrl = React.useCallback(
|
||||||
|
(playUri: string) => {
|
||||||
|
let navigateUrl;
|
||||||
|
if (playUri) {
|
||||||
|
navigateUrl = formatLbryUrlForWeb(playUri);
|
||||||
|
if (collectionId) {
|
||||||
|
const collectionParams = new URLSearchParams();
|
||||||
|
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||||
|
navigateUrl = navigateUrl + `?` + collectionParams.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return navigateUrl;
|
||||||
|
},
|
||||||
|
[collectionId]
|
||||||
|
);
|
||||||
|
|
||||||
// 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(() => {
|
||||||
|
@ -126,6 +169,51 @@ function VideoViewer(props: Props) {
|
||||||
};
|
};
|
||||||
}, [embedded, videoPlaybackRate]);
|
}, [embedded, videoPlaybackRate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startPlayNext) {
|
||||||
|
const navigateUrl = getNavigateUrl(playNextUri);
|
||||||
|
if (!isFloating && navigateUrl) {
|
||||||
|
push(navigateUrl);
|
||||||
|
}
|
||||||
|
if (playNextUri) {
|
||||||
|
doSetPlayingUri(playNextUri, collectionId);
|
||||||
|
doPlayUri(playNextUri);
|
||||||
|
}
|
||||||
|
setStartPlayNext(false);
|
||||||
|
}
|
||||||
|
if (videoNode) {
|
||||||
|
const currentTime = videoNode.currentTime;
|
||||||
|
|
||||||
|
if (startPlayPrevious) {
|
||||||
|
if (currentTime > 5) {
|
||||||
|
videoNode.currentTime = 0;
|
||||||
|
} else {
|
||||||
|
const navigateUrl = getNavigateUrl(playPreviousUri);
|
||||||
|
if (!isFloating && navigateUrl) {
|
||||||
|
push(navigateUrl);
|
||||||
|
}
|
||||||
|
if (playPreviousUri) {
|
||||||
|
doSetPlayingUri(playPreviousUri, collectionId);
|
||||||
|
doPlayUri(playPreviousUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setStartPlayPrevious(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isFloating,
|
||||||
|
push,
|
||||||
|
doSetPlayingUri,
|
||||||
|
playNextUri,
|
||||||
|
doPlayUri,
|
||||||
|
startPlayNext,
|
||||||
|
collectionId,
|
||||||
|
getNavigateUrl,
|
||||||
|
videoNode,
|
||||||
|
startPlayPrevious,
|
||||||
|
playPreviousUri,
|
||||||
|
]);
|
||||||
|
|
||||||
function doTrackingBuffered(e: Event, data: any) {
|
function doTrackingBuffered(e: Event, data: any) {
|
||||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
||||||
data.playerPoweredBy = response.headers.get('x-powered-by');
|
data.playerPoweredBy = response.headers.get('x-powered-by');
|
||||||
|
@ -152,28 +240,41 @@ function VideoViewer(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onEnded = React.useCallback(() => {
|
React.useEffect(() => {
|
||||||
analytics.videoIsPlaying(false);
|
if (ended) {
|
||||||
|
analytics.videoIsPlaying(false);
|
||||||
|
|
||||||
if (adUrl) {
|
if (adUrl) {
|
||||||
setAdUrl(null);
|
setAdUrl(null);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embedded) {
|
||||||
|
setIsEndededEmbed(true);
|
||||||
|
} else if (autoplaySetting) {
|
||||||
|
setShowAutoplayCountdown(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPosition(uri);
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
if (embedded) {
|
embedded,
|
||||||
setIsEndededEmbed(true);
|
setIsEndededEmbed,
|
||||||
} else if (autoplaySetting) {
|
autoplaySetting,
|
||||||
setShowAutoplayCountdown(true);
|
setShowAutoplayCountdown,
|
||||||
}
|
adUrl,
|
||||||
|
setAdUrl,
|
||||||
clearPosition(uri);
|
clearPosition,
|
||||||
}, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl, clearPosition, uri]);
|
uri,
|
||||||
|
ended,
|
||||||
|
]);
|
||||||
|
|
||||||
function onPlay(player) {
|
function onPlay(player) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
setShowAutoplayCountdown(false);
|
setShowAutoplayCountdown(false);
|
||||||
setIsEndededEmbed(false);
|
setIsEndededEmbed(false);
|
||||||
|
setReplay(false);
|
||||||
analytics.videoIsPlaying(true, player);
|
analytics.videoIsPlaying(true, player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,12 +304,19 @@ function VideoViewer(props: Props) {
|
||||||
playerReadyDependencyList.push(desktopPlayStartTime);
|
playerReadyDependencyList.push(desktopPlayStartTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPlayerReady = useCallback((player: Player) => {
|
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, () => setStartPlayNext(true));
|
||||||
|
addPlayPreviousButton(player, () => setStartPlayPrevious(true));
|
||||||
|
} else {
|
||||||
|
addAutoplayNextButton(player, toggleAutoplayNext, autoplaySetting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldPlay = !embedded || autoplayIfEmbedded;
|
const shouldPlay = !embedded || autoplayIfEmbedded;
|
||||||
|
@ -244,7 +352,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 +393,7 @@ function VideoViewer(props: Props) {
|
||||||
})}
|
})}
|
||||||
onContextMenu={stopContextMenu}
|
onContextMenu={stopContextMenu}
|
||||||
>
|
>
|
||||||
{showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
|
{showAutoplayCountdown && <AutoplayCountdown uri={uri} setReplay={setReplay} />}
|
||||||
{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 +440,15 @@ function VideoViewer(props: Props) {
|
||||||
startMuted={autoplayIfEmbedded}
|
startMuted={autoplayIfEmbedded}
|
||||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||||
autoplay={!embedded || autoplayIfEmbedded}
|
autoplay={!embedded || autoplayIfEmbedded}
|
||||||
|
autoplaySetting={autoplaySetting}
|
||||||
claimId={claimId}
|
claimId={claimId}
|
||||||
userId={userId}
|
userId={userId}
|
||||||
allowPreRoll={!embedded && !authenticated}
|
allowPreRoll={!embedded && !authenticated}
|
||||||
shareTelemetry={shareTelemetry}
|
shareTelemetry={shareTelemetry}
|
||||||
|
replay={replay}
|
||||||
|
videoTheaterMode={videoTheaterMode}
|
||||||
|
setStartPlayNext={setStartPlayNext}
|
||||||
|
setStartPlayPrevious={setStartPlayPrevious}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</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';
|
||||||
|
|
|
@ -169,3 +169,6 @@ export const CONTENT = 'Content';
|
||||||
export const STAR = 'star';
|
export const STAR = 'star';
|
||||||
export const MUSIC = 'MusicCategory';
|
export const MUSIC = 'MusicCategory';
|
||||||
export const BADGE_MOD = 'BadgeMod';
|
export const BADGE_MOD = 'BadgeMod';
|
||||||
|
export const REPLAY = 'Replay';
|
||||||
|
export const REPEAT = 'Repeat';
|
||||||
|
export const SHUFFLE = 'Shuffle';
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,18 +112,26 @@ export default function CollectionPage(props: Props) {
|
||||||
title={
|
title={
|
||||||
<span>
|
<span>
|
||||||
<Icon
|
<Icon
|
||||||
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
icon={
|
||||||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||||
className="icon--margin-right" />
|
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||||
|
ICONS.STACK
|
||||||
|
}
|
||||||
|
className="icon--margin-right"
|
||||||
|
/>
|
||||||
{claim ? claim.value.title || claim.name : collection && collection.name}
|
{claim ? claim.value.title || claim.name : collection && collection.name}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
titleActions={titleActions}
|
titleActions={titleActions}
|
||||||
subtitle={subTitle}
|
subtitle={subTitle}
|
||||||
body={
|
body={
|
||||||
!isBuiltin && (
|
<CollectionActions
|
||||||
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} />
|
uri={uri}
|
||||||
)
|
collectionId={collectionId}
|
||||||
|
setShowInfo={setShowInfo}
|
||||||
|
showInfo={showInfo}
|
||||||
|
isBuiltin={isBuiltin}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
showInfo &&
|
showInfo &&
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
@ -385,7 +385,7 @@ export function doSetAutoLaunch(value) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (value === true) {
|
} else if (value === true) {
|
||||||
launcher.isEnabled().then(function(isEnabled) {
|
launcher.isEnabled().then(function (isEnabled) {
|
||||||
if (!isEnabled) {
|
if (!isEnabled) {
|
||||||
launcher.enable().then(() => {
|
launcher.enable().then(() => {
|
||||||
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, true));
|
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, true));
|
||||||
|
@ -396,7 +396,7 @@ export function doSetAutoLaunch(value) {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// value = false
|
// value = false
|
||||||
launcher.isEnabled().then(function(isEnabled) {
|
launcher.isEnabled().then(function (isEnabled) {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
launcher.disable().then(() => {
|
launcher.disable().then(() => {
|
||||||
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, false));
|
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, false));
|
||||||
|
@ -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)(state);
|
||||||
|
|
||||||
|
dispatch(doSetClientSetting(SETTINGS.AUTOPLAY, !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 {
|
||||||
|
|
|
@ -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,78 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vjs-button--autoplay-next.vjs-button {
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
display: block;
|
||||||
|
order: 1;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-left'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='8' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
// Need to repeat these styles because of video.js weirdness
|
||||||
|
// see: https://github.com/lbryio/lbry-desktop/pull/5549#discussion_r580406932
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-left'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='8' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-button--autoplay-next--active.vjs-button {
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
display: block;
|
||||||
|
order: 1;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-right'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='16' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus:not(:focus-visible) {
|
||||||
|
// Need to repeat these styles because of video.js weirdness
|
||||||
|
// see: https://github.com/lbryio/lbry-desktop/pull/5549#discussion_r580406932
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-toggle-right'%3e%3crect x='1' y='5' width='22' height='14' rx='7' ry='7'%3e%3c/rect%3e%3ccircle cx='16' cy='12' r='3'%3e%3c/circle%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vjs-button--play-previous.vjs-button {
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
display: block;
|
||||||
|
order: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
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) {
|
||||||
|
display: block;
|
||||||
|
order: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
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--play-next.vjs-button {
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
display: block;
|
||||||
|
order: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
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) {
|
||||||
|
display: block;
|
||||||
|
order: 0;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.button--link {
|
.button--link {
|
||||||
color: var(--color-link);
|
color: var(--color-link);
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
|
|
|
@ -314,6 +314,7 @@ $thumbnailWidthSmall: 1rem;
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
|
|
||||||
.menu__link {
|
.menu__link {
|
||||||
|
color: var(--color-text);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -10129,9 +10129,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#e4d0662100a5f4b28bb1bf3cbc1e51b2eebab5b6:
|
lbry-redux@lbryio/lbry-redux#aeb1f533b590c0a9a63954beaa52e92d6f0668e3:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/e4d0662100a5f4b28bb1bf3cbc1e51b2eebab5b6"
|
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/aeb1f533b590c0a9a63954beaa52e92d6f0668e3"
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
|
Loading…
Add table
Reference in a new issue