mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-08-23 17:47:24 +00:00
embed functionality inside markdown posts
This commit is contained in:
parent
0738d186a5
commit
1a50e697ce
18 changed files with 343 additions and 186 deletions
|
@ -1,27 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import { URL } from 'config';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
uri: string,
|
|
||||||
title: ?string,
|
|
||||||
};
|
|
||||||
|
|
||||||
function fileViewerEmbeddedTitle(props: Props) {
|
|
||||||
const { uri, title } = props;
|
|
||||||
|
|
||||||
const lbrytvLink = `${URL}${formatLbryUrlForWeb(uri)}?src=embed`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="file-viewer__embedded-title">
|
|
||||||
<Button label={title} button="link" href={lbrytvLink} />
|
|
||||||
<Button className="file-viewer__overlay-logo file-viewer__embedded-title-logo" icon={ICONS.LBRY} href={URL} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(fileViewerEmbeddedTitle);
|
|
|
@ -3,7 +3,7 @@ import type { ElementRef, Node } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOMServer from 'react-dom/server';
|
import ReactDOMServer from 'react-dom/server';
|
||||||
import SimpleMDE from 'react-simplemde-editor';
|
import SimpleMDE from 'react-simplemde-editor';
|
||||||
import MarkdownPreview from 'component/common/markdown-preview-internal';
|
import MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||||
import { MAX_CHARACTERS_IN_COMMENT as defaultTextAreaLimit } from 'constants/comments';
|
import { MAX_CHARACTERS_IN_COMMENT as defaultTextAreaLimit } from 'constants/comments';
|
||||||
import 'easymde/dist/easymde.min.css';
|
import 'easymde/dist/easymde.min.css';
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react';
|
|
||||||
import remark from 'remark';
|
|
||||||
import remarkAttr from 'remark-attr';
|
|
||||||
import remarkStrip from 'strip-markdown';
|
|
||||||
import remarkEmoji from 'remark-emoji';
|
|
||||||
import reactRenderer from 'remark-react';
|
|
||||||
import ExternalLink from 'component/externalLink';
|
|
||||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
|
||||||
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
|
||||||
|
|
||||||
type SimpleTextProps = {
|
|
||||||
children?: React.Node,
|
|
||||||
};
|
|
||||||
|
|
||||||
type SimpleLinkProps = {
|
|
||||||
href?: string,
|
|
||||||
title?: string,
|
|
||||||
children?: React.Node,
|
|
||||||
};
|
|
||||||
|
|
||||||
type MarkdownProps = {
|
|
||||||
strip?: boolean,
|
|
||||||
content: ?string,
|
|
||||||
promptLinks?: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SimpleText = (props: SimpleTextProps) => {
|
|
||||||
return <span>{props.children}</span>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SimpleLink = (props: SimpleLinkProps) => {
|
|
||||||
const { title, children } = props;
|
|
||||||
let { href } = props;
|
|
||||||
if (IS_WEB && href && href.startsWith('lbry://')) {
|
|
||||||
href = formatLbryUrlForWeb(href);
|
|
||||||
// using Link after formatLbryUrl to handle "/" vs "#/"
|
|
||||||
// for web and desktop scenarios respectively
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
title={title}
|
|
||||||
to={href}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<a href={href} title={title}>
|
|
||||||
{children}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use github sanitation schema
|
|
||||||
const schema = { ...defaultSchema };
|
|
||||||
|
|
||||||
// Extend sanitation schema to support lbry protocol
|
|
||||||
schema.protocols.href.push('lbry');
|
|
||||||
schema.attributes.a.push('embed');
|
|
||||||
|
|
||||||
const MarkdownPreview = (props: MarkdownProps) => {
|
|
||||||
const { content, strip, promptLinks } = props;
|
|
||||||
|
|
||||||
const remarkOptions: Object = {
|
|
||||||
sanitize: schema,
|
|
||||||
fragment: React.Fragment,
|
|
||||||
remarkReactComponents: {
|
|
||||||
a: promptLinks ? ExternalLink : SimpleLink,
|
|
||||||
// Workaraund of remarkOptions.Fragment
|
|
||||||
div: React.Fragment,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const remarkAttrOpts = {
|
|
||||||
scope: 'extended',
|
|
||||||
elements: ['link'],
|
|
||||||
extend: { link: ['embed'] },
|
|
||||||
defaultValue: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Strip all content and just render text
|
|
||||||
if (strip) {
|
|
||||||
// Remove new lines and extra space
|
|
||||||
remarkOptions.remarkReactComponents.p = SimpleText;
|
|
||||||
return (
|
|
||||||
<span className="markdown-preview">
|
|
||||||
{
|
|
||||||
remark()
|
|
||||||
.use(remarkStrip)
|
|
||||||
.use(reactRenderer, remarkOptions)
|
|
||||||
.processSync(content).contents
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="markdown-preview">
|
|
||||||
{
|
|
||||||
remark()
|
|
||||||
.use(remarkAttr, remarkAttrOpts)
|
|
||||||
// Remark plugins for lbry urls
|
|
||||||
// Note: The order is important
|
|
||||||
.use(formatedLinks)
|
|
||||||
.use(inlineLinks)
|
|
||||||
// Emojis
|
|
||||||
.use(remarkEmoji)
|
|
||||||
.use(reactRenderer, remarkOptions)
|
|
||||||
.processSync(content).contents
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MarkdownPreview;
|
|
|
@ -1,8 +1,144 @@
|
||||||
import React, { Suspense } from 'react';
|
// @flow
|
||||||
import MarkDownPreview from './markdown-preview-internal';
|
import * as React from 'react';
|
||||||
|
import remark from 'remark';
|
||||||
|
import remarkAttr from 'remark-attr';
|
||||||
|
import remarkStrip from 'strip-markdown';
|
||||||
|
import remarkEmoji from 'remark-emoji';
|
||||||
|
import reactRenderer from 'remark-react';
|
||||||
|
import ExternalLink from 'component/externalLink';
|
||||||
|
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
||||||
|
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import EmbedPlayButton from 'component/embedPlayButton';
|
||||||
|
|
||||||
const MarkdownPreview = props => {
|
type SimpleTextProps = {
|
||||||
return <MarkDownPreview {...props} />;
|
children?: React.Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
type SimpleLinkProps = {
|
||||||
|
href?: string,
|
||||||
|
title?: string,
|
||||||
|
children?: React.Node,
|
||||||
|
};
|
||||||
|
|
||||||
|
type MarkdownProps = {
|
||||||
|
strip?: boolean,
|
||||||
|
content: ?string,
|
||||||
|
promptLinks?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleText = (props: SimpleTextProps) => {
|
||||||
|
return <span>{props.children}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SimpleLink = (props: SimpleLinkProps) => {
|
||||||
|
const { title, children } = props;
|
||||||
|
const { href } = props;
|
||||||
|
|
||||||
|
if (!href) {
|
||||||
|
return children || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!href.startsWith('lbry://')) {
|
||||||
|
return (
|
||||||
|
<a href={href} title={title}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [uri, search] = href.split('?');
|
||||||
|
const urlParams = new URLSearchParams(search);
|
||||||
|
const embed = urlParams.get('embed');
|
||||||
|
|
||||||
|
if (embed) {
|
||||||
|
return <EmbedPlayButton uri={uri} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webLink = formatLbryUrlForWeb(uri);
|
||||||
|
// using Link after formatLbryUrl to handle "/" vs "#/"
|
||||||
|
// for web and desktop scenarios respectively
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
title={title}
|
||||||
|
to={webLink}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use github sanitation schema
|
||||||
|
const schema = { ...defaultSchema };
|
||||||
|
|
||||||
|
// Extend sanitation schema to support lbry protocol
|
||||||
|
schema.protocols.href.push('lbry');
|
||||||
|
schema.attributes.a.push('embed');
|
||||||
|
|
||||||
|
const REPLACE_REGEX = /(<iframe src=")(.*?(?=))("><\/iframe>)/g;
|
||||||
|
|
||||||
|
const MarkdownPreview = (props: MarkdownProps) => {
|
||||||
|
const { content, strip, promptLinks } = props;
|
||||||
|
const strippedContent = content
|
||||||
|
? content.replace(REPLACE_REGEX, (x, y, iframeSrc) => {
|
||||||
|
return `${iframeSrc}?embed=true`;
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const remarkOptions: Object = {
|
||||||
|
sanitize: schema,
|
||||||
|
fragment: React.Fragment,
|
||||||
|
remarkReactComponents: {
|
||||||
|
a: promptLinks ? ExternalLink : linkProps => <SimpleLink {...linkProps} />,
|
||||||
|
// Workaraund of remarkOptions.Fragment
|
||||||
|
div: React.Fragment,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const remarkAttrOpts = {
|
||||||
|
scope: 'extended',
|
||||||
|
elements: ['link'],
|
||||||
|
extend: { link: ['embed'] },
|
||||||
|
defaultValue: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Strip all content and just render text
|
||||||
|
if (strip) {
|
||||||
|
// Remove new lines and extra space
|
||||||
|
remarkOptions.remarkReactComponents.p = SimpleText;
|
||||||
|
return (
|
||||||
|
<span className="markdown-preview">
|
||||||
|
{
|
||||||
|
remark()
|
||||||
|
.use(remarkStrip)
|
||||||
|
.use(reactRenderer, remarkOptions)
|
||||||
|
.processSync(content).contents
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="markdown-preview">
|
||||||
|
{
|
||||||
|
remark()
|
||||||
|
.use(remarkAttr, remarkAttrOpts)
|
||||||
|
// Remark plugins for lbry urls
|
||||||
|
// Note: The order is important
|
||||||
|
.use(formatedLinks)
|
||||||
|
.use(inlineLinks)
|
||||||
|
// Emojis
|
||||||
|
.use(remarkEmoji)
|
||||||
|
.use(reactRenderer, remarkOptions)
|
||||||
|
.processSync(strippedContent).contents
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarkdownPreview;
|
export default MarkdownPreview;
|
||||||
|
|
19
ui/component/embedPlayButton/index.js
Normal file
19
ui/component/embedPlayButton/index.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
|
import { doFetchCostInfoForUri } from 'lbryinc';
|
||||||
|
import { doSetFloatingUri } from 'redux/actions/content';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import ChannelThumbnail from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
floatingPlayerEnabled: makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, {
|
||||||
|
doResolveUri,
|
||||||
|
doFetchCostInfoForUri,
|
||||||
|
doSetFloatingUri,
|
||||||
|
})(ChannelThumbnail);
|
60
ui/component/embedPlayButton/view.jsx
Normal file
60
ui/component/embedPlayButton/view.jsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
// @flow
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import useIsMobile from 'effects/use-is-mobile';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
thumbnail: string,
|
||||||
|
claim: ?Claim,
|
||||||
|
doResolveUri: string => void,
|
||||||
|
doFetchCostInfoForUri: string => void,
|
||||||
|
doSetFloatingUri: string => void,
|
||||||
|
floatingPlayerEnabled: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FileRenderFloating(props: Props) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
thumbnail = '',
|
||||||
|
claim,
|
||||||
|
doResolveUri,
|
||||||
|
doFetchCostInfoForUri,
|
||||||
|
doSetFloatingUri,
|
||||||
|
floatingPlayerEnabled,
|
||||||
|
} = props;
|
||||||
|
const { push } = useHistory();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const hasResolvedUri = claim !== undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasResolvedUri) {
|
||||||
|
doResolveUri(uri);
|
||||||
|
doFetchCostInfoForUri(uri);
|
||||||
|
}
|
||||||
|
}, [uri, hasResolvedUri, doResolveUri, doFetchCostInfoForUri]);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (isMobile || !floatingPlayerEnabled) {
|
||||||
|
const formattedUrl = formatLbryUrlForWeb(uri);
|
||||||
|
push(formattedUrl);
|
||||||
|
} else {
|
||||||
|
doSetFloatingUri(uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
className="embed__inline-button"
|
||||||
|
onClick={handleClick}
|
||||||
|
style={{ backgroundImage: `url('${thumbnail.replace(/'/g, "\\'")}')` }}
|
||||||
|
>
|
||||||
|
<FileViewerEmbeddedTitle uri={uri} isInApp />
|
||||||
|
<Button onClick={handleClick} iconSize={30} title={__('Play')} className={'button--icon button--play'} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,17 +3,20 @@ import { connect } from 'react-redux';
|
||||||
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
|
import { makeSelectFileInfoForUri, makeSelectTitleForUri } from 'lbry-redux';
|
||||||
import {
|
import {
|
||||||
makeSelectIsPlayerFloating,
|
makeSelectIsPlayerFloating,
|
||||||
|
selectFloatingUri,
|
||||||
selectPlayingUri,
|
selectPlayingUri,
|
||||||
makeSelectFileRenderModeForUri,
|
makeSelectFileRenderModeForUri,
|
||||||
makeSelectStreamingUrlForUriWebProxy,
|
makeSelectStreamingUrlForUriWebProxy,
|
||||||
} from 'redux/selectors/content';
|
} from 'redux/selectors/content';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doCloseFloatingPlayer } from 'redux/actions/content';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import FileRenderFloating from './view';
|
import FileRenderFloating from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const uri = selectPlayingUri(state);
|
const floatingUri = selectFloatingUri(state);
|
||||||
|
const playingUri = selectPlayingUri(state);
|
||||||
|
const uri = floatingUri || playingUri;
|
||||||
return {
|
return {
|
||||||
uri,
|
uri,
|
||||||
title: makeSelectTitleForUri(uri)(state),
|
title: makeSelectTitleForUri(uri)(state),
|
||||||
|
@ -26,7 +29,7 @@ const select = (state, props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
clearPlayingUri: () => dispatch(doSetPlayingUri(null)),
|
closeFloatingPlayer: () => dispatch(doCloseFloatingPlayer(null)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(FileRenderFloating));
|
export default withRouter(connect(select, perform)(FileRenderFloating));
|
||||||
|
|
|
@ -21,13 +21,21 @@ type Props = {
|
||||||
streamingUrl?: string,
|
streamingUrl?: string,
|
||||||
title: ?string,
|
title: ?string,
|
||||||
floatingPlayerEnabled: boolean,
|
floatingPlayerEnabled: boolean,
|
||||||
clearPlayingUri: () => void,
|
closeFloatingPlayer: () => void,
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileRenderFloating(props: Props) {
|
export default function FileRenderFloating(props: Props) {
|
||||||
const { fileInfo, uri, streamingUrl, title, isFloating, clearPlayingUri, floatingPlayerEnabled, renderMode } = props;
|
const {
|
||||||
|
fileInfo,
|
||||||
|
uri,
|
||||||
|
streamingUrl,
|
||||||
|
title,
|
||||||
|
isFloating,
|
||||||
|
closeFloatingPlayer,
|
||||||
|
floatingPlayerEnabled,
|
||||||
|
renderMode,
|
||||||
|
} = props;
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
|
const [fileViewerRect, setFileViewerRect] = usePersistedState('inline-file-viewer:rect');
|
||||||
const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
|
const [position, setPosition] = usePersistedState('floating-file-viewer:position', {
|
||||||
|
@ -111,7 +119,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
<Button navigate={uri} icon={ICONS.VIEW} button="primary" />
|
<Button navigate={uri} icon={ICONS.VIEW} button="primary" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label={__('Close')}>
|
<Tooltip label={__('Close')}>
|
||||||
<Button onClick={clearPlayingUri} icon={ICONS.REMOVE} button="primary" />
|
<Button onClick={closeFloatingPlayer} icon={ICONS.REMOVE} button="primary" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
38
ui/component/fileViewerEmbeddedTitle/view.jsx
Normal file
38
ui/component/fileViewerEmbeddedTitle/view.jsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import { URL } from 'config';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
title: ?string,
|
||||||
|
isInApp: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileViewerEmbeddedTitle(props: Props) {
|
||||||
|
const { uri, title, isInApp } = props;
|
||||||
|
|
||||||
|
let contentLink = `${formatLbryUrlForWeb(uri)}`;
|
||||||
|
|
||||||
|
if (!isInApp) {
|
||||||
|
contentLink = `${contentLink}?src=embed`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lbryLinkProps = isInApp ? { navigate: '/' } : { href: URL };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-viewer__embedded-title">
|
||||||
|
<Button label={title} button="link" navigate={contentLink} />
|
||||||
|
<Button
|
||||||
|
className="file-viewer__overlay-logo file-viewer__embedded-title-logo"
|
||||||
|
icon={ICONS.LBRY}
|
||||||
|
{...lbryLinkProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(FileViewerEmbeddedTitle);
|
|
@ -11,7 +11,7 @@ import { FORCE_CONTENT_TYPE_PLAYER } from 'constants/claim';
|
||||||
import AutoplayCountdown from 'component/autoplayCountdown';
|
import AutoplayCountdown from 'component/autoplayCountdown';
|
||||||
import usePrevious from 'effects/use-previous';
|
import usePrevious from 'effects/use-previous';
|
||||||
import FileViewerEmbeddedEnded from 'lbrytv/component/fileViewerEmbeddedEnded';
|
import FileViewerEmbeddedEnded from 'lbrytv/component/fileViewerEmbeddedEnded';
|
||||||
import FileViewerEmbeddedTitle from 'lbrytv/component/fileViewerEmbeddedTitle';
|
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
|
|
||||||
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||||
|
|
|
@ -82,6 +82,7 @@ export const PUBLISH_STARTED = 'PUBLISH_STARTED';
|
||||||
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
|
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
|
||||||
export const PUBLISH_FAILED = 'PUBLISH_FAILED';
|
export const PUBLISH_FAILED = 'PUBLISH_FAILED';
|
||||||
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
|
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
|
||||||
|
export const SET_FLOATING_URI = 'SET_FLOATING_URI';
|
||||||
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
|
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
|
||||||
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
|
||||||
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
|
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
@ -10,7 +11,6 @@ import { push } from 'connected-react-router';
|
||||||
import { doUpdateUnreadSubscriptions } from 'redux/actions/subscriptions';
|
import { doUpdateUnreadSubscriptions } from 'redux/actions/subscriptions';
|
||||||
import { makeSelectUnreadByChannel } from 'redux/selectors/subscriptions';
|
import { makeSelectUnreadByChannel } from 'redux/selectors/subscriptions';
|
||||||
import {
|
import {
|
||||||
ACTIONS,
|
|
||||||
Lbry,
|
Lbry,
|
||||||
makeSelectFileInfoForUri,
|
makeSelectFileInfoForUri,
|
||||||
selectFileInfosByOutpoint,
|
selectFileInfosByOutpoint,
|
||||||
|
@ -24,6 +24,7 @@ import {
|
||||||
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
|
||||||
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
|
import { selectFloatingUri } from 'redux/selectors/content';
|
||||||
|
|
||||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||||
|
|
||||||
|
@ -134,6 +135,28 @@ export function doSetPlayingUri(uri: ?string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function doSetFloatingUri(uri: ?string) {
|
||||||
|
return (dispatch: Dispatch) => {
|
||||||
|
dispatch({
|
||||||
|
type: ACTIONS.SET_FLOATING_URI,
|
||||||
|
data: { uri },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doCloseFloatingPlayer() {
|
||||||
|
return (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
const floatingUri = selectFloatingUri(state);
|
||||||
|
|
||||||
|
if (floatingUri) {
|
||||||
|
dispatch(doSetFloatingUri(null));
|
||||||
|
} else {
|
||||||
|
dispatch(doSetPlayingUri(null));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?() => void) {
|
export function doPurchaseUriWrapper(uri: string, cost: number, saveFile: boolean, cb: ?() => void) {
|
||||||
return (dispatch: Dispatch, getState: () => any) => {
|
return (dispatch: Dispatch, getState: () => any) => {
|
||||||
function onSuccess(fileInfo) {
|
function onSuccess(fileInfo) {
|
||||||
|
@ -249,12 +272,3 @@ export function doClearContentHistoryAll() {
|
||||||
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL });
|
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doSetHistoryPage(page: string) {
|
|
||||||
return (dispatch: Dispatch) => {
|
|
||||||
dispatch({
|
|
||||||
type: ACTIONS.SET_CONTENT_HISTORY_PAGE,
|
|
||||||
data: { page },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
playingUri: null,
|
playingUri: null,
|
||||||
|
floatingUri: null,
|
||||||
channelClaimCounts: {},
|
channelClaimCounts: {},
|
||||||
positions: {},
|
positions: {},
|
||||||
history: [],
|
history: [],
|
||||||
|
@ -13,6 +14,11 @@ reducers[ACTIONS.SET_PLAYING_URI] = (state, action) =>
|
||||||
playingUri: action.data.uri,
|
playingUri: action.data.uri,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
reducers[ACTIONS.SET_FLOATING_URI] = (state, action) =>
|
||||||
|
Object.assign({}, state, {
|
||||||
|
floatingUri: action.data.uri,
|
||||||
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
|
||||||
const { claimId, outpoint, position } = action.data;
|
const { claimId, outpoint, position } = action.data;
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -12,9 +12,9 @@ import {
|
||||||
selectBalance,
|
selectBalance,
|
||||||
selectBlockedChannels,
|
selectBlockedChannels,
|
||||||
parseURI,
|
parseURI,
|
||||||
|
buildURI,
|
||||||
makeSelectContentTypeForUri,
|
makeSelectContentTypeForUri,
|
||||||
makeSelectFileNameForUri,
|
makeSelectFileNameForUri,
|
||||||
buildURI,
|
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
|
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
|
@ -31,17 +31,28 @@ const HISTORY_ITEMS_PER_PAGE = 50;
|
||||||
export const selectState = (state: any) => state.content || {};
|
export const selectState = (state: any) => state.content || {};
|
||||||
|
|
||||||
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
export const selectPlayingUri = createSelector(selectState, state => state.playingUri);
|
||||||
|
export const selectFloatingUri = createSelector(selectState, state => state.floatingUri);
|
||||||
|
|
||||||
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
|
export const makeSelectIsPlaying = (uri: string) => createSelector(selectPlayingUri, playingUri => playingUri === uri);
|
||||||
|
|
||||||
// below is dumb, some context: https://stackoverflow.com/questions/39622864/access-react-router-state-in-selector
|
// below is dumb, some context: https://stackoverflow.com/questions/39622864/access-react-router-state-in-selector
|
||||||
export const makeSelectIsPlayerFloating = location =>
|
export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
|
||||||
createSelector(selectPlayingUri, playingUri => {
|
createSelector(selectFloatingUri, selectPlayingUri, (floatingUri, playingUri) => {
|
||||||
|
if (floatingUri) {
|
||||||
|
if (!playingUri) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return floatingUri !== playingUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no floatingPlayer explicitly set, see if the playingUri can float
|
||||||
try {
|
try {
|
||||||
const { pathname, hash } = location;
|
const { pathname, hash } = location;
|
||||||
const newpath = buildURI(parseURI(pathname.slice(1).replace(/:/g, '#')));
|
const newpath = buildURI(parseURI(pathname.slice(1).replace(/:/g, '#')));
|
||||||
return playingUri && playingUri !== newpath + hash;
|
return playingUri && playingUri !== newpath + hash;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
return !!playingUri;
|
return !!playingUri;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,3 +8,20 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-black);
|
background-color: var(--color-black);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embed__inline-button {
|
||||||
|
@include thumbnail;
|
||||||
|
position: relative;
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100%;
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -229,6 +229,10 @@
|
||||||
.button {
|
.button {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,18 +46,6 @@
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Paragraphs
|
|
||||||
p {
|
|
||||||
svg {
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p,
|
p,
|
||||||
blockquote,
|
blockquote,
|
||||||
dl,
|
dl,
|
||||||
|
|
Loading…
Add table
Reference in a new issue