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",
|
||||
"json-loader": "^0.5.4",
|
||||
"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",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, SETTINGS, COLLECTIONS_CONSTS, makeSelectNextUrlForCollectionAndUrl } from 'lbry-redux';
|
||||
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 { doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
||||
import AutoplayCountdown from './view';
|
||||
|
@ -15,7 +19,8 @@ const select = (state, props) => {
|
|||
const { location } = props;
|
||||
const { search } = location;
|
||||
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;
|
||||
if (collectionId) {
|
||||
|
|
|
@ -7,6 +7,8 @@ import { formatLbryUrlForWeb } from 'util/url';
|
|||
import { withRouter } from 'react-router';
|
||||
import debounce from 'util/debounce';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 150;
|
||||
const CLASSNAME_AUTOPLAY_COUNTDOWN = 'autoplay-countdown';
|
||||
|
||||
|
@ -15,10 +17,11 @@ type Props = {
|
|||
nextRecommendedClaim: ?StreamClaim,
|
||||
nextRecommendedUri: string,
|
||||
isFloating: boolean,
|
||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||
doSetPlayingUri: ({ uri: ?string, collectionId: ?string }) => void,
|
||||
doPlayUri: (string) => void,
|
||||
modal: { id: string, modalProps: {} },
|
||||
collectionId?: string,
|
||||
setReplay: (boolean) => void,
|
||||
};
|
||||
|
||||
function AutoplayCountdown(props: Props) {
|
||||
|
@ -31,6 +34,7 @@ function AutoplayCountdown(props: Props) {
|
|||
history: { push },
|
||||
modal,
|
||||
collectionId,
|
||||
setReplay,
|
||||
} = props;
|
||||
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
|
||||
|
||||
|
@ -54,19 +58,14 @@ function AutoplayCountdown(props: Props) {
|
|||
}
|
||||
|
||||
const doNavigate = useCallback(() => {
|
||||
if (!isFloating) {
|
||||
if (navigateUrl) {
|
||||
push(navigateUrl);
|
||||
doSetPlayingUri({ uri: nextRecommendedUri });
|
||||
doPlayUri(nextRecommendedUri);
|
||||
}
|
||||
} else {
|
||||
if (nextRecommendedUri) {
|
||||
doSetPlayingUri({ uri: nextRecommendedUri });
|
||||
doPlayUri(nextRecommendedUri);
|
||||
}
|
||||
if (!isFloating && navigateUrl) {
|
||||
push(navigateUrl);
|
||||
}
|
||||
}, [navigateUrl, nextRecommendedUri, isFloating, doSetPlayingUri, doPlayUri, push]);
|
||||
if (nextRecommendedUri) {
|
||||
doSetPlayingUri({ uri: nextRecommendedUri, collectionId });
|
||||
doPlayUri(nextRecommendedUri);
|
||||
}
|
||||
}, [isFloating, navigateUrl, nextRecommendedUri, push, doSetPlayingUri, collectionId, doPlayUri]);
|
||||
|
||||
function shouldPauseAutoplay() {
|
||||
const elm = document.querySelector(`.${CLASSNAME_AUTOPLAY_COUNTDOWN}`);
|
||||
|
@ -88,26 +87,30 @@ function AutoplayCountdown(props: Props) {
|
|||
|
||||
// Update countdown timer.
|
||||
React.useEffect(() => {
|
||||
let interval;
|
||||
if (!timerCanceled && nextRecommendedUri) {
|
||||
if (isTimerPaused) {
|
||||
clearInterval(interval);
|
||||
setTimer(countdownTime);
|
||||
} else {
|
||||
interval = setInterval(() => {
|
||||
const newTime = timer - 1;
|
||||
if (newTime === 0) {
|
||||
doNavigate();
|
||||
} else {
|
||||
setTimer(timer - 1);
|
||||
}
|
||||
}, 1000);
|
||||
if (collectionId) {
|
||||
doNavigate();
|
||||
} else {
|
||||
let interval;
|
||||
if (!timerCanceled && nextRecommendedUri) {
|
||||
if (isTimerPaused) {
|
||||
clearInterval(interval);
|
||||
setTimer(countdownTime);
|
||||
} else {
|
||||
interval = setInterval(() => {
|
||||
const newTime = timer - 1;
|
||||
if (newTime === 0) {
|
||||
doNavigate();
|
||||
} else {
|
||||
setTimer(timer - 1);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [timer, doNavigate, navigateUrl, push, timerCanceled, isTimerPaused, nextRecommendedUri]);
|
||||
}, [timer, doNavigate, navigateUrl, push, timerCanceled, isTimerPaused, nextRecommendedUri, collectionId]);
|
||||
|
||||
if (timerCanceled || !nextRecommendedUri) {
|
||||
return null;
|
||||
|
@ -138,6 +141,15 @@ function AutoplayCountdown(props: Props) {
|
|||
<Button label={__('Cancel')} button="link" onClick={() => setTimerCanceled(true)} />
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
label={__('Replay?')}
|
||||
button="link"
|
||||
iconRight={ICONS.REPLAY}
|
||||
onClick={() => {
|
||||
setTimerCanceled(true);
|
||||
setReplay(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,8 @@ import {
|
|||
COLLECTIONS_CONSTS,
|
||||
makeSelectEditedCollectionForId,
|
||||
makeSelectClaimIsMine,
|
||||
doFetchItemsInCollection,
|
||||
makeSelectUrlsForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { doChannelMute, doChannelUnmute } from 'redux/actions/blocked';
|
||||
|
@ -28,16 +30,23 @@ import { doToast } from 'redux/actions/notifications';
|
|||
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
|
||||
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||
import ClaimPreview from './view';
|
||||
import fs from 'fs';
|
||||
|
||||
const select = (state, props) => {
|
||||
const claim = makeSelectClaimForUri(props.uri, false)(state);
|
||||
const collectionId = props.collectionId;
|
||||
const resolvedList = makeSelectUrlsForCollectionId(collectionId)(state);
|
||||
const repostedClaim = claim && claim.reposted_claim;
|
||||
const contentClaim = repostedClaim || claim;
|
||||
const contentSigningChannel = contentClaim && contentClaim.signing_channel;
|
||||
const contentPermanentUri = contentClaim && contentClaim.permanent_url;
|
||||
const contentChannelUri = (contentSigningChannel && contentSigningChannel.permanent_url) || contentPermanentUri;
|
||||
const shuffleList = selectListShuffle(state);
|
||||
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||
const playNextUri = shuffle && shuffle[0];
|
||||
|
||||
return {
|
||||
claim,
|
||||
|
@ -60,10 +69,12 @@ const select = (state, props) => {
|
|||
isSubscribed: makeSelectIsSubscribed(contentChannelUri, true)(state),
|
||||
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
|
||||
isAdmin: selectHasAdminChannel(state),
|
||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, contentPermanentUri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
|
||||
claimInCollection: makeSelectCollectionForIdHasClaimUrl(collectionId, contentPermanentUri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(collectionId)(state),
|
||||
editedCollection: makeSelectEditedCollectionForId(collectionId)(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
resolvedList,
|
||||
playNextUri,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -90,6 +101,9 @@ const perform = (dispatch) => ({
|
|||
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),
|
||||
doChannelUnsubscribe: (subscription) => dispatch(doChannelUnsubscribe(subscription)),
|
||||
doCollectionEdit: (collection, props) => dispatch(doCollectionEdit(collection, props)),
|
||||
fetchCollectionItems: (collectionId) => dispatch(doFetchItemsInCollection({ collectionId })),
|
||||
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ClaimPreview);
|
||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
|||
import classnames from 'classnames';
|
||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { generateShareUrl, generateRssUrl, generateLbryContentUrl } from 'util/url';
|
||||
import { generateShareUrl, generateRssUrl, generateLbryContentUrl, formatLbryUrlForWeb } from 'util/url';
|
||||
import { useHistory } from 'react-router';
|
||||
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
|
@ -56,6 +56,11 @@ type Props = {
|
|||
isChannelPage: boolean,
|
||||
editedCollection: Collection,
|
||||
isAuthenticated: boolean,
|
||||
fetchCollectionItems: (string) => void,
|
||||
resolvedList: boolean,
|
||||
playNextUri: string,
|
||||
doSetPlayingUri: (string) => void,
|
||||
doToggleShuffleList: (string) => void,
|
||||
};
|
||||
|
||||
function ClaimMenuList(props: Props) {
|
||||
|
@ -93,7 +98,13 @@ function ClaimMenuList(props: Props) {
|
|||
isChannelPage = false,
|
||||
editedCollection,
|
||||
isAuthenticated,
|
||||
fetchCollectionItems,
|
||||
resolvedList,
|
||||
playNextUri,
|
||||
doSetPlayingUri,
|
||||
doToggleShuffleList,
|
||||
} = props;
|
||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
|
||||
const isChannel = !incognitoClaim && !contentSigningChannel;
|
||||
const { channelName } = parseURI(contentChannelUri);
|
||||
|
@ -107,6 +118,27 @@ function ClaimMenuList(props: Props) {
|
|||
: __('Follow');
|
||||
|
||||
const { push, replace } = useHistory();
|
||||
|
||||
const fetchItems = React.useCallback(() => {
|
||||
if (collectionId) {
|
||||
fetchCollectionItems(collectionId);
|
||||
}
|
||||
}, [collectionId, fetchCollectionItems]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (resolvedList && doShuffle) {
|
||||
doToggleShuffleList(collectionId);
|
||||
if (playNextUri) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
|
||||
setDoShuffle(false);
|
||||
doSetPlayingUri(playNextUri);
|
||||
push(navigateUrl);
|
||||
}
|
||||
}
|
||||
}, [playNextUri, doShuffle, resolvedList, doToggleShuffleList, collectionId, doSetPlayingUri, push]);
|
||||
|
||||
if (!claim) {
|
||||
return null;
|
||||
}
|
||||
|
@ -245,10 +277,28 @@ function ClaimMenuList(props: Props) {
|
|||
{/* COLLECTION OPERATIONS */}
|
||||
{collectionId && isCollectionClaim ? (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||
<div className="menu__link">
|
||||
<MenuItem
|
||||
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} />
|
||||
{__('View List')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => {
|
||||
if (!resolvedList) fetchItems();
|
||||
setDoShuffle(true);
|
||||
}}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.SHUFFLE} />
|
||||
{__('Shuffle Play')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{isMyCollection && (
|
||||
|
|
|
@ -10,21 +10,33 @@ import {
|
|||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||
import CollectionActions from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const collectionId = props.collectionId;
|
||||
const shuffleList = selectListShuffle(state);
|
||||
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||
const playNextUri = shuffle && shuffle[0];
|
||||
|
||||
return {
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
claimIsPending: makeSelectClaimIsPending(props.uri)(state),
|
||||
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
|
||||
collectionHasEdits: Boolean(makeSelectEditedCollectionForId(props.collectionId)(state)),
|
||||
playNextUri,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
doSetPlayingUri: (uri) => dispatch(doSetPlayingUri({ uri })),
|
||||
doToggleShuffleList: (collectionId) => dispatch(doToggleShuffleList(undefined, collectionId, true, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CollectionActions);
|
||||
|
|
|
@ -11,6 +11,8 @@ import { useHistory } from 'react-router';
|
|||
import { EDIT_PAGE, PAGE_VIEW_QUERY } from 'page/collection/view';
|
||||
import classnames from 'classnames';
|
||||
import { ENABLE_FILE_REACTIONS } from 'config';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -24,6 +26,10 @@ type Props = {
|
|||
showInfo: boolean,
|
||||
setShowInfo: (boolean) => void,
|
||||
collectionHasEdits: boolean,
|
||||
doToggleShuffleList: (string) => void,
|
||||
playNextUri: string,
|
||||
doSetPlayingUri: (string) => void,
|
||||
isBuiltin: boolean,
|
||||
};
|
||||
|
||||
function CollectionActions(props: Props) {
|
||||
|
@ -37,61 +43,92 @@ function CollectionActions(props: Props) {
|
|||
showInfo,
|
||||
setShowInfo,
|
||||
collectionHasEdits,
|
||||
doToggleShuffleList,
|
||||
playNextUri,
|
||||
doSetPlayingUri,
|
||||
isBuiltin,
|
||||
} = props;
|
||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
const { push } = useHistory();
|
||||
const isMobile = useIsMobile();
|
||||
const claimId = claim && claim.claim_id;
|
||||
const webShareable = true; // collections have cost?
|
||||
|
||||
React.useEffect(() => {
|
||||
if (playNextUri && doShuffle) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
|
||||
setDoShuffle(false);
|
||||
doSetPlayingUri(playNextUri);
|
||||
push(navigateUrl);
|
||||
}
|
||||
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]);
|
||||
|
||||
const lhsSection = (
|
||||
<>
|
||||
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
|
||||
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
||||
{/* TODO Add ClaimRepostButton component */}
|
||||
{uri && (
|
||||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.SHARE}
|
||||
label={__('Share')}
|
||||
title={__('Share')}
|
||||
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
|
||||
/>
|
||||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.SHUFFLE}
|
||||
label={__('Shuffle Play')}
|
||||
title={__('Shuffle Play')}
|
||||
onClick={() => {
|
||||
doToggleShuffleList(collectionId);
|
||||
setDoShuffle(true);
|
||||
}}
|
||||
/>
|
||||
{!isBuiltin && (
|
||||
<>
|
||||
{ENABLE_FILE_REACTIONS && uri && <FileReactions uri={uri} />}
|
||||
{uri && <ClaimSupportButton uri={uri} fileAction />}
|
||||
{/* TODO Add ClaimRepostButton component */}
|
||||
{uri && (
|
||||
<Button
|
||||
className="button--file-action"
|
||||
icon={ICONS.SHARE}
|
||||
label={__('Share')}
|
||||
title={__('Share')}
|
||||
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const rhsSection = (
|
||||
<>
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
title={uri ? __('Update') : __('Publish')}
|
||||
label={uri ? __('Update') : __('Publish')}
|
||||
className={classnames('button--file-action')}
|
||||
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
||||
icon={ICONS.PUBLISH}
|
||||
iconColor={collectionHasEdits && 'red'}
|
||||
iconSize={18}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
)}
|
||||
{isMyCollection && (
|
||||
<Button
|
||||
className={classnames('button--file-action')}
|
||||
title={__('Delete List')}
|
||||
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
||||
icon={ICONS.DELETE}
|
||||
iconSize={18}
|
||||
description={__('Delete List')}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
)}
|
||||
{!isMyCollection && (
|
||||
<Button
|
||||
title={__('Report content')}
|
||||
className="button--file-action"
|
||||
icon={ICONS.REPORT}
|
||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||
/>
|
||||
)}
|
||||
{!isBuiltin &&
|
||||
(isMyCollection ? (
|
||||
<>
|
||||
<Button
|
||||
title={uri ? __('Update') : __('Publish')}
|
||||
label={uri ? __('Update') : __('Publish')}
|
||||
className={classnames('button--file-action')}
|
||||
onClick={() => push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`)}
|
||||
icon={ICONS.PUBLISH}
|
||||
iconColor={collectionHasEdits && 'red'}
|
||||
iconSize={18}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
<Button
|
||||
className={classnames('button--file-action')}
|
||||
title={__('Delete List')}
|
||||
onClick={() => openModal(MODALS.COLLECTION_DELETE, { uri, collectionId, redirect: `/$/${PAGES.LISTS}` })}
|
||||
icon={ICONS.DELETE}
|
||||
iconSize={18}
|
||||
description={__('Delete List')}
|
||||
disabled={claimIsPending}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
title={__('Report content')}
|
||||
className="button--file-action"
|
||||
icon={ICONS.REPORT}
|
||||
navigate={`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -7,15 +7,18 @@ import {
|
|||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
} from 'lbry-redux';
|
||||
import {
|
||||
selectPlayingUri,
|
||||
} from 'redux/selectors/content';
|
||||
import { selectPlayingUri, selectListLoop, selectListShuffle } from 'redux/selectors/content';
|
||||
import { doToggleLoopList, doToggleShuffleList } from 'redux/actions/content';
|
||||
|
||||
const select = (state, props) => {
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const playingUrl = playingUri && playingUri.uri;
|
||||
const claim = makeSelectClaimForUri(playingUrl)(state);
|
||||
const url = claim && claim.permanent_url;
|
||||
const loopList = selectListLoop(state);
|
||||
const loop = loopList && loopList.collectionId === props.id && loopList.loop;
|
||||
const shuffleList = selectListShuffle(state);
|
||||
const shuffle = shuffleList && shuffleList.collectionId === props.id && shuffleList.newUrls;
|
||||
|
||||
return {
|
||||
url,
|
||||
|
@ -23,7 +26,12 @@ const select = (state, props) => {
|
|||
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
||||
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
||||
isMine: makeSelectClaimIsMine(url)(state),
|
||||
loop,
|
||||
shuffle,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select)(CollectionContent);
|
||||
export default connect(select, {
|
||||
doToggleLoopList,
|
||||
doToggleShuffleList,
|
||||
})(CollectionContent);
|
||||
|
|
|
@ -15,23 +15,52 @@ type Props = {
|
|||
collectionUrls: Array<Claim>,
|
||||
collectionName: string,
|
||||
collection: any,
|
||||
loop: boolean,
|
||||
shuffle: boolean,
|
||||
doToggleLoopList: (string, boolean) => void,
|
||||
doToggleShuffleList: (string, string, boolean) => void,
|
||||
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
||||
};
|
||||
|
||||
export default function CollectionContent(props: Props) {
|
||||
const { collectionUrls, collectionName, id, url } = props;
|
||||
const { collectionUrls, collectionName, id, url, loop, shuffle, doToggleLoopList, doToggleShuffleList } = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
isBodyList
|
||||
className="file-page__recommended"
|
||||
className="file-page__recommended-collection"
|
||||
title={
|
||||
<span>
|
||||
<Icon
|
||||
icon={(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
||||
className="icon--margin-right" />
|
||||
{collectionName}
|
||||
</span>
|
||||
<>
|
||||
<span className="file-page__recommended-collection__row">
|
||||
<Icon
|
||||
icon={
|
||||
(id === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(id === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||
ICONS.STACK
|
||||
}
|
||||
className="icon--margin-right"
|
||||
/>
|
||||
{collectionName}
|
||||
</span>
|
||||
<span className="file-page__recommended-collection__row">
|
||||
<Button
|
||||
button="alt"
|
||||
title="Loop"
|
||||
icon={ICONS.REPEAT}
|
||||
iconColor={loop && 'blue'}
|
||||
className="button--file-action"
|
||||
onClick={() => doToggleLoopList(id, !loop)}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
title="Shuffle"
|
||||
icon={ICONS.SHUFFLE}
|
||||
iconColor={shuffle && 'blue'}
|
||||
className="button--file-action"
|
||||
onClick={() => doToggleShuffleList(url, id, !shuffle)}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
titleActions={
|
||||
<div className="card__title-actions--link">
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doCollectionEdit, makeSelectNameForCollectionId, doCollectionDelete } from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectListShuffle } from 'redux/selectors/content';
|
||||
import { doSetPlayingUri, doToggleShuffleList } from 'redux/actions/content';
|
||||
import CollectionMenuList from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const collectionId = props.collectionId;
|
||||
const shuffleList = selectListShuffle(state);
|
||||
const shuffle = shuffleList && shuffleList.collectionId === collectionId && shuffleList.newUrls;
|
||||
const playNextUri = shuffle && shuffle[0];
|
||||
|
||||
return {
|
||||
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
|
||||
playNextUri,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -13,4 +21,6 @@ export default connect(select, {
|
|||
doCollectionEdit,
|
||||
doOpenModal,
|
||||
doCollectionDelete,
|
||||
doSetPlayingUri,
|
||||
doToggleShuffleList,
|
||||
})(CollectionMenuList);
|
||||
|
|
|
@ -7,19 +7,44 @@ import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
|||
import Icon from 'component/common/icon';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { useHistory } from 'react-router';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
inline?: boolean,
|
||||
doOpenModal: (string, {}) => void,
|
||||
collectionName?: string,
|
||||
collectionId: string,
|
||||
playNextUri: string,
|
||||
doSetPlayingUri: ({ uri: ?string }) => void,
|
||||
doToggleShuffleList: (string, string, boolean, boolean) => void,
|
||||
};
|
||||
|
||||
function CollectionMenuList(props: Props) {
|
||||
const { inline = false, collectionId, collectionName, doOpenModal } = props;
|
||||
const {
|
||||
inline = false,
|
||||
collectionId,
|
||||
collectionName,
|
||||
doOpenModal,
|
||||
playNextUri,
|
||||
doSetPlayingUri,
|
||||
doToggleShuffleList,
|
||||
} = props;
|
||||
const [doShuffle, setDoShuffle] = React.useState(false);
|
||||
|
||||
const { push } = useHistory();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (playNextUri && doShuffle) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
const navigateUrl = formatLbryUrlForWeb(playNextUri) + `?` + collectionParams.toString();
|
||||
setDoShuffle(false);
|
||||
doSetPlayingUri({ uri: playNextUri });
|
||||
push(navigateUrl);
|
||||
}
|
||||
}, [push, doSetPlayingUri, collectionId, playNextUri, doShuffle]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
|
@ -35,9 +60,21 @@ function CollectionMenuList(props: Props) {
|
|||
{collectionId && collectionName && (
|
||||
<>
|
||||
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
|
||||
<div className="menu__link">
|
||||
<a className="menu__link" href={`/$/${PAGES.LIST}/${collectionId}`}>
|
||||
<Icon aria-hidden icon={ICONS.VIEW} />
|
||||
{__('View List')}
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="comment__menu-option"
|
||||
onSelect={() => {
|
||||
doToggleShuffleList('', collectionId, true, true);
|
||||
setDoShuffle(true);
|
||||
}}
|
||||
>
|
||||
<div className="menu__link">
|
||||
<Icon aria-hidden icon={ICONS.SHUFFLE} />
|
||||
{__('Shuffle Play')}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
|
|
|
@ -2401,4 +2401,27 @@ export const icons = {
|
|||
/>
|
||||
</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 primaryUri = selectPrimaryUri(state);
|
||||
const uri = playingUri && playingUri.uri;
|
||||
const collectionId = playingUri && playingUri.collectionId;
|
||||
|
||||
return {
|
||||
uri,
|
||||
|
@ -35,6 +36,7 @@ const select = (state, props) => {
|
|||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(uri)(state),
|
||||
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
||||
collectionId,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { onFullscreenChange } from 'util/full-screen';
|
|||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import debounce from 'util/debounce';
|
||||
import { useHistory } from 'react-router';
|
||||
import { isURIEqual } from 'lbry-redux';
|
||||
import { isURIEqual, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
||||
|
@ -34,6 +34,7 @@ type Props = {
|
|||
primaryUri: ?string,
|
||||
videoTheaterMode: boolean,
|
||||
doFetchRecommendedContent: (string, boolean) => void,
|
||||
collectionId: string,
|
||||
};
|
||||
|
||||
export default function FileRenderFloating(props: Props) {
|
||||
|
@ -51,6 +52,7 @@ export default function FileRenderFloating(props: Props) {
|
|||
primaryUri,
|
||||
videoTheaterMode,
|
||||
doFetchRecommendedContent,
|
||||
collectionId,
|
||||
} = props;
|
||||
const {
|
||||
location: { pathname },
|
||||
|
@ -69,6 +71,13 @@ export default function FileRenderFloating(props: Props) {
|
|||
y: 0,
|
||||
});
|
||||
|
||||
let navigateUrl;
|
||||
if (collectionId) {
|
||||
const collectionParams = new URLSearchParams();
|
||||
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId);
|
||||
navigateUrl = uri + `?` + collectionParams.toString();
|
||||
}
|
||||
|
||||
const playingUriSource = playingUri && playingUri.source;
|
||||
const isPlayable = RENDER_MODES.FLOATING_MODES.includes(renderMode);
|
||||
const isReadyToPlay = isPlayable && (streamingUrl || (fileInfo && fileInfo.completed));
|
||||
|
@ -303,7 +312,12 @@ export default function FileRenderFloating(props: Props) {
|
|||
{isFloating && (
|
||||
<div className="draggable content__info">
|
||||
<div className="claim-preview__title" title={title || uri}>
|
||||
<Button label={title || uri} navigate={uri} button="link" className="content__floating-link" />
|
||||
<Button
|
||||
label={title || uri}
|
||||
navigate={navigateUrl || uri}
|
||||
button="link"
|
||||
className="content__floating-link"
|
||||
/>
|
||||
</div>
|
||||
<UriIndicator link uri={uri} />
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
makeSelectStreamingUrlForUri,
|
||||
makeSelectClaimWasPurchased,
|
||||
SETTINGS,
|
||||
COLLECTIONS_CONSTS,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
@ -22,27 +23,34 @@ import {
|
|||
import FileRenderInitiator from './view';
|
||||
import { doAnaltyicsPurchaseEvent } from 'redux/actions/app';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID);
|
||||
|
||||
return {
|
||||
claimThumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
insufficientCredits: makeSelectInsufficientCreditsForUri(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
hasCostInfo: Boolean(makeSelectCostInfoForUri(props.uri)(state)),
|
||||
costInfo: makeSelectCostInfoForUri(props.uri)(state),
|
||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
collectionId,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
play: (uri) => {
|
||||
play: (uri, collectionId) => {
|
||||
dispatch(doSetPrimaryUri(uri));
|
||||
dispatch(doSetPlayingUri({ uri }));
|
||||
dispatch(doSetPlayingUri({ uri, collectionId }));
|
||||
dispatch(doPlayUri(uri, undefined, undefined, (fileInfo) => dispatch(doAnaltyicsPurchaseEvent(fileInfo))));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import FileRenderPlaceholder from 'static/img/fileRenderPlaceholder.png';
|
|||
const SPACE_BAR_KEYCODE = 32;
|
||||
|
||||
type Props = {
|
||||
play: (string) => void,
|
||||
play: (string, string) => void,
|
||||
isLoading: boolean,
|
||||
isPlaying: boolean,
|
||||
fileInfo: FileListItem,
|
||||
|
@ -36,6 +36,7 @@ type Props = {
|
|||
claimWasPurchased: boolean,
|
||||
authenticated: boolean,
|
||||
videoTheaterMode: boolean,
|
||||
collectionId: string,
|
||||
};
|
||||
|
||||
export default function FileRenderInitiator(props: Props) {
|
||||
|
@ -55,6 +56,7 @@ export default function FileRenderInitiator(props: Props) {
|
|||
claimWasPurchased,
|
||||
authenticated,
|
||||
videoTheaterMode,
|
||||
collectionId,
|
||||
} = props;
|
||||
|
||||
// force autoplay if a timestamp is present
|
||||
|
@ -109,9 +111,9 @@ export default function FileRenderInitiator(props: Props) {
|
|||
e.stopPropagation();
|
||||
}
|
||||
|
||||
play(uri);
|
||||
play(uri, collectionId);
|
||||
},
|
||||
[play, uri]
|
||||
[play, uri, collectionId]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -7,7 +7,7 @@ $transition-duration: 300ms;
|
|||
// Classnames must line up with classes in classes.js
|
||||
.ff-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
|
||||
.ff-image {
|
||||
z-index: $base-zindex;
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimForUri, makeSelectFileInfoForUri, makeSelectThumbnailForUri, SETTINGS } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
SETTINGS,
|
||||
COLLECTIONS_CONSTS,
|
||||
makeSelectNextUrlForCollectionAndUrl,
|
||||
makeSelectPreviousUrlForCollectionAndUrl,
|
||||
} from 'lbry-redux';
|
||||
import { doChangeVolume, doChangeMute, doAnalyticsView, doAnalyticsBuffer } from 'redux/actions/app';
|
||||
import { selectVolume, selectMute } from 'redux/selectors/app';
|
||||
import { savePosition, clearPosition } from 'redux/actions/content';
|
||||
import { makeSelectContentPositionForUri } from 'redux/selectors/content';
|
||||
import { savePosition, clearPosition, doSetPlayingUri, doPlayUri } from 'redux/actions/content';
|
||||
import { makeSelectContentPositionForUri, selectPlayingUri, makeSelectIsPlayerFloating } from 'redux/selectors/content';
|
||||
import VideoViewer from './view';
|
||||
import { withRouter } from 'react-router';
|
||||
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
|
||||
import { selectDaemonSettings, makeSelectClientSetting, selectHomepageData } from 'redux/selectors/settings';
|
||||
import { toggleVideoTheaterMode, doSetClientSetting } from 'redux/actions/settings';
|
||||
import { toggleVideoTheaterMode, toggleAutoplayNext, doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -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)
|
||||
const position = urlParams.get('t') !== null ? urlParams.get('t') : makeSelectContentPositionForUri(props.uri)(state);
|
||||
const userId = selectUser(state) && selectUser(state).id;
|
||||
const playingUri = selectPlayingUri(state);
|
||||
const collectionId = urlParams.get(COLLECTIONS_CONSTS.COLLECTION_ID) || (playingUri && playingUri.collectionId);
|
||||
|
||||
let playNextUri;
|
||||
let playPreviousUri;
|
||||
if (collectionId) {
|
||||
playNextUri = makeSelectNextUrlForCollectionAndUrl(collectionId, props.uri)(state);
|
||||
playPreviousUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, props.uri)(state);
|
||||
}
|
||||
|
||||
return {
|
||||
autoplayIfEmbedded: Boolean(autoplay),
|
||||
|
@ -33,6 +50,11 @@ const select = (state, props) => {
|
|||
authenticated: selectUserVerifiedEmail(state),
|
||||
userId: userId,
|
||||
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)),
|
||||
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
|
||||
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
|
||||
toggleAutoplayNext: () => dispatch(toggleAutoplayNext()),
|
||||
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));
|
||||
|
|
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
|
||||
import type { Player } from './videojs';
|
||||
import videojs from 'video.js';
|
||||
|
||||
class TheaterModeButton extends videojs.getComponent('Button') {
|
||||
constructor(player, options = {}) {
|
||||
super(player, options);
|
||||
this.addClass('vjs-button--theater-mode');
|
||||
this.controlText('Theater Mode');
|
||||
}
|
||||
}
|
||||
|
||||
export function addTheaterModeButton(player: Player, toggleVideoTheaterMode: () => void) {
|
||||
var myButton = player.controlBar.addChild('button', {
|
||||
const controlBar = player.getChild('controlBar');
|
||||
|
||||
const theaterMode = new TheaterModeButton(player, {
|
||||
name: 'TheaterModeButton',
|
||||
text: __('Theater mode'),
|
||||
clickHandler: () => {
|
||||
toggleVideoTheaterMode();
|
||||
},
|
||||
});
|
||||
|
||||
// $FlowFixMe
|
||||
myButton.addClass('vjs-button--theater-mode');
|
||||
// $FlowFixMe
|
||||
myButton.setAttribute('title', __('Theater mode'));
|
||||
controlBar.addChild(theaterMode);
|
||||
}
|
||||
|
|
|
@ -49,16 +49,21 @@ type Props = {
|
|||
source: string,
|
||||
sourceType: string,
|
||||
poster: ?string,
|
||||
onPlayerReady: (Player) => void,
|
||||
onPlayerReady: (Player, any) => void,
|
||||
isAudio: boolean,
|
||||
startMuted: boolean,
|
||||
autoplay: boolean,
|
||||
autoplaySetting: boolean,
|
||||
toggleVideoTheaterMode: () => void,
|
||||
adUrl: ?string,
|
||||
claimId: ?string,
|
||||
userId: ?number,
|
||||
// allowPreRoll: ?boolean,
|
||||
shareTelemetry: boolean,
|
||||
replay: boolean,
|
||||
videoTheaterMode: boolean,
|
||||
setStartPlayPrevious: (boolean) => void,
|
||||
setStartPlayNext: (boolean) => void,
|
||||
};
|
||||
|
||||
// type VideoJSOptions = {
|
||||
|
@ -103,6 +108,20 @@ const SMALL_J_KEYCODE = 74;
|
|||
const SMALL_K_KEYCODE = 75;
|
||||
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 MUTE_KEYCODE = SMALL_M_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) {
|
||||
const {
|
||||
autoplay,
|
||||
autoplaySetting,
|
||||
startMuted,
|
||||
source,
|
||||
sourceType,
|
||||
|
@ -197,6 +217,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
userId,
|
||||
// allowPreRoll,
|
||||
shareTelemetry,
|
||||
replay,
|
||||
videoTheaterMode,
|
||||
setStartPlayPrevious,
|
||||
setStartPlayNext,
|
||||
} = props;
|
||||
|
||||
const [reload, setReload] = useState('initial');
|
||||
|
@ -323,13 +347,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
resolveCtrlText({ type: 'pause' });
|
||||
resolveCtrlText({ type: 'volumechange' });
|
||||
resolveCtrlText({ type: 'fullscreenchange' });
|
||||
// (1) The 'Theater mode' button should probably be changed to a class
|
||||
// so that we can use getChild() with a specific name. There might be
|
||||
// clashes if we add a new button in the future.
|
||||
// (2) We'll have to get 'makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)'
|
||||
// as a prop here so we can say "Theater mode|Default mode" instead of
|
||||
// "Toggle Theater mode".
|
||||
controlBar.getChild('Button').controlText(__('Toggle Theater mode (t)'));
|
||||
controlBar
|
||||
.getChild('TheaterModeButton')
|
||||
.controlText(videoTheaterMode ? __('Default Mode (t)') : __('Theater Mode (t)'));
|
||||
controlBar.getChild('PlayNextButton').controlText(__('Play Next (SHIFT+N)'));
|
||||
controlBar.getChild('PlayPreviousButton').controlText(__('Play Previous (SHIFT+P)'));
|
||||
break;
|
||||
default:
|
||||
if (isDev) throw Error('Unexpected: ' + e.type);
|
||||
|
@ -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.keyCode === PERIOD_KEYCODE) changePlaybackSpeed(true);
|
||||
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) {
|
||||
|
@ -397,19 +421,37 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
if (e.keyCode === VOLUME_UP_KEYCODE) volumeUp();
|
||||
if (e.keyCode === VOLUME_DOWN_KEYCODE) volumeDown();
|
||||
if (e.keyCode === THEATER_MODE_KEYCODE) toggleTheaterMode();
|
||||
if (e.keyCode === SEEK_FORWARD_KEYCODE) seekVideo(SEEK_STEP);
|
||||
if (e.keyCode === SEEK_BACKWARD_KEYCODE) seekVideo(-SEEK_STEP);
|
||||
if (e.keyCode === SEEK_FORWARD_KEYCODE_5) seekVideo(SEEK_STEP_5);
|
||||
if (e.keyCode === SEEK_BACKWARD_KEYCODE_5) seekVideo(-SEEK_STEP_5);
|
||||
if (e.keyCode === SEEK_FORWARD_KEYCODE) seekVideo(SEEK_STEP, false);
|
||||
if (e.keyCode === SEEK_BACKWARD_KEYCODE) seekVideo(-SEEK_STEP, false);
|
||||
if (e.keyCode === SEEK_FORWARD_KEYCODE_5) seekVideo(SEEK_STEP_5, false);
|
||||
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 videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||
if (!videoNode || !player) return;
|
||||
const duration = videoNode.duration;
|
||||
const numberedStepSize = duration / 10;
|
||||
const currentTime = videoNode.currentTime;
|
||||
const newDuration = currentTime + stepSize;
|
||||
|
||||
let newDuration;
|
||||
if (numberedStep) {
|
||||
newDuration = numberedStepSize * stepSize;
|
||||
} else {
|
||||
newDuration = currentTime + stepSize;
|
||||
}
|
||||
|
||||
if (newDuration < 0) {
|
||||
videoNode.currentTime = 0;
|
||||
} else if (newDuration > duration) {
|
||||
|
@ -417,7 +459,10 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
} else {
|
||||
videoNode.currentTime = newDuration;
|
||||
}
|
||||
OVERLAY.showSeekedOverlay(player, Math.abs(stepSize), stepSize > 0);
|
||||
|
||||
if (!numberedStep) {
|
||||
OVERLAY.showSeekedOverlay(player, Math.abs(stepSize), stepSize > 0);
|
||||
}
|
||||
player.userActive(true);
|
||||
}
|
||||
|
||||
|
@ -580,7 +625,8 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
player.children_[0].setAttribute('playsinline', '');
|
||||
|
||||
// I think this is a callback function
|
||||
onPlayerReady(player);
|
||||
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
|
||||
onPlayerReady(player, videoNode);
|
||||
});
|
||||
|
||||
// pre-roll ads
|
||||
|
@ -601,6 +647,41 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
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.
|
||||
useEffect(() => {
|
||||
const vjsElement = createVideoPlayerDOM(containerRef.current);
|
||||
|
|
|
@ -16,12 +16,17 @@ import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
|
|||
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import { addTheaterModeButton } from './internal/theater-mode';
|
||||
import { addAutoplayNextButton } from './internal/autoplay-next';
|
||||
import { addPlayNextButton } from './internal/play-next';
|
||||
import { addPlayPreviousButton } from './internal/play-previous';
|
||||
import { useGetAds } from 'effects/use-get-ads';
|
||||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { useHistory } from 'react-router';
|
||||
import { getAllIds } from 'util/buildHomepage';
|
||||
import type { HomepageCat } from 'util/buildHomepage';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||
|
||||
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||
const PLAY_TIMEOUT_LIMIT = 2000;
|
||||
|
@ -47,11 +52,19 @@ type Props = {
|
|||
savePosition: (string, number) => void,
|
||||
clearPosition: (string) => void,
|
||||
toggleVideoTheaterMode: () => void,
|
||||
toggleAutoplayNext: () => void,
|
||||
setVideoPlaybackRate: (number) => void,
|
||||
doSetPlayingUri: (string, string) => void,
|
||||
doPlayUri: (string) => void,
|
||||
playNextUri: string,
|
||||
playPreviousUri: string,
|
||||
authenticated: boolean,
|
||||
userId: number,
|
||||
homepageData?: { [string]: HomepageCat },
|
||||
shareTelemetry: boolean,
|
||||
videoTheaterMode: boolean,
|
||||
collectionId: string,
|
||||
isFloating: boolean,
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -81,11 +94,19 @@ function VideoViewer(props: Props) {
|
|||
clearPosition,
|
||||
desktopPlayStartTime,
|
||||
toggleVideoTheaterMode,
|
||||
toggleAutoplayNext,
|
||||
setVideoPlaybackRate,
|
||||
doSetPlayingUri,
|
||||
doPlayUri,
|
||||
playNextUri,
|
||||
playPreviousUri,
|
||||
homepageData,
|
||||
authenticated,
|
||||
userId,
|
||||
shareTelemetry,
|
||||
videoTheaterMode,
|
||||
collectionId,
|
||||
isFloating,
|
||||
} = props;
|
||||
|
||||
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||
|
@ -95,8 +116,10 @@ function VideoViewer(props: Props) {
|
|||
const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
|
||||
const {
|
||||
location: { pathname },
|
||||
push,
|
||||
} = useHistory();
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [ended, setEnded] = useState(false);
|
||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
||||
const vjsCallbackDataRef: any = React.useRef();
|
||||
|
@ -108,6 +131,26 @@ function VideoViewer(props: Props) {
|
|||
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
|
||||
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [replay, setReplay] = useState(false);
|
||||
const [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)
|
||||
useEffect(() => {
|
||||
|
@ -126,6 +169,51 @@ function VideoViewer(props: Props) {
|
|||
};
|
||||
}, [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) {
|
||||
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
|
||||
data.playerPoweredBy = response.headers.get('x-powered-by');
|
||||
|
@ -152,28 +240,41 @@ function VideoViewer(props: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
const onEnded = React.useCallback(() => {
|
||||
analytics.videoIsPlaying(false);
|
||||
React.useEffect(() => {
|
||||
if (ended) {
|
||||
analytics.videoIsPlaying(false);
|
||||
|
||||
if (adUrl) {
|
||||
setAdUrl(null);
|
||||
return;
|
||||
if (adUrl) {
|
||||
setAdUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (embedded) {
|
||||
setIsEndededEmbed(true);
|
||||
} else if (autoplaySetting) {
|
||||
setShowAutoplayCountdown(true);
|
||||
}
|
||||
|
||||
clearPosition(uri);
|
||||
}
|
||||
|
||||
if (embedded) {
|
||||
setIsEndededEmbed(true);
|
||||
} else if (autoplaySetting) {
|
||||
setShowAutoplayCountdown(true);
|
||||
}
|
||||
|
||||
clearPosition(uri);
|
||||
}, [embedded, setIsEndededEmbed, autoplaySetting, setShowAutoplayCountdown, adUrl, setAdUrl, clearPosition, uri]);
|
||||
}, [
|
||||
embedded,
|
||||
setIsEndededEmbed,
|
||||
autoplaySetting,
|
||||
setShowAutoplayCountdown,
|
||||
adUrl,
|
||||
setAdUrl,
|
||||
clearPosition,
|
||||
uri,
|
||||
ended,
|
||||
]);
|
||||
|
||||
function onPlay(player) {
|
||||
setIsLoading(false);
|
||||
setIsPlaying(true);
|
||||
setShowAutoplayCountdown(false);
|
||||
setIsEndededEmbed(false);
|
||||
setReplay(false);
|
||||
analytics.videoIsPlaying(true, player);
|
||||
}
|
||||
|
||||
|
@ -203,12 +304,19 @@ function VideoViewer(props: Props) {
|
|||
playerReadyDependencyList.push(desktopPlayStartTime);
|
||||
}
|
||||
|
||||
const onPlayerReady = useCallback((player: Player) => {
|
||||
const onPlayerReady = useCallback((player: Player, videoNode: any) => {
|
||||
if (!embedded) {
|
||||
setVideoNode(videoNode);
|
||||
player.muted(muted);
|
||||
player.volume(volume);
|
||||
player.playbackRate(videoPlaybackRate);
|
||||
addTheaterModeButton(player, toggleVideoTheaterMode);
|
||||
if (collectionId) {
|
||||
addPlayNextButton(player, () => setStartPlayNext(true));
|
||||
addPlayPreviousButton(player, () => setStartPlayPrevious(true));
|
||||
} else {
|
||||
addAutoplayNextButton(player, toggleAutoplayNext, autoplaySetting);
|
||||
}
|
||||
}
|
||||
|
||||
const shouldPlay = !embedded || autoplayIfEmbedded;
|
||||
|
@ -244,7 +352,7 @@ function VideoViewer(props: Props) {
|
|||
|
||||
// first play tracking, used for initializing the watchman api
|
||||
player.on('tracking:firstplay', doTrackingFirstPlay);
|
||||
player.on('ended', onEnded);
|
||||
player.on('ended', () => setEnded(true));
|
||||
player.on('play', onPlay);
|
||||
player.on('pause', (event) => onPause(event, player));
|
||||
player.on('dispose', (event) => onDispose(event, player));
|
||||
|
@ -285,7 +393,7 @@ function VideoViewer(props: Props) {
|
|||
})}
|
||||
onContextMenu={stopContextMenu}
|
||||
>
|
||||
{showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
|
||||
{showAutoplayCountdown && <AutoplayCountdown uri={uri} setReplay={setReplay} />}
|
||||
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||
|
@ -332,10 +440,15 @@ function VideoViewer(props: Props) {
|
|||
startMuted={autoplayIfEmbedded}
|
||||
toggleVideoTheaterMode={toggleVideoTheaterMode}
|
||||
autoplay={!embedded || autoplayIfEmbedded}
|
||||
autoplaySetting={autoplaySetting}
|
||||
claimId={claimId}
|
||||
userId={userId}
|
||||
allowPreRoll={!embedded && !authenticated}
|
||||
shareTelemetry={shareTelemetry}
|
||||
replay={replay}
|
||||
videoTheaterMode={videoTheaterMode}
|
||||
setStartPlayNext={setStartPlayNext}
|
||||
setStartPlayPrevious={setStartPlayPrevious}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -100,6 +100,8 @@ export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
|||
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
|
||||
export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
|
||||
export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
|
||||
export const TOGGLE_LOOP_LIST = 'TOGGLE_LOOP_LIST';
|
||||
export const TOGGLE_SHUFFLE_LIST = 'TOGGLE_SHUFFLE_LIST';
|
||||
|
||||
// Files
|
||||
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
|
||||
|
|
|
@ -169,3 +169,6 @@ export const CONTENT = 'Content';
|
|||
export const STAR = 'star';
|
||||
export const MUSIC = 'MusicCategory';
|
||||
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>
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -112,18 +112,26 @@ export default function CollectionPage(props: Props) {
|
|||
title={
|
||||
<span>
|
||||
<Icon
|
||||
icon={(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) || ICONS.STACK}
|
||||
className="icon--margin-right" />
|
||||
icon={
|
||||
(collectionId === COLLECTIONS_CONSTS.WATCH_LATER_ID && ICONS.TIME) ||
|
||||
(collectionId === COLLECTIONS_CONSTS.FAVORITES_ID && ICONS.STAR) ||
|
||||
ICONS.STACK
|
||||
}
|
||||
className="icon--margin-right"
|
||||
/>
|
||||
{claim ? claim.value.title || claim.name : collection && collection.name}
|
||||
</span>
|
||||
}
|
||||
titleActions={titleActions}
|
||||
subtitle={subTitle}
|
||||
body={
|
||||
!isBuiltin && (
|
||||
<CollectionActions uri={uri} collectionId={collectionId} setShowInfo={setShowInfo} showInfo={showInfo} />
|
||||
)
|
||||
<CollectionActions
|
||||
uri={uri}
|
||||
collectionId={collectionId}
|
||||
setShowInfo={setShowInfo}
|
||||
showInfo={showInfo}
|
||||
isBuiltin={isBuiltin}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
showInfo &&
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
makeSelectClaimWasPurchased,
|
||||
doToast,
|
||||
makeSelectUrlsForCollectionId,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
|
@ -110,16 +112,18 @@ export function doSetPlayingUri({
|
|||
source,
|
||||
pathname,
|
||||
commentId,
|
||||
collectionId,
|
||||
}: {
|
||||
uri: ?string,
|
||||
source?: string,
|
||||
commentId?: string,
|
||||
pathname: string,
|
||||
collectionId: string,
|
||||
}) {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.SET_PLAYING_URI,
|
||||
data: { uri, source, pathname, commentId },
|
||||
data: { uri, source, pathname, commentId, collectionId },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -279,3 +283,58 @@ export const doRecommendationClicked = (claimId: string, index: number) => (disp
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function doToggleLoopList(collectionId: string, loop: boolean, hideToast: boolean) {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.TOGGLE_LOOP_LIST,
|
||||
data: { collectionId, loop },
|
||||
});
|
||||
if (loop && !hideToast) {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __('Loop is on.'),
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function doToggleShuffleList(currentUri: string, collectionId: string, shuffle: boolean, hideToast: boolean) {
|
||||
return (dispatch: Dispatch, getState: () => any) => {
|
||||
if (shuffle) {
|
||||
const state = getState();
|
||||
const urls = makeSelectUrlsForCollectionId(collectionId)(state);
|
||||
|
||||
let newUrls = urls
|
||||
.map((item) => ({ item, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ item }) => item);
|
||||
|
||||
// the currently playing URI should be first in list or else
|
||||
// can get in strange position where it might be in the middle or last
|
||||
// and the shuffled list ends before scrolling through all entries
|
||||
if (currentUri && currentUri !== '') {
|
||||
newUrls.splice(newUrls.indexOf(currentUri), 1);
|
||||
newUrls.splice(0, 0, currentUri);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.TOGGLE_SHUFFLE_LIST,
|
||||
data: { collectionId, newUrls },
|
||||
});
|
||||
if (!hideToast) {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: __('Shuffle is on.'),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatch({
|
||||
type: ACTIONS.TOGGLE_SHUFFLE_LIST,
|
||||
data: { collectionId, newUrls: false },
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ export const IS_MAC = process.platform === 'darwin';
|
|||
const UPDATE_IS_NIGHT_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
export function doFetchDaemonSettings() {
|
||||
return dispatch => {
|
||||
Lbry.settings_get().then(settings => {
|
||||
return (dispatch) => {
|
||||
Lbry.settings_get().then((settings) => {
|
||||
analytics.toggleInternal(settings.share_usage_data);
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||
|
@ -32,11 +32,11 @@ export function doFetchDaemonSettings() {
|
|||
}
|
||||
|
||||
export function doFindFFmpeg() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_STARTED,
|
||||
});
|
||||
return Lbry.ffmpeg_find().then(done => {
|
||||
return Lbry.ffmpeg_find().then((done) => {
|
||||
dispatch(doGetDaemonStatus());
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.FINDING_FFMPEG_COMPLETED,
|
||||
|
@ -46,8 +46,8 @@ export function doFindFFmpeg() {
|
|||
}
|
||||
|
||||
export function doGetDaemonStatus() {
|
||||
return dispatch => {
|
||||
return Lbry.status().then(status => {
|
||||
return (dispatch) => {
|
||||
return Lbry.status().then((status) => {
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_STATUS_RECEIVED,
|
||||
data: {
|
||||
|
@ -72,7 +72,7 @@ export function doClearDaemonSetting(key) {
|
|||
key,
|
||||
};
|
||||
// not if syncLocked
|
||||
Lbry.settings_clear(clearKey).then(defaultSettings => {
|
||||
Lbry.settings_clear(clearKey).then((defaultSettings) => {
|
||||
if (SDK_SYNC_KEYS.includes(key)) {
|
||||
dispatch({
|
||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||
|
@ -83,7 +83,7 @@ export function doClearDaemonSetting(key) {
|
|||
dispatch(doWalletReconnect());
|
||||
}
|
||||
});
|
||||
Lbry.settings_get().then(settings => {
|
||||
Lbry.settings_get().then((settings) => {
|
||||
analytics.toggleInternal(settings.share_usage_data);
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||
|
@ -108,7 +108,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
|||
key,
|
||||
value: !value && value !== false ? null : value,
|
||||
};
|
||||
Lbry.settings_set(newSettings).then(newSetting => {
|
||||
Lbry.settings_set(newSettings).then((newSetting) => {
|
||||
if (SDK_SYNC_KEYS.includes(key) && !doNotDispatch) {
|
||||
dispatch({
|
||||
type: ACTIONS.SHARED_PREFERENCE_SET,
|
||||
|
@ -121,7 +121,7 @@ export function doSetDaemonSetting(key, value, doNotDispatch = false) {
|
|||
// todo: add sdk reloadsettings() (or it happens automagically?)
|
||||
}
|
||||
});
|
||||
Lbry.settings_get().then(settings => {
|
||||
Lbry.settings_get().then((settings) => {
|
||||
analytics.toggleInternal(settings.share_usage_data);
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_SETTINGS_RECEIVED,
|
||||
|
@ -170,7 +170,7 @@ export function doUpdateIsNight() {
|
|||
}
|
||||
|
||||
export function doUpdateIsNightAsync() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch(doUpdateIsNight());
|
||||
|
||||
setInterval(() => dispatch(doUpdateIsNight()), UPDATE_IS_NIGHT_INTERVAL);
|
||||
|
@ -201,8 +201,8 @@ export function doSetDarkTime(value, options) {
|
|||
|
||||
export function doGetWalletSyncPreference() {
|
||||
const SYNC_KEY = 'enable-sync';
|
||||
return dispatch => {
|
||||
return Lbry.preference_get({ key: SYNC_KEY }).then(result => {
|
||||
return (dispatch) => {
|
||||
return Lbry.preference_get({ key: SYNC_KEY }).then((result) => {
|
||||
const enabled = result && result[SYNC_KEY];
|
||||
if (enabled !== null) {
|
||||
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
||||
|
@ -214,8 +214,8 @@ export function doGetWalletSyncPreference() {
|
|||
|
||||
export function doSetWalletSyncPreference(pref) {
|
||||
const SYNC_KEY = 'enable-sync';
|
||||
return dispatch => {
|
||||
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then(result => {
|
||||
return (dispatch) => {
|
||||
return Lbry.preference_set({ key: SYNC_KEY, value: pref }).then((result) => {
|
||||
const enabled = result && result[SYNC_KEY];
|
||||
if (enabled !== null) {
|
||||
dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, enabled));
|
||||
|
@ -226,7 +226,7 @@ export function doSetWalletSyncPreference(pref) {
|
|||
}
|
||||
|
||||
export function doPushSettingsToPrefs() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.SYNC_CLIENT_SETTINGS,
|
||||
|
@ -274,8 +274,8 @@ export function doFetchLanguage(language) {
|
|||
if (settings.language !== language || (settings.loadedLanguages && !settings.loadedLanguages.includes(language))) {
|
||||
// this should match the behavior/logic in index-web.html
|
||||
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
window.i18n_messages[language] = j;
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
||||
|
@ -284,7 +284,7 @@ export function doFetchLanguage(language) {
|
|||
},
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_FAILURE,
|
||||
});
|
||||
|
@ -324,8 +324,8 @@ export function doSetLanguage(language) {
|
|||
) {
|
||||
// this should match the behavior/logic in index-web.html
|
||||
fetch('https://lbry.com/i18n/get/lbry-desktop/app-strings/' + language + '.json')
|
||||
.then(r => r.json())
|
||||
.then(j => {
|
||||
.then((r) => r.json())
|
||||
.then((j) => {
|
||||
window.i18n_messages[language] = j;
|
||||
dispatch({
|
||||
type: LOCAL_ACTIONS.DOWNLOAD_LANGUAGE_SUCCESS,
|
||||
|
@ -344,7 +344,7 @@ export function doSetLanguage(language) {
|
|||
});
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
window.localStorage.setItem(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE);
|
||||
dispatch(doSetClientSetting(SETTINGS.LANGUAGE, DEFAULT_LANGUAGE));
|
||||
const languageName = SUPPORTED_LANGUAGES[language] ? SUPPORTED_LANGUAGES[language] : language;
|
||||
|
@ -369,7 +369,7 @@ export function doSetAutoLaunch(value) {
|
|||
}
|
||||
|
||||
if (value === undefined) {
|
||||
launcher.isEnabled().then(isEnabled => {
|
||||
launcher.isEnabled().then((isEnabled) => {
|
||||
if (isEnabled) {
|
||||
if (!autoLaunch) {
|
||||
launcher.disable().then(() => {
|
||||
|
@ -385,7 +385,7 @@ export function doSetAutoLaunch(value) {
|
|||
}
|
||||
});
|
||||
} else if (value === true) {
|
||||
launcher.isEnabled().then(function(isEnabled) {
|
||||
launcher.isEnabled().then(function (isEnabled) {
|
||||
if (!isEnabled) {
|
||||
launcher.enable().then(() => {
|
||||
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, true));
|
||||
|
@ -396,7 +396,7 @@ export function doSetAutoLaunch(value) {
|
|||
});
|
||||
} else {
|
||||
// value = false
|
||||
launcher.isEnabled().then(function(isEnabled) {
|
||||
launcher.isEnabled().then(function (isEnabled) {
|
||||
if (isEnabled) {
|
||||
launcher.disable().then(() => {
|
||||
dispatch(doSetClientSetting(SETTINGS.AUTO_LAUNCH, false));
|
||||
|
@ -410,7 +410,7 @@ export function doSetAutoLaunch(value) {
|
|||
}
|
||||
|
||||
export function doSetAppToTrayWhenClosed(value) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
window.localStorage.setItem(SETTINGS.TO_TRAY_WHEN_CLOSED, value);
|
||||
dispatch(doSetClientSetting(SETTINGS.TO_TRAY_WHEN_CLOSED, value));
|
||||
};
|
||||
|
@ -424,3 +424,12 @@ export function toggleVideoTheaterMode() {
|
|||
dispatch(doSetClientSetting(SETTINGS.VIDEO_THEATER_MODE, !videoTheaterMode));
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleAutoplayNext() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const autoplayNext = makeSelectClientSetting(SETTINGS.AUTOPLAY)(state);
|
||||
|
||||
dispatch(doSetClientSetting(SETTINGS.AUTOPLAY, !autoplayNext));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,10 +25,27 @@ reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
|
|||
source: action.data.source,
|
||||
pathname: action.data.pathname,
|
||||
commentId: action.data.commentId,
|
||||
collectionId: action.data.collectionId,
|
||||
primaryUri: state.primaryUri,
|
||||
},
|
||||
});
|
||||
|
||||
reducers[ACTIONS.TOGGLE_LOOP_LIST] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
loopList: {
|
||||
collectionId: action.data.collectionId,
|
||||
loop: action.data.loop,
|
||||
},
|
||||
});
|
||||
|
||||
reducers[ACTIONS.TOGGLE_SHUFFLE_LIST] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
shuffleList: {
|
||||
collectionId: action.data.collectionId,
|
||||
newUrls: action.data.newUrls,
|
||||
},
|
||||
});
|
||||
|
||||
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
||||
const { claimId, outpoint, position } = action.data;
|
||||
return {
|
||||
|
|
|
@ -29,6 +29,9 @@ export const selectState = (state: any) => state.content || {};
|
|||
export const selectPlayingUri = createSelector(selectState, (state) => state.playingUri);
|
||||
export const selectPrimaryUri = createSelector(selectState, (state) => state.primaryUri);
|
||||
|
||||
export const selectListLoop = createSelector(selectState, (state) => state.loopList);
|
||||
export const selectListShuffle = createSelector(selectState, (state) => state.shuffleList);
|
||||
|
||||
export const makeSelectIsPlaying = (uri: string) =>
|
||||
createSelector(selectPrimaryUri, (primaryUri) => primaryUri === uri);
|
||||
|
||||
|
|
|
@ -132,6 +132,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 {
|
||||
color: var(--color-link);
|
||||
transition: color 0.2s;
|
||||
|
|
|
@ -314,6 +314,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
font-size: var(--font-xsmall);
|
||||
|
||||
.menu__link {
|
||||
color: var(--color-text);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-page__recommended-collection {
|
||||
@extend .file-page__recommended;
|
||||
flex-direction: column;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
.file-page__recommended-collection__row {
|
||||
display: block;
|
||||
|
||||
@media (min-width: $breakpoint-medium) {
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
max-width: 50rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
flex-direction: column;
|
||||
> :first-child {
|
||||
|
|
|
@ -10129,9 +10129,9 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#e4d0662100a5f4b28bb1bf3cbc1e51b2eebab5b6:
|
||||
lbry-redux@lbryio/lbry-redux#aeb1f533b590c0a9a63954beaa52e92d6f0668e3:
|
||||
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:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
|
Loading…
Reference in a new issue