Merge branch 'master' of github.com:lbryio/lbry-desktop

This commit is contained in:
Jeremy Kauffman 2020-01-28 14:52:26 -05:00
commit 71119ecdd7
33 changed files with 438 additions and 95 deletions

View file

@ -1,16 +1,10 @@
const { getHtml } = require('./html'); const { getHtml } = require('./html');
const { generateStreamUrl, generateDownloadUrl } = require('../../ui/util/lbrytv'); const { generateDownloadUrl } = require('../../ui/util/lbrytv');
const { LBRY_TV_API } = require('../../config');
const Router = require('@koa/router'); const Router = require('@koa/router');
const send = require('koa-send');
const router = new Router(); const router = new Router();
router.get(`/$/embed/:claimName/:claimId`, async ctx => {
const { claimName, claimId } = ctx.params;
const streamUrl = generateStreamUrl(claimName, claimId);
ctx.redirect(streamUrl);
});
router.get(`/$/download/:claimName/:claimId`, async ctx => { router.get(`/$/download/:claimName/:claimId`, async ctx => {
const { claimName, claimId } = ctx.params; const { claimName, claimId } = ctx.params;
const downloadUrl = generateDownloadUrl(claimName, claimId); const downloadUrl = generateDownloadUrl(claimName, claimId);

View file

@ -157,6 +157,7 @@
"Details": "Details", "Details": "Details",
"Transaction": "Transaction", "Transaction": "Transaction",
"Date": "Date", "Date": "Date",
"Abandon Support": "Abandon Support",
"Abandon Claim": "Abandon Claim", "Abandon Claim": "Abandon Claim",
"Find New Tags To Follow": "Find New Tags To Follow", "Find New Tags To Follow": "Find New Tags To Follow",
"Aw shucks!": "Aw shucks!", "Aw shucks!": "Aw shucks!",

View file

@ -0,0 +1,25 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetPlayingUri } from 'redux/actions/content';
import RecommendedVideos from './view';
const select = (state, props) => {
const nextRecommendedUri = makeSelectNextUnplayedRecommended(props.uri)(state);
return {
nextRecommendedUri,
nextRecommendedClaim: makeSelectClaimForUri(nextRecommendedUri)(state),
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
};
};
const perform = dispatch => ({
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
});
export default connect(
select,
perform
)(RecommendedVideos);

View file

@ -0,0 +1,76 @@
// @flow
import React from 'react';
import Button from 'component/button';
import UriIndicator from 'component/uriIndicator';
import I18nMessage from 'component/i18nMessage';
import { formatLbryUrlForWeb } from 'util/url';
import { withRouter } from 'react-router';
type Props = {
history: { push: string => void },
nextRecommendedClaim: ?StreamClaim,
nextRecommendedUri: string,
setPlayingUri: (string | null) => void,
};
function AutoplayCountdown(props: Props) {
const {
nextRecommendedUri,
nextRecommendedClaim,
setPlayingUri,
history: { push },
} = props;
const nextTitle = nextRecommendedClaim && nextRecommendedClaim.value && nextRecommendedClaim.value.title;
const [timer, setTimer] = React.useState(5);
const [timerCanceled, setTimerCanceled] = React.useState(false);
let navigateUrl;
if (nextTitle) {
navigateUrl = formatLbryUrlForWeb(nextRecommendedUri);
}
React.useEffect(() => {
let interval;
if (!timerCanceled) {
interval = setInterval(() => {
const newTime = timer - 1;
if (newTime === 0) {
// Set the playingUri to null so the app doesn't try to make a floating window with the video that just finished
setPlayingUri(null);
push(navigateUrl);
} else {
setTimer(timer - 1);
}
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [timer, navigateUrl, push, timerCanceled, setPlayingUri, nextRecommendedUri]);
if (timerCanceled) {
return null;
}
return (
<div className="video-overlay__wrapper">
<div className="video-overlay__subtitle">
<I18nMessage tokens={{ channel: <UriIndicator link uri={nextRecommendedUri} /> }}>
Up Next by %channel%
</I18nMessage>
</div>
<div className="video-overlay__title">{nextTitle}</div>
<div className="video-overlay__actions">
<div className="video-overlay__subtitle">
{__('Playing in %seconds_left% seconds', { seconds_left: timer })}
</div>
<div className="section__actions--centered">
<Button label={__('Cancel')} button="link" onClick={() => setTimerCanceled(true)} />
</div>
</div>
</div>
);
}
export default withRouter(AutoplayCountdown);

View file

@ -147,6 +147,7 @@ class ChannelCreate extends React.PureComponent<Props, State> {
error={newChannelBidError} error={newChannelBidError}
value={newChannelBid} value={newChannelBid}
onChange={event => this.handleNewChannelBidChange(parseFloat(event.target.value))} onChange={event => this.handleNewChannelBidChange(parseFloat(event.target.value))}
onWheel={e => e.stopPropagation()}
/> />
<div className="card__actions"> <div className="card__actions">
<Button <Button

View file

@ -116,20 +116,20 @@ function ClaimListDiscover(props: Props) {
// For more than 20, drop it down to 6 months // For more than 20, drop it down to 6 months
// This helps with timeout issues for users that are following a ton of stuff // This helps with timeout issues for users that are following a ton of stuff
// https://github.com/lbryio/lbry-sdk/issues/2420 // https://github.com/lbryio/lbry-sdk/issues/2420
if (options.channel_ids.length > 10 || options.any_tags.length > 10) { if (options.channel_ids.length > 20 || options.any_tags.length > 20) {
options.release_time = `>${Math.floor(
moment()
.subtract(1, TIME_YEAR)
.startOf('week')
.unix()
)}`;
} else if (options.channel_ids.length > 20 || options.any_tags.length > 20) {
options.release_time = `>${Math.floor( options.release_time = `>${Math.floor(
moment() moment()
.subtract(6, TIME_MONTH) .subtract(6, TIME_MONTH)
.startOf('week') .startOf('week')
.unix() .unix()
)}`; )}`;
} else if (options.channel_ids.length > 10 || options.any_tags.length > 10) {
options.release_time = `>${Math.floor(
moment()
.subtract(1, TIME_YEAR)
.startOf('week')
.unix()
)}`;
} }
} }

View file

@ -53,8 +53,12 @@ function ClaimPreviewTile(props: Props) {
} = props; } = props;
const shouldFetch = claim === undefined; const shouldFetch = claim === undefined;
const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail; const thumbnailUrl = useGetThumbnail(uri, claim, streamingUrl, getFile, placeholder) || thumbnail;
const navigateUrl = uri ? formatLbryUrlForWeb(uri) : undefined;
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
const navigateUrl = uri ? formatLbryUrlForWeb(uri) : undefined;
const navLinkProps = {
to: navigateUrl,
onClick: e => e.stopPropagation(),
};
let isChannel; let isChannel;
let isValid = false; let isValid = false;
@ -142,10 +146,10 @@ function ClaimPreviewTile(props: Props) {
'claim-preview--channel': isChannel, 'claim-preview--channel': isChannel,
})} })}
> >
<NavLink to={navigateUrl}> <NavLink {...navLinkProps}>
<FileThumbnail thumbnail={thumbnailUrl} /> <FileThumbnail thumbnail={thumbnailUrl} />
</NavLink> </NavLink>
<NavLink to={navigateUrl}> <NavLink {...navLinkProps}>
<h2 className="claim-tile__title"> <h2 className="claim-tile__title">
<TruncatedText text={title || (claim && claim.name)} lines={2} /> <TruncatedText text={title || (claim && claim.name)} lines={2} />
</h2> </h2>

View file

@ -1,10 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doToast } from 'lbry-redux'; import { doToast } from 'lbry-redux';
import EmbedArea from './view'; import EmbedTextArea from './view';
export default connect( export default connect(
null, null,
{ {
doToast, doToast,
} }
)(EmbedArea); )(EmbedTextArea);

