floating player actually working
This commit is contained in:
parent
90bcde49e7
commit
2b09d56b63
39 changed files with 582 additions and 271 deletions
|
@ -148,6 +148,7 @@
|
|||
"rc-progress": "^2.0.6",
|
||||
"react": "^16.8.2",
|
||||
"react-dom": "^16.8.2",
|
||||
"react-draggable": "^3.3.0",
|
||||
"react-ga": "^2.5.7",
|
||||
"react-hot-loader": "^4.11.1",
|
||||
"react-modal": "^3.1.7",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { hot } from 'react-hot-loader/root';
|
||||
import { connect } from 'react-redux';
|
||||
import { doUpdateBlockHeight, doError, doFetchTransactions } from 'lbry-redux';
|
||||
import { doError, doFetchTransactions } from 'lbry-redux';
|
||||
import { selectUser, doRewardList, doFetchRewardedContent, doFetchAccessToken, selectAccessToken } from 'lbryinc';
|
||||
import { selectThemePath } from 'redux/selectors/settings';
|
||||
import App from './view';
|
||||
|
@ -13,7 +13,6 @@ const select = state => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
alertError: errorList => dispatch(doError(errorList)),
|
||||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import analytics from 'analytics';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import { Lbry, buildURI } from 'lbry-redux';
|
||||
import Router from 'component/router/index';
|
||||
import ModalRouter from 'modal/modalRouter';
|
||||
import ReactModal from 'react-modal';
|
||||
|
@ -10,6 +10,8 @@ import Header from 'component/header';
|
|||
import { openContextMenu } from 'util/context-menu';
|
||||
import useKonamiListener from 'util/enhanced-layout';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import FileViewer from 'component/fileViewer';
|
||||
import { withRouter } from 'react-router';
|
||||
|
||||
export const MAIN_WRAPPER_CLASS = 'main-wrapper';
|
||||
|
||||
|
@ -20,6 +22,7 @@ type Props = {
|
|||
theme: string,
|
||||
accessToken: ?string,
|
||||
user: ?{ id: string, has_verified_email: boolean },
|
||||
location: { pathname: string },
|
||||
fetchRewards: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
fetchTransactions: () => void,
|
||||
|
@ -33,6 +36,18 @@ function App(props: Props) {
|
|||
const userId = user && user.id;
|
||||
const hasVerifiedEmail = user && user.has_verified_email;
|
||||
|
||||
const { pathname } = props.location;
|
||||
const urlParts = pathname.split('/');
|
||||
const claimName = urlParts[1];
|
||||
const claimId = urlParts[2];
|
||||
|
||||
// @routingfixme
|
||||
// claimName and claimId come from the url `{domain}/{claimName}/{claimId}"
|
||||
let uri;
|
||||
try {
|
||||
uri = buildURI({ contentName: claimName, claimId: claimId });
|
||||
} catch (e) {}
|
||||
|
||||
useEffect(() => {
|
||||
ReactModal.setAppElement(appRef.current);
|
||||
fetchAccessToken();
|
||||
|
@ -42,7 +57,7 @@ function App(props: Props) {
|
|||
fetchRewards();
|
||||
fetchTransactions();
|
||||
// @endif
|
||||
}, [fetchRewards, fetchRewardedContent, fetchTransactions]);
|
||||
}, [fetchRewards, fetchRewardedContent, fetchTransactions, fetchAccessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
// $FlowFixMe
|
||||
|
@ -73,9 +88,11 @@ function App(props: Props) {
|
|||
</div>
|
||||
|
||||
<ModalRouter />
|
||||
<FileViewer pageUri={uri} />
|
||||
|
||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default withRouter(App);
|
||||
|
|
|
@ -127,7 +127,12 @@ const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
title={title}
|
||||
aria-label={description || label || title}
|
||||
className={combinedClassName}
|
||||
onClick={onClick}
|
||||
onClick={e => {
|
||||
if (onClick) {
|
||||
e.stopPropagation();
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
type={type}
|
||||
{...otherProps}
|
||||
|
|
|
@ -276,4 +276,11 @@ export const icons = {
|
|||
<polyline points="17 6 23 6 23 12" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.VIEW]: buildIcon(
|
||||
<g>
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
|
||||
<polyline points="10 17 15 12 10 7" />
|
||||
<line x1="15" y1="12" x2="3" y2="12" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import * as React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import Button from 'component/button';
|
||||
import { withRouter } from 'react-router';
|
||||
|
@ -8,7 +9,7 @@ import Native from 'native';
|
|||
import { Lbry } from 'lbry-redux';
|
||||
|
||||
type Props = {
|
||||
children: React.Node,
|
||||
children: Node,
|
||||
history: {
|
||||
replace: string => void,
|
||||
},
|
||||
|
@ -73,7 +74,7 @@ class ErrorBoundary extends React.Component<Props, State> {
|
|||
type="sad"
|
||||
title={__('Aw shucks!')}
|
||||
subtitle={
|
||||
<div>
|
||||
<Fragment>
|
||||
<p>
|
||||
{__("There was an error. It's been reported and will be fixed")}. {__('Try')}{' '}
|
||||
<Button
|
||||
|
@ -84,7 +85,7 @@ class ErrorBoundary extends React.Component<Props, State> {
|
|||
/>{' '}
|
||||
{__('to fix it')}.
|
||||
</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -16,8 +16,9 @@ type Props = {
|
|||
class FileActions extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { fileInfo, uri, openModal, claimIsMine, claimId } = this.props;
|
||||
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed === true));
|
||||
|
||||
const showDelete =
|
||||
claimIsMine ||
|
||||
(fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed === fileInfo.blobs_in_stream));
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showDelete && (
|
||||
|
|
|
@ -94,9 +94,12 @@ class FileRender extends React.PureComponent<Props> {
|
|||
|
||||
renderViewer() {
|
||||
const { mediaType, currentTheme, claim, contentType, downloadPath, fileName, streamingUrl, uri } = this.props;
|
||||
|
||||
const fileType = fileName && path.extname(fileName).substring(1);
|
||||
|
||||
// Ideally the lbrytv api server would just replace the streaming_url returned by the sdk so we don't need this check
|
||||
// https://github.com/lbryio/lbrytv/issues/51
|
||||
const source = IS_WEB ? `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream` : streamingUrl;
|
||||
|
||||
// Human-readable files (scripts and plain-text files)
|
||||
const readableFiles = ['text', 'document', 'script'];
|
||||
|
||||
|
@ -108,17 +111,19 @@ class FileRender extends React.PureComponent<Props> {
|
|||
application: <AppViewer uri={uri} />,
|
||||
// @endif
|
||||
|
||||
video: <VideoViewer source={streamingUrl} contentType={contentType} />,
|
||||
audio: <VideoViewer source={streamingUrl} contentType={contentType} />,
|
||||
image: <ImageViewer source={streamingUrl} />,
|
||||
video: <VideoViewer uri={uri} source={source} contentType={contentType} />,
|
||||
audio: <VideoViewer uri={uri} source={source} contentType={contentType} />,
|
||||
image: <ImageViewer uri={uri} source={source} />,
|
||||
// Add routes to viewer...
|
||||
};
|
||||
|
||||
// Supported fileType
|
||||
const fileTypes = {
|
||||
// @if TARGET='app'
|
||||
pdf: <PdfViewer source={downloadPath} />,
|
||||
docx: <DocxViewer source={downloadPath} />,
|
||||
html: <HtmlViewer source={downloadPath} />,
|
||||
// @endif
|
||||
// Add routes to viewer...
|
||||
};
|
||||
|
||||
|
@ -151,7 +156,7 @@ class FileRender extends React.PureComponent<Props> {
|
|||
// @endif
|
||||
|
||||
// Message Error
|
||||
const unsupportedMessage = __("We can't preview this file.");
|
||||
const unsupportedMessage = __("Sorry, we can't preview this file.");
|
||||
const unsupported = <LoadingScreen status={unsupportedMessage} spinner={false} />;
|
||||
|
||||
// Return viewer
|
||||
|
|
|
@ -1,30 +1,42 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import { doPlayUri } from 'redux/actions/content';
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
makeSelectUriIsStreamable,
|
||||
makeSelectTitleForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectIsPlaying, makeSelectShouldObscurePreview } from 'redux/selectors/content';
|
||||
import { makeSelectIsPlaying, makeSelectShouldObscurePreview, selectPlayingUri } from 'redux/selectors/content';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doSetPlayingUri } from 'redux/actions/content';
|
||||
import { withRouter } from 'react-router';
|
||||
import FileViewer from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const uri = selectPlayingUri(state);
|
||||
return {
|
||||
uri,
|
||||
title: makeSelectTitleForUri(uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(uri)(state),
|
||||
mediaType: makeSelectMediaTypeForUri(uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(uri)(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
||||
isStreamable: makeSelectUriIsStreamable(uri)(state),
|
||||
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
play: uri => dispatch(doPlayUri(uri)),
|
||||
clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer);
|
||||
export default withRouter(
|
||||
connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer)
|
||||
);
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
// @flow
|
||||
import React, { Fragment, useEffect, useCallback } from 'react';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { useEffect } from 'react';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import Button from 'component/button';
|
||||
import FileRender from 'component/fileRender';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
|
||||
const SPACE_BAR_KEYCODE = 32;
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'util/use-persisted-state';
|
||||
import { FILE_WRAPPER_CLASS } from 'page/file/view';
|
||||
import Draggable from 'react-draggable';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
|
||||
type Props = {
|
||||
play: (string, boolean) => void,
|
||||
mediaType: string,
|
||||
isLoading: boolean,
|
||||
isPlaying: boolean,
|
||||
|
@ -20,87 +22,122 @@ type Props = {
|
|||
isStreamable: boolean,
|
||||
thumbnail?: string,
|
||||
streamingUrl?: string,
|
||||
floatingPlayer: boolean,
|
||||
pageUri: ?string,
|
||||
title: ?string,
|
||||
floatingPlayerEnabled: boolean,
|
||||
clearPlayingUri: () => void,
|
||||
};
|
||||
|
||||
export default function FileViewer(props: Props) {
|
||||
const {
|
||||
play,
|
||||
mediaType,
|
||||
isPlaying,
|
||||
fileInfo,
|
||||
uri,
|
||||
obscurePreview,
|
||||
insufficientCredits,
|
||||
thumbnail,
|
||||
streamingUrl,
|
||||
isStreamable,
|
||||
pageUri,
|
||||
title,
|
||||
clearPlayingUri,
|
||||
floatingPlayerEnabled,
|
||||
} = props;
|
||||
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
|
||||
const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
|
||||
x: -25,
|
||||
y: window.innerHeight - 400,
|
||||
});
|
||||
|
||||
const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||
const fileStatus = fileInfo && fileInfo.status;
|
||||
const isReadyToPlay = (isStreamable && streamingUrl) || (fileInfo && fileInfo.completed);
|
||||
const inline = pageUri === uri;
|
||||
const isReadyToPlay = (IS_WEB && isStreamable) || (isStreamable && streamingUrl) || (fileInfo && fileInfo.completed);
|
||||
const loadingMessage =
|
||||
!isStreamable && fileInfo && fileInfo.blobs_completed >= 1 && (!fileInfo.download_path || !fileInfo.written_bytes)
|
||||
? __("It looks like you deleted or moved this file. We're rebuilding it now. It will only take a few seconds.")
|
||||
: __('Loading');
|
||||
|
||||
// Wrap this in useCallback because we need to use it to the keyboard effect
|
||||
// If we don't a new instance will be created for every render and react will think the dependencies have change, which will add/remove the listener for every render
|
||||
const viewFile = useCallback(
|
||||
(e: SyntheticInputEvent<*> | KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Check for user setting here
|
||||
const saveFile = !isStreamable;
|
||||
|
||||
play(uri, saveFile);
|
||||
},
|
||||
[play, uri, isStreamable]
|
||||
);
|
||||
function handleDrag(e, ui) {
|
||||
const { x, y } = position;
|
||||
const newX = x + ui.deltaX;
|
||||
const newY = y + ui.deltaY;
|
||||
setPosition({
|
||||
x: newX,
|
||||
y: newY,
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// This is just for beginning to download a file
|
||||
// Play/Pause/Fullscreen will be handled by the respective viewers because not every file type should behave the same
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (!isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isPlaying || fileStatus === 'stopped') {
|
||||
viewFile(e);
|
||||
}
|
||||
function handleResize() {
|
||||
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
|
||||
if (!element) {
|
||||
throw new Error("Can't find file viewer wrapper to attach to");
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
// @FlowFixMe
|
||||
setFileViewerRect(rect);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isPlaying, fileStatus, viewFile]);
|
||||
if (inline) {
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}
|
||||
}, [setFileViewerRect, inline]);
|
||||
|
||||
const hidePlayer = !isPlaying || !uri || (!inline && (!floatingPlayerEnabled || !isStreamable));
|
||||
if (hidePlayer) {
|
||||
clearPlayingUri();
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={viewFile}
|
||||
style={!obscurePreview && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||
className={classnames('video content__cover content__embedded', {
|
||||
'card__media--nsfw': obscurePreview,
|
||||
'card__media--disabled': !fileInfo && insufficientCredits,
|
||||
})}
|
||||
<Draggable
|
||||
onDrag={handleDrag}
|
||||
defaultPosition={position}
|
||||
position={inline ? { x: 0, y: 0 } : position}
|
||||
bounds="parent"
|
||||
disabled={inline}
|
||||
handle=".content__info"
|
||||
cancel=".button"
|
||||
>
|
||||
{isPlaying && (
|
||||
<Fragment>{isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={loadingMessage} />}</Fragment>
|
||||
)}
|
||||
|
||||
{!isPlaying && (
|
||||
<Button
|
||||
onClick={viewFile}
|
||||
iconSize={30}
|
||||
title={isPlayable ? __('Play') : __('View')}
|
||||
className={classnames('button--icon', {
|
||||
'button--play': isPlayable,
|
||||
'button--view': !isPlayable,
|
||||
<div
|
||||
className={classnames('content__viewer', {
|
||||
'content__viewer--floating': !inline,
|
||||
})}
|
||||
style={
|
||||
inline && fileViewerRect
|
||||
? { width: fileViewerRect.width, height: fileViewerRect.height, left: fileViewerRect.x }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={classnames('content__wrapper', {
|
||||
'content__wrapper--floating': !inline,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
{!inline && (
|
||||
<div className="content__actions">
|
||||
<Tooltip label={__('View File')}>
|
||||
<Button navigate={uri} icon={ICONS.VIEW} button="close" className="content__hide-viewer" />
|
||||
</Tooltip>
|
||||
<Tooltip label={__('Close')}>
|
||||
<Button onClick={clearPlayingUri} icon={ICONS.REMOVE} button="close" className="content__hide-viewer" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isReadyToPlay ? <FileRender uri={uri} /> : <LoadingScreen status={loadingMessage} />}
|
||||
{!inline && (
|
||||
<div className="content__info">
|
||||
<div className="claim-preview-title" title={title || uri}>
|
||||
{title || uri}
|
||||
</div>
|
||||
<UriIndicator link addTooltip={false} uri={uri} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
);
|
||||
}
|
||||
|
|
37
src/ui/component/fileViewerInitiator/index.js
Normal file
37
src/ui/component/fileViewerInitiator/index.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { connect } from 'react-redux';
|
||||
import { doPlayUri, doSetPlayingUri } from 'redux/actions/content';
|
||||
import {
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectMediaTypeForUri,
|
||||
makeSelectUriIsStreamable,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectIsPlaying, makeSelectShouldObscurePreview, selectPlayingUri } from 'redux/selectors/content';
|
||||
import FileViewer from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||
mediaType: makeSelectMediaTypeForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscurePreview: makeSelectShouldObscurePreview(props.uri)(state),
|
||||
isPlaying: makeSelectIsPlaying(props.uri)(state),
|
||||
playingUri: selectPlayingUri(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(props.uri)(state),
|
||||
isStreamable: makeSelectUriIsStreamable(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(SETTINGS.AUTOPLAY)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
play: uri => {
|
||||
dispatch(doSetPlayingUri(uri));
|
||||
dispatch(doPlayUri(uri));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer);
|
94
src/ui/component/fileViewerInitiator/view.jsx
Normal file
94
src/ui/component/fileViewerInitiator/view.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// @flow
|
||||
// This component is entirely for triggering the start of a file view
|
||||
// The actual viewer for a file exists in FileViewer
|
||||
// They can't exist in one component because we need to handle/listen for the start of a new file view
|
||||
// while a file is currently being viewed
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
|
||||
const SPACE_BAR_KEYCODE = 32;
|
||||
|
||||
type Props = {
|
||||
play: string => void,
|
||||
mediaType: string,
|
||||
isLoading: boolean,
|
||||
isPlaying: boolean,
|
||||
fileInfo: FileListItem,
|
||||
uri: string,
|
||||
obscurePreview: boolean,
|
||||
insufficientCredits: boolean,
|
||||
isStreamable: boolean,
|
||||
thumbnail?: string,
|
||||
autoplay: boolean,
|
||||
};
|
||||
|
||||
export default function FileViewer(props: Props) {
|
||||
const { play, mediaType, isPlaying, fileInfo, uri, obscurePreview, insufficientCredits, thumbnail, autoplay } = props;
|
||||
|
||||
const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||
const fileStatus = fileInfo && fileInfo.status;
|
||||
|
||||
// Wrap this in useCallback because we need to use it to the keyboard effect
|
||||
// If we don't a new instance will be created for every render and react will think the dependencies have change, which will add/remove the listener for every render
|
||||
const viewFile = useCallback(
|
||||
(e?: SyntheticInputEvent<*> | KeyboardEvent) => {
|
||||
if (e) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
play(uri);
|
||||
},
|
||||
[play, uri]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// This is just for beginning to download a file
|
||||
// Play/Pause/Fullscreen will be handled by the respective viewers because not every file type should behave the same
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
if (!isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isPlaying || fileStatus === 'stopped') {
|
||||
viewFile(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isPlaying, fileStatus, viewFile]);
|
||||
|
||||
useEffect(() => {
|
||||
const videoOnPage = document.querySelector('video');
|
||||
if (autoplay && !videoOnPage) {
|
||||
viewFile();
|
||||
}
|
||||
}, [autoplay, viewFile]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={viewFile}
|
||||
style={!obscurePreview && thumbnail ? { backgroundImage: `url("${thumbnail}")` } : {}}
|
||||
className={classnames('content__cover', {
|
||||
'card__media--nsfw': obscurePreview,
|
||||
'card__media--disabled': !fileInfo && insufficientCredits,
|
||||
})}
|
||||
>
|
||||
{!isPlaying && (
|
||||
<Button
|
||||
onClick={viewFile}
|
||||
iconSize={30}
|
||||
title={isPlayable ? __('Play') : __('View')}
|
||||
className={classnames('button--icon', {
|
||||
'button--play': isPlayable,
|
||||
'button--view': !isPlayable,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -40,7 +40,10 @@ function AppViewer(props: Props) {
|
|||
return (
|
||||
<div className="file-render__viewer">
|
||||
{!appUrl && (
|
||||
<LoadingScreen status={loading ? __('Almost there') : __('Unable to view this file')} spinner={loading} />
|
||||
<LoadingScreen
|
||||
status={loading ? __('Almost there') : __('Unable to view this file in the app')}
|
||||
spinner={loading}
|
||||
/>
|
||||
)}
|
||||
{appUrl && (
|
||||
<webview
|
||||
|
|
|
@ -35,8 +35,10 @@ class CodeViewer extends React.PureComponent<Props> {
|
|||
const me = this;
|
||||
const { theme, contentType } = me.props;
|
||||
// Init CodeMirror
|
||||
import(/* webpackChunkName: "codemirror" */
|
||||
'codemirror/lib/codemirror').then(CodeMirror => {
|
||||
import(
|
||||
/* webpackChunkName: "codemirror" */
|
||||
'codemirror/lib/codemirror'
|
||||
).then(CodeMirror => {
|
||||
me.codeMirror = CodeMirror.fromTextArea(me.textarea, {
|
||||
// Auto detect syntax with file contentType
|
||||
mode: contentType,
|
||||
|
@ -62,7 +64,7 @@ class CodeViewer extends React.PureComponent<Props> {
|
|||
render() {
|
||||
const { value } = this.props;
|
||||
return (
|
||||
<div className="code-viewer" onContextMenu={stopContextMenu}>
|
||||
<div className="file-render__content" onContextMenu={stopContextMenu}>
|
||||
<textarea ref={textarea => (this.textarea = textarea)} disabled value={value} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,8 +5,10 @@ import LoadingScreen from 'component/common/loading-screen';
|
|||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
|
||||
const LazyCodeViewer = React.lazy<*>(() =>
|
||||
import(/* webpackChunkName: "codeViewer" */
|
||||
'component/viewers/codeViewer')
|
||||
import(
|
||||
/* webpackChunkName: "codeViewer" */
|
||||
'component/viewers/codeViewer'
|
||||
)
|
||||
);
|
||||
|
||||
type Props = {
|
||||
|
@ -81,7 +83,7 @@ class DocumentViewer extends React.PureComponent<Props, State> {
|
|||
const errorMessage = __("Sorry, looks like we can't load the document.");
|
||||
|
||||
return (
|
||||
<div className="file-render__viewer document-viewer">
|
||||
<div className="file-render__viewer--document">
|
||||
{loading && !error && <LoadingScreen status={loadingMessage} spinner />}
|
||||
{error && <LoadingScreen status={errorMessage} spinner={!error} />}
|
||||
{isReady && <Suspense fallback={<div />}>{this.renderDocument()}</Suspense>}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import React from 'react';
|
||||
import mammoth from 'mammoth';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
|
||||
type Props = {
|
||||
source: string,
|
||||
|
@ -59,10 +58,10 @@ class DocxViewer extends React.PureComponent<Props, State> {
|
|||
const errorMessage = __("Sorry, looks like we can't load the document.");
|
||||
|
||||
return (
|
||||
<div className="document-viewer file-render__viewer">
|
||||
<div className="file-render__viewer--document">
|
||||
{loading && <LoadingScreen status={loadingMessage} spinner />}
|
||||
{error && <LoadingScreen status={errorMessage} spinner={false} />}
|
||||
{content && <div className="document-viewer__content" dangerouslySetInnerHTML={{ __html: content }} />}
|
||||
{content && <div className="file-render__content" dangerouslySetInnerHTML={{ __html: content }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class PdfViewer extends React.PureComponent<Props> {
|
|||
shell.openExternal(path);
|
||||
// @endif
|
||||
// @if TARGET='web'
|
||||
console.error('provide stub for shell.openExternal');
|
||||
console.error('provide stub for shell.openExternal'); // eslint-disable-line
|
||||
// @endif
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ class PdfViewer extends React.PureComponent<Props> {
|
|||
// This was disabled on electron@3
|
||||
// https://github.com/electron/electron/issues/12337
|
||||
return (
|
||||
<div className="file-render__viewer file-render--pdf" onContextMenu={stopContextMenu}>
|
||||
<div className="file-render__viewer--pdf" onContextMenu={stopContextMenu}>
|
||||
<p>
|
||||
{__('PDF opened externally.')} <Button button="link" label={__('Click here')} onClick={this.openFile} />{' '}
|
||||
{__('to open it again.')}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectFileInfoForUri } from 'lbry-redux';
|
||||
import { doChangeVolume, doChangeMute } from 'redux/actions/app';
|
||||
import { selectVolume, selectMute } from 'redux/selectors/app';
|
||||
import { savePosition } from 'redux/actions/content';
|
||||
|
@ -9,11 +10,12 @@ const select = (state, props) => ({
|
|||
volume: selectVolume(state),
|
||||
position: makeSelectContentPositionForUri(props.uri)(state),
|
||||
muted: selectMute(state),
|
||||
hasFileInfo: Boolean(makeSelectFileInfoForUri(props.uri)(state)),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
changeVolume: volume => dispatch(doChangeVolume(volume)),
|
||||
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
|
||||
savePosition: (uri, position) => dispatch(savePosition(uri, position)),
|
||||
changeMute: muted => dispatch(doChangeMute(muted)),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React, { createRef, useEffect } from 'react';
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { stopContextMenu } from 'util/context-menu';
|
||||
import videojs from 'video.js';
|
||||
import 'video.js/dist/video-js.css';
|
||||
|
@ -10,20 +10,20 @@ const VIDEO_JS_OPTIONS = {
|
|||
autoplay: true,
|
||||
controls: true,
|
||||
preload: 'auto',
|
||||
playbackRates: [0.5, 1, 1.25, 1.5, 2],
|
||||
playbackRates: [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 2],
|
||||
};
|
||||
|
||||
type Props = {
|
||||
source: string,
|
||||
contentType: string,
|
||||
muted: boolean,
|
||||
hasFileInfo: boolean,
|
||||
};
|
||||
|
||||
function VideoViewer(props: Props) {
|
||||
const { contentType, source } = props;
|
||||
const videoRef = createRef();
|
||||
const videoRef = useRef();
|
||||
const [requireRedraw, setRequireRedraw] = useState(false);
|
||||
|
||||
// Handle any other effects separately to avoid re-mounting the video player when props change
|
||||
useEffect(() => {
|
||||
const videoNode = videoRef.current;
|
||||
const videoJsOptions = {
|
||||
|
@ -36,42 +36,57 @@ function VideoViewer(props: Props) {
|
|||
],
|
||||
};
|
||||
|
||||
const player = videojs(videoNode, videoJsOptions);
|
||||
let player;
|
||||
if (!requireRedraw) {
|
||||
player = videojs(videoNode, videoJsOptions);
|
||||
}
|
||||
|
||||
return () => {
|
||||
player.dispose();
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Video.js has a player.dispose() function that is meant to cleanup a previous video
|
||||
// We can't use this because it does some weird stuff to remove the video element from the page
|
||||
// This makes it really hard to use because the ref we keep still thinks it's on the page
|
||||
// requireRedraw just makes it so the video component is removed from the page _by react_
|
||||
// Then it's set to false immediately after so we can re-mount a new player
|
||||
setRequireRedraw(true);
|
||||
};
|
||||
}, [videoRef, source, contentType]);
|
||||
}, [videoRef, source, contentType, setRequireRedraw, requireRedraw]);
|
||||
|
||||
useEffect(() => {
|
||||
if (requireRedraw) {
|
||||
setRequireRedraw(false);
|
||||
}
|
||||
}, [requireRedraw]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
const videoNode = videoRef && videoRef.current;
|
||||
if (!videoNode) return;
|
||||
const videoNode = videoRef.current;
|
||||
|
||||
if (!isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
|
||||
if (videoNode && !isUserTyping() && e.keyCode === SPACE_BAR_KEYCODE) {
|
||||
e.preventDefault();
|
||||
|
||||
const isPaused = videoNode.paused;
|
||||
if (isPaused) {
|
||||
videoNode.play();
|
||||
return;
|
||||
}
|
||||
|
||||
videoNode.pause();
|
||||
videoNode.paused ? videoNode.play() : videoNode.pause();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [videoRef]);
|
||||
|
||||
// include requireRedraw here so the event listener is re-added when we need to manually remove/add the video player
|
||||
}, [videoRef, requireRedraw]);
|
||||
|
||||
return (
|
||||
<div className="file-render__viewer" onContextMenu={stopContextMenu}>
|
||||
<div data-vjs-player>
|
||||
<video ref={videoRef} className="video-js" />
|
||||
</div>
|
||||
{!requireRedraw && (
|
||||
<div data-vjs-player>
|
||||
<video ref={videoRef} className="video-js" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,7 +35,6 @@ export const WEB = 'Globe';
|
|||
export const SHARE = 'Share2';
|
||||
export const EXTERNAL = 'ExternalLink';
|
||||
export const TIP = 'Gift';
|
||||
export const VIEW = 'Eye';
|
||||
export const PLAY = 'Play';
|
||||
export const FACEBOOK = 'Facebook';
|
||||
export const TWITTER = 'Twitter';
|
||||
|
@ -73,3 +72,4 @@ export const TAG = 'Tag';
|
|||
export const SUPPORT = 'TrendingUp';
|
||||
export const BLOCK = 'Slash';
|
||||
export const UNBLOCK = 'Circle';
|
||||
export const VIEW = 'View';
|
||||
|
|
|
@ -19,3 +19,4 @@ export const AUTO_DOWNLOAD = 'autoDownload';
|
|||
export const SUPPORT_OPTION = 'supportOption';
|
||||
export const HIDE_BALANCE = 'hideBalance';
|
||||
export const HIDE_SPLASH_ANIMATION = 'hideSplashAnimation';
|
||||
export const FLOATING_PLAYER = 'floatingPlayer';
|
||||
|
|
|
@ -14,7 +14,10 @@ const perform = dispatch => ({
|
|||
dispatch(doHideModal());
|
||||
},
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
loadVideo: uri => dispatch(doPlayUri(uri, true)),
|
||||
loadVideo: uri => {
|
||||
dispatch(doSetPlayingUri(uri));
|
||||
dispatch(doPlayUri(uri, true));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import * as settings from 'constants/settings';
|
||||
import { selectRewardContentClaimIds, selectPlayingUri } from 'redux/selectors/content';
|
||||
import { selectRewardContentClaimIds } from 'redux/selectors/content';
|
||||
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { doSetContentHistoryItem } from 'redux/actions/content';
|
||||
|
@ -32,9 +32,7 @@ const select = (state, props) => ({
|
|||
obscureNsfw: !selectShowMatureContent(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
playingUri: selectPlayingUri(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
|
||||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
|
||||
viewCount: makeSelectViewCountForUri(props.uri)(state),
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as MODALS from 'constants/modal_types';
|
|||
import * as icons from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import { buildURI, normalizeURI } from 'lbry-redux';
|
||||
import FileViewer from 'component/fileViewer';
|
||||
import FileViewerInitiator from 'component/fileViewerInitiator';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
import FileActions from 'component/fileActions';
|
||||
|
@ -19,6 +19,8 @@ import CommentCreate from 'component/commentCreate';
|
|||
import ClaimUri from 'component/claimUri';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
|
||||
export const FILE_WRAPPER_CLASS = 'grid-area--content';
|
||||
|
||||
type Props = {
|
||||
claim: StreamClaim,
|
||||
fileInfo: FileListItem,
|
||||
|
@ -149,7 +151,7 @@ class FilePage extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<Page className="main--file-page">
|
||||
<div className="grid-area--content card">
|
||||
<div className={`card ${FILE_WRAPPER_CLASS}`}>
|
||||
{!fileInfo && insufficientCredits && (
|
||||
<div className="media__insufficient-credits help--warning">
|
||||
{__(
|
||||
|
@ -159,7 +161,7 @@ class FilePage extends React.Component<Props> {
|
|||
{__('or send more LBC to your wallet.')}
|
||||
</div>
|
||||
)}
|
||||
<FileViewer uri={uri} insufficientCredits={insufficientCredits} />
|
||||
<FileViewerInitiator uri={uri} insufficientCredits={insufficientCredits} />
|
||||
</div>
|
||||
|
||||
<div className="columns">
|
||||
|
|
|
@ -28,6 +28,7 @@ const select = state => ({
|
|||
supportOption: makeSelectClientSetting(settings.SUPPORT_OPTION)(state),
|
||||
userBlockedChannelsCount: selectBlockedChannelsCount(state),
|
||||
hideBalance: makeSelectClientSetting(settings.HIDE_BALANCE)(state),
|
||||
floatingPlayer: makeSelectClientSetting(settings.FLOATING_PLAYER)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -50,7 +50,8 @@ type Props = {
|
|||
supportOption: boolean,
|
||||
userBlockedChannelsCount?: number,
|
||||
hideBalance: boolean,
|
||||
maxConnections: number,
|
||||
floatingPlayer: boolean,
|
||||
clearPlayingUri: () => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -166,7 +167,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
supportOption,
|
||||
hideBalance,
|
||||
userBlockedChannelsCount,
|
||||
maxConnections,
|
||||
floatingPlayer,
|
||||
} = this.props;
|
||||
|
||||
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
|
||||
|
@ -174,7 +175,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
const defaultMaxKeyFee = { currency: 'USD', amount: 50 };
|
||||
|
||||
const disableMaxKeyFee = !(daemonSettings && daemonSettings.max_key_fee);
|
||||
const connectionOptions = [1, 4, 6, 10, 20];
|
||||
const connectionOptions = [1, 2, 4, 6, 10, 20];
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
@ -209,10 +210,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
name="save_files"
|
||||
onChange={() => setDaemonSetting('save_files', !daemonSettings.save_files)}
|
||||
checked={daemonSettings.save_files}
|
||||
label={__(
|
||||
'Enables saving of all viewed content to your downloads directory. Paid content and some file types are saved by default.'
|
||||
label={__('Save all viewed content to your downloads directory')}
|
||||
helper={__(
|
||||
'Paid content and some file types are saved by default. Changing this setting will not affect previously downloaded content.'
|
||||
)}
|
||||
helper={__('This is not retroactive, only works from the time it was changed.')}
|
||||
/>
|
||||
</Form>
|
||||
<Form>
|
||||
|
@ -221,13 +222,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
name="save_blobs"
|
||||
onChange={() => setDaemonSetting('save_blobs', !daemonSettings.save_blobs)}
|
||||
checked={daemonSettings.save_blobs}
|
||||
label={
|
||||
label={__('Save hosting data to help the LBRY network')}
|
||||
helper={
|
||||
<React.Fragment>
|
||||
{__('Enables saving of hosting data to help the LBRY network.')}{' '}
|
||||
{__("If disabled, LBRY will be very sad and you won't be helping improve the network.")}{' '}
|
||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/host-content" />.
|
||||
</React.Fragment>
|
||||
}
|
||||
helper={__("If disabled, LBRY will be very sad and you won't be helping improve the network")}
|
||||
/>
|
||||
</Form>
|
||||
</section>
|
||||
|
@ -313,19 +314,38 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
|
||||
<section className="card card--section">
|
||||
<h2 className="card__title">{__('Content Settings')}</h2>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="floating_player"
|
||||
onChange={() => {
|
||||
setClientSetting(SETTINGS.FLOATING_PLAYER, !floatingPlayer);
|
||||
}}
|
||||
checked={floatingPlayer}
|
||||
label={__('Floating video player')}
|
||||
helper={__('Keep content playing in the corner when navigating to a different page.')}
|
||||
/>
|
||||
|
||||
<Form>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="show_nsfw"
|
||||
onChange={() => setClientSetting(SETTINGS.SHOW_NSFW, !showNsfw)}
|
||||
checked={showNsfw}
|
||||
label={__('Show mature content')}
|
||||
helper={__(
|
||||
'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. '
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="autoplay"
|
||||
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
|
||||
checked={autoplay}
|
||||
label={__('Autoplay media files')}
|
||||
helper={__(
|
||||
'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.'
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="show_nsfw"
|
||||
onChange={() => setClientSetting(SETTINGS.SHOW_NSFW, !showNsfw)}
|
||||
checked={showNsfw}
|
||||
label={__('Show mature content')}
|
||||
helper={__(
|
||||
'Mature content may include nudity, intense sexuality, profanity, or other adult content. By displaying mature content, you are affirming you are of legal age to view mature content in your country or jurisdiction. '
|
||||
)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="card card--section">
|
||||
|
@ -465,17 +485,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="checkbox"
|
||||
name="autoplay"
|
||||
onChange={() => setClientSetting(SETTINGS.AUTOPLAY, !autoplay)}
|
||||
checked={autoplay}
|
||||
label={__('Autoplay media files')}
|
||||
helper={__(
|
||||
'Autoplay video and audio files when navigating to a file, as well as the next related item when a file finishes playing.'
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="language_select"
|
||||
type="select"
|
||||
|
@ -499,11 +508,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
name="max_connections"
|
||||
type="select"
|
||||
label={__('Max Connections')}
|
||||
helper={__('More connections, like, do stuff dude')}
|
||||
helper={__(
|
||||
'For users with good bandwidth, try a higher value to improve streaming and download speeds. Low bandwidth users may benefit from a lower setting. Default is 4.'
|
||||
)}
|
||||
min={1}
|
||||
max={100}
|
||||
onChange={this.onMaxConnectionsChange}
|
||||
value={maxConnections}
|
||||
value={daemonSettings.max_connections_per_download}
|
||||
>
|
||||
{connectionOptions.map(connectionOption => (
|
||||
<option key={connectionOption} value={connectionOption}>
|
||||
|
@ -520,7 +531,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
|
||||
<p className="card__subtitle--status">
|
||||
{__(
|
||||
'This will clear the application cache. Your wallet will not be affected. Currently, followed tags will be cleared.'
|
||||
'This will clear the application cache. Your wallet will not be affected. Currently, followed tags and blocked channels will be cleared.'
|
||||
)}
|
||||
</p>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React, { createRef } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import Page from 'component/page';
|
||||
import ClaimListDiscover from 'component/claimListDiscover';
|
||||
import Button from 'component/button';
|
||||
|
@ -18,7 +18,7 @@ function TagsPage(props: Props) {
|
|||
followedTags,
|
||||
doToggleTagFollow,
|
||||
} = props;
|
||||
const buttonRef = createRef();
|
||||
const buttonRef = useRef();
|
||||
const isHovering = useHover(buttonRef);
|
||||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
doPurchaseUri,
|
||||
makeSelectUriIsStreamable,
|
||||
selectDownloadingByOutpoint,
|
||||
makeSelectClaimForUri,
|
||||
} from 'lbry-redux';
|
||||
import { makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||
|
@ -89,7 +90,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
|
|||
// If notifications are disabled(false) just return
|
||||
if (!selectosNotificationsEnabled(getState()) || !fileInfo.written_bytes) return;
|
||||
|
||||
const notif = new window.Notification('LBRY Download Complete', {
|
||||
const notif = new window.Notification(__('LBRY Download Complete'), {
|
||||
body: fileInfo.metadata.title,
|
||||
silent: false,
|
||||
});
|
||||
|
@ -194,9 +195,6 @@ export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolea
|
|||
|
||||
export function doPlayUri(uri: string, skipCostCheck: boolean = false, saveFileOverride: boolean = false) {
|
||||
return (dispatch: Dispatch, getState: () => any) => {
|
||||
// Set the active playing uri so we can avoid showing error notifications if a previously started download fails
|
||||
dispatch(doSetPlayingUri(uri));
|
||||
|
||||
const state = getState();
|
||||
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||
const uriIsStreamable = makeSelectUriIsStreamable(uri)(state);
|
||||
|
@ -243,8 +241,13 @@ export function doPlayUri(uri: string, skipCostCheck: boolean = false, saveFileO
|
|||
};
|
||||
}
|
||||
|
||||
export function savePosition(claimId: string, outpoint: string, position: number) {
|
||||
return (dispatch: Dispatch) => {
|
||||
export function savePosition(uri: string, position: number) {
|
||||
return (dispatch: Dispatch, getState: () => any) => {
|
||||
const state = getState();
|
||||
const claim = makeSelectClaimForUri(uri)(state);
|
||||
const { claim_id: claimId, txid, nout } = claim;
|
||||
const outpoint = `${txid}:${nout}`;
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.SET_CONTENT_POSITION,
|
||||
data: { claimId, outpoint, position },
|
||||
|
|
|
@ -25,12 +25,13 @@ const defaultState = {
|
|||
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
|
||||
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
|
||||
[SETTINGS.SUPPORT_OPTION]: getLocalStorageSetting(SETTINGS.SUPPORT_OPTION, false),
|
||||
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
|
||||
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, true),
|
||||
[SETTINGS.RESULT_COUNT]: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)),
|
||||
[SETTINGS.AUTO_DOWNLOAD]: getLocalStorageSetting(SETTINGS.AUTO_DOWNLOAD, true),
|
||||
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: Boolean(getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true)),
|
||||
[SETTINGS.HIDE_BALANCE]: Boolean(getLocalStorageSetting(SETTINGS.HIDE_BALANCE, false)),
|
||||
[SETTINGS.HIDE_SPLASH_ANIMATION]: Boolean(getLocalStorageSetting(SETTINGS.HIDE_SPLASH_ANIMATION, false)),
|
||||
[SETTINGS.FLOATING_PLAYER]: Boolean(getLocalStorageSetting(SETTINGS.FLOATING_PLAYER, true)),
|
||||
},
|
||||
isNight: false,
|
||||
languages: { en: 'English', pl: 'Polish', id: 'Bahasa Indonesia' }, // temporarily hard code these so we can advance i18n testing
|
||||
|
|
|
@ -81,11 +81,11 @@
|
|||
right: var(--spacing-miniscule);
|
||||
padding: 0.3rem;
|
||||
transition: all var(--transition-duration) var(--transition-style);
|
||||
border-radius: var(--card-radius);
|
||||
|
||||
&:hover {
|
||||
background-color: $lbry-black;
|
||||
color: $lbry-white;
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -193,3 +193,12 @@
|
|||
@extend .card__title;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card__media--nsfw {
|
||||
background-color: $lbry-grape-3;
|
||||
}
|
||||
|
||||
.card__media--disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
|
@ -213,6 +213,9 @@ $border-color--dark: var(--dm-color-04);
|
|||
margin-right: auto;
|
||||
padding-right: var(--spacing-medium);
|
||||
font-size: larger;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.claim-preview-metadata {
|
||||
|
|
|
@ -1,14 +1,80 @@
|
|||
.content__cover {
|
||||
// Video thumbnail with play/download button
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
.content__viewer {
|
||||
@extend .card;
|
||||
position: absolute;
|
||||
top: var(--spacing-large);
|
||||
}
|
||||
|
||||
.content__viewer--floating {
|
||||
position: fixed;
|
||||
margin-bottom: 0;
|
||||
height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
|
||||
overflow: hidden;
|
||||
left: calc(var(--spacing-large) + var(--spacing-small));
|
||||
z-index: 9999;
|
||||
|
||||
&:hover {
|
||||
.content__actions {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content__wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content__wrapper--floating {
|
||||
height: var(--floating-viewer-height);
|
||||
width: var(--floating-viewer-width);
|
||||
}
|
||||
|
||||
.content__actions {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
right: var(--spacing-small);
|
||||
top: var(--spacing-small);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
margin-left: var(--spacing-small);
|
||||
|
||||
.icon:not(:hover) {
|
||||
[data-mode='dark'] & {
|
||||
stroke: $lbry-black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content__hide-viewer {
|
||||
background-color: $lbry-white;
|
||||
stroke: $lbry-black;
|
||||
|
||||
&:hover {
|
||||
background-color: $lbry-teal-5;
|
||||
}
|
||||
}
|
||||
|
||||
.content__info {
|
||||
cursor: grab;
|
||||
height: var(--floating-viewer-info-height);
|
||||
padding: var(--spacing-medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.content__cover {
|
||||
@include thumbnail;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
|
@ -26,23 +92,6 @@
|
|||
background-color: $lbry-teal-2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content__embedded {
|
||||
@include thumbnail;
|
||||
position: relative;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&:-webkit-full-screen {
|
||||
width: 100%;
|
||||
|
@ -50,26 +99,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.content__empty {
|
||||
@include thumbnail;
|
||||
align-items: center;
|
||||
background-color: $lbry-black;
|
||||
color: $lbry-white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content__empty--nsfw,
|
||||
.card__media--nsfw {
|
||||
background-color: $lbry-grape-3;
|
||||
}
|
||||
|
||||
.card__media--disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.content__loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
@ -1,23 +1,8 @@
|
|||
.file-render {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
|
||||
[data-mode='dark'] & {
|
||||
border: 1px solid rgba($lbry-gray-1, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.document-viewer {
|
||||
background-color: $lbry-white;
|
||||
|
||||
[data-mode='dark'] & {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.file-render__viewer {
|
||||
|
@ -33,15 +18,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-render--pdf {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.document-viewer {
|
||||
.file-render__viewer--document {
|
||||
@extend .file-render__viewer;
|
||||
overflow: auto;
|
||||
background-color: $lbry-white;
|
||||
|
||||
[data-mode='dark'] & {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
height: 100%;
|
||||
|
@ -50,18 +34,28 @@
|
|||
}
|
||||
}
|
||||
|
||||
.code-viewer,
|
||||
.document-viewer__content {
|
||||
.file-render__viewer--pdf {
|
||||
@extend .file-render__viewer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.file-render__content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
//
|
||||
// Custom viewers live below here
|
||||
// These either have custom class names that can't be changed or have styles that need to be ovverridden
|
||||
//
|
||||
|
||||
// Code-viewer
|
||||
.code-viewer .CodeMirror {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100px;
|
||||
.CodeMirror {
|
||||
@extend .file-render__content;
|
||||
|
||||
.cm-invalidchar {
|
||||
display: none;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.main-wrapper {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
[data-mode='dark'] & {
|
||||
background-color: var(--dm-color-08);
|
||||
|
|
|
@ -20,10 +20,12 @@ $large-breakpoint: 1921px;
|
|||
--file-page-max-width: 1787px;
|
||||
--file-max-height: 788px;
|
||||
--file-max-width: 1400px;
|
||||
--video-aspect-ratio: 56.25%; // 9 x 16
|
||||
--floating-viewer-width: 32rem;
|
||||
--floating-viewer-height: 18rem; // 32 * 9/16
|
||||
--floating-viewer-info-height: 5rem;
|
||||
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
|
||||
|
||||
// Font
|
||||
|
||||
--font-multiplier-small: 0.9em;
|
||||
--font-multiplier-medium: 1.1em;
|
||||
--font-multiplier-large: 1.4em;
|
||||
|
|
|
@ -43,7 +43,7 @@ const fileInfoFilter = createFilter('fileInfo', [
|
|||
'fileListDownloadedSort',
|
||||
'fileListSubscriptionSort',
|
||||
]);
|
||||
const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpanded']);
|
||||
const appFilter = createFilter('app', ['hasClickedComment', 'searchOptionsExpanded', 'volume', 'muted']);
|
||||
// We only need to persist the receiveAddress for the wallet
|
||||
const walletFilter = createFilter('wallet', ['receiveAddress']);
|
||||
const searchFilter = createFilter('search', ['options']);
|
||||
|
|
|
@ -4,13 +4,19 @@ export default function usePersistedState(key, firstTimeDefault) {
|
|||
// If no key is passed in, act as a normal `useState`
|
||||
let defaultValue;
|
||||
if (key) {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === 'true') {
|
||||
defaultValue = true;
|
||||
} else if (item === 'false') {
|
||||
defaultValue = false;
|
||||
} else {
|
||||
defaultValue = item;
|
||||
let item = localStorage.getItem(key);
|
||||
|
||||
if (item) {
|
||||
let parsedItem;
|
||||
try {
|
||||
parsedItem = JSON.parse(item);
|
||||
} catch (e) {}
|
||||
|
||||
if (parsedItem) {
|
||||
defaultValue = parsedItem;
|
||||
} else {
|
||||
defaultValue = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +28,7 @@ export default function usePersistedState(key, firstTimeDefault) {
|
|||
|
||||
useEffect(() => {
|
||||
if (key) {
|
||||
localStorage.setItem(key, value);
|
||||
localStorage.setItem(key, typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
}
|
||||
}, [key, value]);
|
||||
|
||||
|
|
|
@ -9566,6 +9566,14 @@ react-dom@^16.8.2, react-dom@^16.8.6:
|
|||
prop-types "^15.6.2"
|
||||
scheduler "^0.13.6"
|
||||
|
||||
react-draggable@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.3.0.tgz#2ed7ea3f92e7d742d747f9e6324860606cd4d997"
|
||||
integrity sha512-U7/jD0tAW4T0S7DCPK0kkKLyL0z61sC/eqU+NUfDjnq+JtBKaYKDHpsK2wazctiA4alEzCXUnzkREoxppOySVw==
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-ga@^2.5.7:
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.5.7.tgz#1c80a289004bf84f84c26d46f3a6a6513081bf2e"
|
||||
|
|
Loading…
Reference in a new issue