View file

@ -13,7 +13,7 @@ type Props = {
claim: Claim, claim: Claim,
}; };
export default function EmbedArea(props: Props) { export default function EmbedTextArea(props: Props) {
const { doToast, snackMessage, label, claim } = props; const { doToast, snackMessage, label, claim } = props;
const { claim_id: claimId, name } = claim; const { claim_id: claimId, name } = claim;
const input = useRef(); const input = useRef();

View file

@ -8,13 +8,14 @@ import {
makeSelectDownloadPathForUri, makeSelectDownloadPathForUri,
makeSelectFileNameForUri, makeSelectFileNameForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { THEME, AUTOPLAY } from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { makeSelectNextUnplayedRecommended, makeSelectIsText } from 'redux/selectors/content'; import { makeSelectIsText } from 'redux/selectors/content';
import { doSetPlayingUri } from 'redux/actions/content';
import FileRender from './view'; import FileRender from './view';
const select = (state, props) => ({ const select = (state, props) => ({
currentTheme: makeSelectClientSetting(THEME)(state), currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
mediaType: makeSelectMediaTypeForUri(props.uri)(state), mediaType: makeSelectMediaTypeForUri(props.uri)(state),
thumbnail: makeSelectThumbnailForUri(props.uri)(state), thumbnail: makeSelectThumbnailForUri(props.uri)(state),
@ -22,9 +23,15 @@ const select = (state, props) => ({
downloadPath: makeSelectDownloadPathForUri(props.uri)(state), downloadPath: makeSelectDownloadPathForUri(props.uri)(state),
fileName: makeSelectFileNameForUri(props.uri)(state), fileName: makeSelectFileNameForUri(props.uri)(state),
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state), streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
autoplay: makeSelectClientSetting(AUTOPLAY)(state), autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
nextUnplayed: makeSelectNextUnplayedRecommended(props.uri)(state),
isText: makeSelectIsText(props.uri)(state), isText: makeSelectIsText(props.uri)(state),
}); });
export default connect(select)(FileRender); const perform = dispatch => ({
setPlayingUri: uri => dispatch(doSetPlayingUri(uri)),
});
export default connect(
select,
perform
)(FileRender);

View file

@ -8,7 +8,7 @@ import ImageViewer from 'component/viewers/imageViewer';
import AppViewer from 'component/viewers/appViewer'; import AppViewer from 'component/viewers/appViewer';
import Button from 'component/button'; import Button from 'component/button';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { formatLbryUrlForWeb } from 'util/url'; import AutoplayCountdown from 'component/autoplayCountdown';
// @if TARGET='web' // @if TARGET='web'
import { generateStreamUrl } from 'util/lbrytv'; import { generateStreamUrl } from 'util/lbrytv';
// @endif // @endif
@ -31,23 +31,35 @@ type Props = {
mediaType: string, mediaType: string,
isText: true, isText: true,
streamingUrl: string, streamingUrl: string,
embedded?: boolean,
contentType: string, contentType: string,
claim: StreamClaim, claim: StreamClaim,
currentTheme: string, currentTheme: string,
downloadPath: string, downloadPath: string,
fileName: string, fileName: string,
autoplay: boolean, autoplay: boolean,
nextFileToPlay: string, setPlayingUri: (string | null) => void,
nextUnplayed: string, currentlyFloating: boolean,
history: { push: string => void },
}; };
class FileRender extends React.PureComponent<Props> { type State = {
showAutoplayCountdown: boolean,
showEmbededMessage: boolean,
};
class FileRender extends React.PureComponent<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = {
showAutoplayCountdown: false,
showEmbededMessage: false,
};
(this: any).escapeListener = this.escapeListener.bind(this); (this: any).escapeListener = this.escapeListener.bind(this);
(this: any).onEndedCb = this.onEndedCb.bind(this); (this: any).onEndedAutoplay = this.onEndedAutoplay.bind(this);
(this: any).onEndedEmbedded = this.onEndedEmbedded.bind(this);
(this: any).getOnEndedCb = this.getOnEndedCb.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -72,11 +84,29 @@ class FileRender extends React.PureComponent<Props> {
remote.getCurrentWindow().setFullScreen(false); remote.getCurrentWindow().setFullScreen(false);
} }
onEndedCb() { getOnEndedCb() {
const { autoplay, nextUnplayed, history } = this.props; const { setPlayingUri, currentlyFloating, embedded } = this.props;
if (autoplay && nextUnplayed) {
history.push(formatLbryUrlForWeb(nextUnplayed)); if (embedded) {
return this.onEndedEmbedded;
} }
if (!currentlyFloating) {
return this.onEndedAutoplay;
}
return () => setPlayingUri(null);
}
onEndedAutoplay() {
const { autoplay } = this.props;
if (autoplay) {
this.setState({ showAutoplayCountdown: true });
}
}
onEndedEmbedded() {
this.setState({ showEmbededMessage: true });
} }
renderViewer() { renderViewer() {
@ -98,8 +128,8 @@ class FileRender extends React.PureComponent<Props> {
application: <AppViewer uri={uri} />, application: <AppViewer uri={uri} />,
// @endif // @endif
video: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.onEndedCb} />, video: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
audio: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.onEndedCb} />, audio: <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />,
image: <ImageViewer uri={uri} source={source} />, image: <ImageViewer uri={uri} source={source} />,
// Add routes to viewer... // Add routes to viewer...
}; };
@ -107,10 +137,10 @@ class FileRender extends React.PureComponent<Props> {
// Supported contentTypes // Supported contentTypes
const contentTypes = { const contentTypes = {
'application/x-ext-mkv': ( 'application/x-ext-mkv': (
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.onEndedCb} /> <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
), ),
'video/x-matroska': ( 'video/x-matroska': (
<VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.onEndedCb} /> <VideoViewer uri={uri} source={source} contentType={contentType} onEndedCB={this.getOnEndedCb()} />
), ),
'application/pdf': <PdfViewer source={downloadPath || source} />, 'application/pdf': <PdfViewer source={downloadPath || source} />,
'text/html': <HtmlViewer source={downloadPath || source} />, 'text/html': <HtmlViewer source={downloadPath || source} />,
@ -186,12 +216,30 @@ class FileRender extends React.PureComponent<Props> {
// Return viewer // Return viewer
return viewer || unsupported; return viewer || unsupported;
} }
render() { render() {
const { isText } = this.props; const { isText, uri, currentlyFloating, embedded } = this.props;
const { showAutoplayCountdown, showEmbededMessage } = this.state;
return ( return (
<div className={classnames('file-render', { 'file-render--document': isText })}> <div
className={classnames({
'file-render': !embedded,
'file-render--document': isText && !embedded,
'file-render__embed': embedded,
})}
>
{embedded && showEmbededMessage && (
<div className="video-overlay__wrapper">
<div className="video-overlay__title">{__('View More on lbry.tv')}</div>
<div className="video-overlay__actions">
<div className="section__actions--centered">
<Button label={__('Explore')} button="primary" href="https://lbry.tv?src=embed" />
</div>
</div>
</div>
)}
{!currentlyFloating && showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
<Suspense fallback={<div />}>{this.renderViewer()}</Suspense> <Suspense fallback={<div />}>{this.renderViewer()}</Suspense>
</div> </div>
); );

View file

@ -175,7 +175,11 @@ export default function FileViewer(props: Props) {
</div> </div>
)} )}
{isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={loadingMessage} />} {isReadyToPlay ? (
<FileRender currentlyFloating={!inline} uri={uri} />
) : (
<LoadingScreen status={loadingMessage} />
)}
{!inline && ( {!inline && (
<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}>

View file

@ -165,14 +165,6 @@ const Header = (props: Props) => {
<Icon aria-hidden icon={ICONS.CHANNEL} /> <Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')} {__('Channels')}
</MenuItem> </MenuItem>
{/* @if TARGET='app' */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIBRARY}`)}>
<Icon aria-hidden icon={ICONS.LIBRARY} />
{__('Library')}
</MenuItem>
{/* @endif */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} /> <Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')} {__('Rewards')}

View file

@ -113,6 +113,7 @@ function PublishName(props: Props) {
error={bidError} error={bidError}
disabled={!name} disabled={!name}
onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })} onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })}
onWheel={e => e.stopPropagation()}
helper={ helper={
<BidHelpText <BidHelpText
uri={'lbry://' + name} uri={'lbry://' + name}

View file

@ -28,6 +28,7 @@ import FourOhFourPage from 'page/fourOhFour';
import SignInPage from 'page/signIn'; import SignInPage from 'page/signIn';
import SignInVerifyPage from 'page/signInVerify'; import SignInVerifyPage from 'page/signInVerify';
import ChannelsPage from 'page/channels'; import ChannelsPage from 'page/channels';
import EmbedWrapperPage from 'page/embedWrapper';
// Tell the browser we are handling scroll restoration // Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
@ -105,6 +106,9 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} /> <PrivateRoute {...props} path={`/$/${PAGES.CHANNELS}`} component={ChannelsPage} />
<Route path={`/$/${PAGES.EMBED}/:claimName`} exact component={EmbedWrapperPage} />
<Route path={`/$/${PAGES.EMBED}/:claimName/:claimId`} exact component={EmbedWrapperPage} />
{/* Below need to go at the end to make sure we don't match any of our pages first */} {/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} /> <Route path="/:claimName" exact component={ShowPage} />
<Route path="/:claimName/:streamName" exact component={ShowPage} /> <Route path="/:claimName/:streamName" exact component={ShowPage} />

View file

@ -112,6 +112,11 @@ function SideNavigation(props: Props) {
{ {
...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER), ...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
}, },
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
].map(linkProps => ( ].map(linkProps => (
<li key={linkProps.navigate}> <li key={linkProps.navigate}>
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" /> <Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
@ -120,11 +125,6 @@ function SideNavigation(props: Props) {
{expanded && {expanded &&
[ [
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{ {
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL), ...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
}, },

View file

@ -3,7 +3,7 @@ import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import CopyableText from 'component/copyableText'; import CopyableText from 'component/copyableText';
import EmbedArea from 'component/embedArea'; import EmbedTextArea from 'component/embedTextArea';
type Props = { type Props = {
claim: Claim, claim: Claim,
@ -58,7 +58,7 @@ class SocialShare extends React.PureComponent<Props> {
href={`https://twitter.com/intent/tweet?text=${encodedLbryURL}`} href={`https://twitter.com/intent/tweet?text=${encodedLbryURL}`}
/> />
</div> </div>
{webShareable && !isChannel && <EmbedArea label={__('Embedded')} claim={claim} noSnackbar />} {webShareable && !isChannel && <EmbedTextArea label={__('Embedded')} claim={claim} noSnackbar />}
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -28,7 +28,8 @@ class TransactionListItem extends React.PureComponent<Props> {
if (type === TXN_TYPES.TIP) { if (type === TXN_TYPES.TIP) {
return <Button button="secondary" icon={ICONS.UNLOCK} onClick={this.abandonClaim} title={__('Unlock Tip')} />; return <Button button="secondary" icon={ICONS.UNLOCK} onClick={this.abandonClaim} title={__('Unlock Tip')} />;
} }
return <Button button="secondary" icon={ICONS.DELETE} onClick={this.abandonClaim} title={__('Abandon Claim')} />; const abandonTitle = type === TXN_TYPES.SUPPORT ? 'Abandon Support' : 'Abandon Claim';
return <Button button="secondary" icon={ICONS.DELETE} onClick={this.abandonClaim} title={__(abandonTitle)} />;
} }
abandonClaim() { abandonClaim() {

View file

@ -38,7 +38,6 @@ type Props = {
changeVolume: number => void, changeVolume: number => void,
savePosition: (string, number) => void, savePosition: (string, number) => void,
changeMute: boolean => void, changeMute: boolean => void,
setPlayingUri: (string | null) => void,
source: string, source: string,
contentType: string, contentType: string,
thumbnail: string, thumbnail: string,
@ -48,18 +47,7 @@ type Props = {
}; };
function VideoViewer(props: Props) { function VideoViewer(props: Props) {
const { const { contentType, source, onEndedCB, changeVolume, changeMute, volume, muted, thumbnail, claim } = props;
contentType,
source,
setPlayingUri,
onEndedCB,
changeVolume,
changeMute,
volume,
muted,
thumbnail,
claim,
} = props;
const claimId = claim && claim.claim_id; const claimId = claim && claim.claim_id;
const videoRef = useRef(); const videoRef = useRef();
const isAudio = contentType.includes('audio'); const isAudio = contentType.includes('audio');
@ -85,13 +73,13 @@ function VideoViewer(props: Props) {
} }
function doEnded() { function doEnded() {
// clear position
setPlayingUri(null);
onEndedCB(); onEndedCB();
} }
function doPause(e: Event) { function doPause(e: Event) {
// store position e.target.currentTime // store position e.target.currentTime
} }
function doVolume(e: Event) { function doVolume(e: Event) {
// $FlowFixMe volume is missing in EventTarget // $FlowFixMe volume is missing in EventTarget
changeVolume(e.target.volume); changeVolume(e.target.volume);

View file

@ -27,3 +27,4 @@ exports.CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
exports.WALLET = 'wallet'; exports.WALLET = 'wallet';
exports.BLOCKED = 'blocked'; exports.BLOCKED = 'blocked';
exports.CHANNELS = 'channels'; exports.CHANNELS = 'channels';
exports.EMBED = 'embed';

View file

@ -3,7 +3,13 @@ import { useState, useEffect } from 'react';
export default function usePersistedState(key, firstTimeDefault) { export default function usePersistedState(key, firstTimeDefault) {
// If no key is passed in, act as a normal `useState` // If no key is passed in, act as a normal `useState`
let defaultValue; let defaultValue;
if (key) { let localStorageAvailable;
try {
localStorageAvailable = Boolean(window.localStorage);
} catch (e) {
localStorageAvailable = false;
}
if (key && localStorageAvailable) {
let item = localStorage.getItem(key); let item = localStorage.getItem(key);
if (item) { if (item) {
@ -27,7 +33,7 @@ export default function usePersistedState(key, firstTimeDefault) {
const [value, setValue] = useState(defaultValue); const [value, setValue] = useState(defaultValue);
useEffect(() => { useEffect(() => {
if (key) { if (key && localStorageAvailable) {
localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : value); localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : value);
} }
}, [key, value]); }, [key, value]);

View file

@ -4,6 +4,12 @@ let fs = require('fs');
const isProduction = process.env.NODE_ENV === 'production'; const isProduction = process.env.NODE_ENV === 'production';
let knownMessages = null; let knownMessages = null;
let localStorageAvailable;
try {
localStorageAvailable = Boolean(window.localStorage);
} catch (e) {
localStorageAvailable = false;
}
window.i18n_messages = window.i18n_messages || {}; window.i18n_messages = window.i18n_messages || {};
@ -44,7 +50,9 @@ function saveMessage(message) {
// @endif // @endif
export function __(message, tokens) { export function __(message, tokens) {
const language = window.localStorage.getItem('language') || 'en'; const language = localStorageAvailable
? window.localStorage.getItem('language') || 'en'
: window.navigator.language.slice(0, 2) || 'en';
if (!isProduction) { if (!isProduction) {
saveMessage(message); saveMessage(message);

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import EmbedWrapperPage from './view';
import { doResolveUri, makeSelectClaimForUri, buildURI } from 'lbry-redux';
const select = (state, props) => {
const { match } = props;
const { params } = match;
const { claimName, claimId } = params;
const uri = claimName && claimId ? buildURI({ claimName, claimId }) : '';
return {
uri,
claim: makeSelectClaimForUri(uri)(state),
};
};
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)),
});
export default connect(
select,
perform
)(EmbedWrapperPage);

View file

@ -0,0 +1,30 @@
// @flow
import React, { useEffect } from 'react';
import FileRender from 'component/fileRender';
import Spinner from 'component/spinner';
type Props = {
uri: string,
resolveUri: string => void,
claim: Claim,
};
const EmbedWrapperPage = (props: Props) => {
const { resolveUri, claim, uri } = props;
useEffect(() => {
if (resolveUri && uri) {
resolveUri(uri);
}
}, []);
if (uri && claim) {
return (
<div className={'embed__wrapper'}>
<FileRender uri={uri} embedded />
</div>
);
} else {
return <Spinner />;
}
};
export default EmbedWrapperPage;

View file

@ -45,12 +45,14 @@ const defaultState: AppState = {
modal: null, modal: null,
modalProps: {}, modalProps: {},
platform: process.platform, platform: process.platform,
upgradeSkipped: sessionStorage.getItem('upgradeSkipped') === 'true',
daemonVersionMatched: null, daemonVersionMatched: null,
daemonReady: false, daemonReady: false,
hasSignature: false, hasSignature: false,
badgeNumber: 0, badgeNumber: 0,
volume: Number(sessionStorage.getItem('volume')) || 1, volume: 1,
// @if TARGET='app'
upgradeSkipped: sessionStorage.getItem('upgradeSkipped') === 'true',
// @endif
muted: false, muted: false,
autoUpdateDownloaded: false, autoUpdateDownloaded: false,
autoUpdateDeclined: false, autoUpdateDeclined: false,
@ -71,6 +73,10 @@ const defaultState: AppState = {
// @@router comes from react-router // @@router comes from react-router
// This action is dispatched any time a user navigates forward or back // This action is dispatched any time a user navigates forward or back
try {
defaultState.volume = Number(sessionStorage.getItem('volume'));
} catch (e) {}
reducers['@@router/LOCATION_CHANGE'] = (state, action) => { reducers['@@router/LOCATION_CHANGE'] = (state, action) => {
const { currentScroll } = state; const { currentScroll } = state;
const scrollHistory = (state.scrollHistory && state.scrollHistory.slice()) || []; const scrollHistory = (state.scrollHistory && state.scrollHistory.slice()) || [];

View file

@ -5,6 +5,14 @@ import SUPPORTED_LANGUAGES from 'constants/supported_languages';
import { ACTIONS as LBRY_REDUX_ACTIONS, SHARED_PREFERENCES } from 'lbry-redux'; import { ACTIONS as LBRY_REDUX_ACTIONS, SHARED_PREFERENCES } from 'lbry-redux';
const reducers = {}; const reducers = {};
let settingLanguage = [];
try {
let appLanguage = window.localStorage.getItem(SETTINGS.LANGUAGE);
settingLanguage.push(appLanguage);
} catch (e) {}
settingLanguage.push(window.navigator.language.slice(0, 2));
settingLanguage.push('en');
const defaultState = { const defaultState = {
isNight: false, isNight: false,
loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'], loadedLanguages: [...Object.keys(window.i18n_messages), 'en'] || ['en'],
@ -21,11 +29,7 @@ const defaultState = {
[SETTINGS.ENABLE_SYNC]: true, [SETTINGS.ENABLE_SYNC]: true,
// UI // UI
[SETTINGS.LANGUAGE]: [ [SETTINGS.LANGUAGE]: settingLanguage.find(language => SUPPORTED_LANGUAGES[language]),
window.localStorage.getItem(SETTINGS.LANGUAGE),
window.navigator.language.slice(0, 2),
'en',
].find(language => SUPPORTED_LANGUAGES[language]),
[SETTINGS.THEME]: __('light'), [SETTINGS.THEME]: __('light'),
[SETTINGS.THEMES]: [__('light'), __('dark')], [SETTINGS.THEMES]: [__('light'), __('dark')],
[SETTINGS.SUPPORT_OPTION]: false, [SETTINGS.SUPPORT_OPTION]: false,
@ -111,7 +115,7 @@ reducers[LBRY_REDUX_ACTIONS.SHARED_PREFERENCE_SET] = (state, action) => {
return Object.assign({}, state, { return Object.assign({}, state, {
sharedPreferences, sharedPreferences,
}); });
} };
reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => { reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
const { key, value } = action.data; const { key, value } = action.data;
@ -124,10 +128,7 @@ reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
}); });
}; };
reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = ( reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => {
state,
action,
) => {
const { settings: sharedPreferences } = action.data; const { settings: sharedPreferences } = action.data;
// process clientSettings and daemonSettings // process clientSettings and daemonSettings
@ -135,8 +136,8 @@ reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (
}; };
reducers[LBRY_REDUX_ACTIONS.SAVE_CUSTOM_WALLET_SERVERS] = (state, action) => { reducers[LBRY_REDUX_ACTIONS.SAVE_CUSTOM_WALLET_SERVERS] = (state, action) => {
return Object.assign({}, state, {customWalletServers: action.data}); return Object.assign({}, state, { customWalletServers: action.data });
} };
export default function reducer(state = defaultState, action) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];

View file

@ -15,6 +15,7 @@
@import 'component/comments'; @import 'component/comments';
@import 'component/content'; @import 'component/content';
@import 'component/dat-gui'; @import 'component/dat-gui';
@import 'component/embed-player';
@import 'component/expandable'; @import 'component/expandable';
@import 'component/file-properties'; @import 'component/file-properties';
@import 'component/file-render'; @import 'component/file-render';

View file

@ -302,6 +302,12 @@
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
@media (max-width: $breakpoint-medium) {
.channel-thumbnail {
display: none;
}
}
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
width: calc((100% - var(--spacing-medium) * 1) / 2); width: calc((100% - var(--spacing-medium) * 1) / 2);
margin-bottom: var(--spacing-large); margin-bottom: var(--spacing-large);

View file

@ -0,0 +1,9 @@
.embed__wrapper {
height: 100vh;
width: 100vw;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}

View file

@ -153,3 +153,92 @@
} }
} }
} }
.file-render__embed {
height: 100%;
width: 100%;
position: fixed;
.video-js {
display: flex;
align-items: center;
justify-content: center;
.vjs-big-play-button {
@extend .button--icon;
@extend .button--play;
border: none;
position: static;
z-index: 2;
.vjs-icon-placeholder {
display: none;
}
}
}
}
.video-overlay__wrapper {
position: absolute;
left: auto;
right: auto;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.9);
z-index: 999;
color: var(--color-white);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-large);
.button--uri-indicator {
color: var(--color-gray-3);
}
@media (max-width: $breakpoint-small) {
align-items: flex-start;
padding: var(--spacing-small);
.button,
.video-overlay__subtitle,
.video-overlay__actions {
font-size: var(--font-small);
}
}
}
.video-overlay__title {
font-size: var(--font-title);
font-weight: var(--font-weight-light);
margin-top: var(--spacing-medium);
margin-bottom: var(--spacing-small);
@media (max-width: $breakpoint-small) {
margin: 0;
font-size: var(--font-medium);
}
}
.video-overlay__subtitle {
color: var(--color-gray-3);
margin: var(--spacing-medium) 0;
line-height: 1;
@media (max-width: $breakpoint-small) {
margin: 0;
}
}
.video-overlay__actions {
margin-top: var(--spacing-large);
.button--link {
color: var(--color-white);
}
@media (max-width: $breakpoint-small) {
margin-top: var(--spacing-small);
}
}

View file

@ -15,7 +15,7 @@
height: 100%; height: 100%;
position: absolute; position: absolute;
z-index: 1; z-index: 1;
stroke: var(--color-input-placeholder); stroke: white;
} }
} }
@ -53,6 +53,13 @@
padding-right: var(--spacing-small); padding-right: var(--spacing-small);
padding-left: 2.5rem; padding-left: 2.5rem;
transition: all 0.2s; transition: all 0.2s;
background-color: #677e87;
color: white;
&::placeholder {
color: white;
opacity: 0.8;
}
&:focus { &:focus {
border-radius: var(--border-radius); border-radius: var(--border-radius);

View file

@ -92,6 +92,15 @@
margin-top: var(--spacing-medium); margin-top: var(--spacing-medium);
} }
.section__actions--centered {
@extend .section__actions;
justify-content: center;
@media (max-width: $breakpoint-small) {
justify-content: flex-start;
}
}
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
.section__actions { .section__actions {
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -4,6 +4,7 @@ $spacing-width: 36px;
$breakpoint-xsmall: 600px; $breakpoint-xsmall: 600px;
$breakpoint-small: 900px; $breakpoint-small: 900px;
$breakpoint-medium: 1150px;
:root { :root {
// Width & spacing // Width & spacing