Merge branch 'master' into master
This commit is contained in:
commit
455188dff3
43 changed files with 640 additions and 192 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -5,6 +5,42 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
|
||||
## [0.22.2] - 2018-07-09
|
||||
|
||||
### Fixed
|
||||
* Fixed 'Get Credits' screen so the app doesn't break when LBC is unavailable on ShapeShift ([#1739](https://github.com/lbryio/lbry-app/pull/1739))
|
||||
|
||||
|
||||
## [0.22.1] - 2018-07-05
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
### Fixed
|
||||
* Take previous bid amount into account when determining how much users have available to deposit ([#1725](https://github.com/lbryio/lbry-app/pull/1725))
|
||||
* Sidebar sizing on larger screens ([#1709](https://github.com/lbryio/lbry-app/pull/1709))
|
||||
* Publishing scenario while editing and changing URI ([#1716](https://github.com/lbryio/lbry-app/pull/1716))
|
||||
* Fix can't right click > paste into description on publish ([#1664](https://github.com/lbryio/lbry-app/issues/1664))
|
||||
* Mac/Linux error when starting app up too quickly after shutdown ([#1727](https://github.com/lbryio/lbry-app/pull/1727))
|
||||
* Console errors when multiple downloads for same claim exist ([#1724](https://github.com/lbryio/lbry-app/pull/1724))
|
||||
* App version in dev mode ([#1722](https://github.com/lbryio/lbry-app/pull/1722))
|
||||
* Long URI name displays in transaction list/Help ([#1694](https://github.com/lbryio/lbry-app/pull/1694))/([#1692](https://github.com/lbryio/lbry-app/pull/1692))
|
||||
* Edit option missing from certain published claims ([#175](https://github.com/lbryio/lbry-desktop/issues/1756))
|
||||
|
||||
### Changed
|
||||
* Show claim name, instead of URI, when loading a channel([#1711](https://github.com/lbryio/lbry-app/pull/1711))
|
||||
* Updated LBRY daemon to 0.20.3 which contains some availability improvements ([v0.20.3](https://github.com/lbryio/lbry/releases/tag/v0.20.3))
|
||||
|
||||
## [0.22.0] - 2018-06-26
|
||||
|
||||
### Added
|
||||
* Ability to upload thumbnails through spee.ch while publishing ([#1248](https://github.com/lbryio/lbry-app/pull/1248))
|
||||
* QR code for wallet address to Send and Receive page ([#1582](https://github.com/lbryio/lbry-app/pull/1582))
|
||||
|
|
11
README.md
11
README.md
|
@ -3,7 +3,7 @@
|
|||
[](https://travis-ci.org/lbryio/lbry-app)
|
||||
[](https://david-dm.org/lbryio/lbry-app)
|
||||
[](https://www.codacy.com/app/LBRY/lbry-app?utm_source=github.com&utm_medium=referral&utm_content=lbryio/lbry-app&utm_campaign=Badge_Grade)
|
||||
[](https://discord.gg/U5aRyN6)
|
||||
[](https://chat.lbry.io)
|
||||
|
||||
The LBRY app is a graphical browser for the decentralized content marketplace provided by the
|
||||
[LBRY](https://lbry.io) protocol. It is essentially the
|
||||
|
@ -22,17 +22,18 @@ We provide installers for Windows, macOS (v10.9 or greater), and Debian-based Li
|
|||
| Latest Pre-release | [Download](https://lbry.io/get/lbry.pre.exe) | [Download](https://lbry.io/get/lbry.pre.dmg) | [Download](https://lbry.io/get/lbry.pre.deb)
|
||||
|
||||
Our [releases page](https://github.com/lbryio/lbry-app/releases) also contains the latest
|
||||
release, pre-releases, and past builds.
|
||||
release, pre-releases, and past builds.
|
||||
*Note: If the deb fails to install using the Ubuntu Software Center, install manually via `sudo dpkg -i <path to deb>`. You'll need to run `sudo apt-get install -f` if this is the first time installing it to install dependencies*
|
||||
|
||||
To install from source or make changes to the application, continue to the next section below.
|
||||
|
||||
**Community maintained** builds for Arch Linux and Flatpak are available, see below. These installs will need to be updated manually as the in-app update process only supports deb installs at this time.
|
||||
*Note: If coming from a deb install, the directory structure is different and you'll need to [migrate data](https://lbry.io/faq/backup-data).*
|
||||
|
||||
| | Flatpak | Arch
|
||||
| | Flatpak | Arch
|
||||
| --------------------- | ------------------------------------------| --------------------------------------------
|
||||
| Latest Release | [FlatHub Page](https://flathub.org/apps/details/io.lbry.lbry-app) | [AUR Package](https://aur.archlinux.org/packages/lbry-app-bin/)
|
||||
| Maintainers | [@choofee](https://github.com/choffee)/[@iuyte](https://github.com/iuyte) | [@kcseb]()/[@TimurKiyivinski](https://github.com/TimurKiyivinski)
|
||||
| Maintainers | [@choofee](https://github.com/choffee)/[@iuyte](https://github.com/iuyte) | [@kcseb]()/[@TimurKiyivinski](https://github.com/TimurKiyivinski)
|
||||
|
||||
## Usage
|
||||
Double click the installed application to browse with the LBRY network.
|
||||
|
@ -48,7 +49,7 @@ Double click the installed application to browse with the LBRY network.
|
|||
|
||||
#### Steps
|
||||
|
||||
1. Clone this repository: `git clone https://github.com/lbryio/lbry-app`
|
||||
1. Clone (or [fork](https://help.github.com/articles/fork-a-repo/)) this repository: `git clone https://github.com/lbryio/lbry-app`
|
||||
2. Change directories into the downloaded folder: `cd lbry-app`
|
||||
3. Install the dependencies: `yarn`
|
||||
4. Run the app: `yarn dev`
|
||||
|
|
14
package.json
14
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "LBRY",
|
||||
"version": "0.22.0",
|
||||
"version": "0.22.2",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
|
@ -30,8 +30,8 @@
|
|||
"flow-defs": "flow-typed install",
|
||||
"release": "yarn compile && electron-builder build",
|
||||
"precommit": "lint-staged",
|
||||
"postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js",
|
||||
"clean": "rm -r node_modules && yarn cache clean lbry-redux && yarn"
|
||||
"preinstall": "yarn cache clean lbry-redux",
|
||||
"postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.1",
|
||||
|
@ -48,8 +48,9 @@
|
|||
"formik": "^0.10.4",
|
||||
"hast-util-sanitize": "^1.1.2",
|
||||
"keytar": "^4.2.1",
|
||||
"lbry-redux": "lbryio/lbry-redux#201d78b68a329065ee5d2a03bfb1607ea0666588",
|
||||
"lbry-redux": "lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd",
|
||||
"localforage": "^1.7.1",
|
||||
"mime": "^2.3.1",
|
||||
"mixpanel-browser": "^2.17.1",
|
||||
"moment": "^2.22.0",
|
||||
"qrcode.react": "^0.8.0",
|
||||
|
@ -60,7 +61,7 @@
|
|||
"react-modal": "^3.1.7",
|
||||
"react-paginate": "^5.2.1",
|
||||
"react-redux": "^5.0.3",
|
||||
"react-simplemde-editor": "^3.6.15",
|
||||
"react-simplemde-editor": "^3.6.16",
|
||||
"react-toggle": "^4.0.2",
|
||||
"react-transition-group": "1.x",
|
||||
"redux": "^3.6.0",
|
||||
|
@ -76,6 +77,7 @@
|
|||
"semver": "^5.3.0",
|
||||
"shapeshift.io": "^1.3.1",
|
||||
"source-map-support": "^0.5.4",
|
||||
"stream-to-blob-url": "^2.1.1",
|
||||
"tree-kill": "^1.1.0",
|
||||
"y18n": "^4.0.0"
|
||||
},
|
||||
|
@ -127,7 +129,7 @@
|
|||
"yarn": "^1.3"
|
||||
},
|
||||
"lbrySettings": {
|
||||
"lbrynetDaemonVersion": "0.20.2",
|
||||
"lbrynetDaemonVersion": "0.20.3",
|
||||
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip",
|
||||
"lbrynetDaemonDir": "static/daemon",
|
||||
"lbrynetDaemonFileName": "lbrynet-daemon"
|
||||
|
|
|
@ -14,7 +14,7 @@ export default appState => {
|
|||
defaultHeight: height,
|
||||
});
|
||||
|
||||
let windowConfiguration = {
|
||||
const windowConfiguration = {
|
||||
backgroundColor: '#44b098',
|
||||
minWidth: 950,
|
||||
minHeight: 600,
|
||||
|
@ -26,17 +26,13 @@ export default appState => {
|
|||
// If state is undefined, create window as maximized.
|
||||
width: windowState.width === undefined ? width : windowState.width,
|
||||
height: windowState.height === undefined ? height : windowState.height,
|
||||
};
|
||||
|
||||
// Disable renderer process's webSecurity on development to enable CORS.
|
||||
windowConfiguration = isDev
|
||||
? {
|
||||
...windowConfiguration,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
},
|
||||
}
|
||||
: windowConfiguration;
|
||||
webPreferences: {
|
||||
// Disable renderer process's webSecurity on development to enable CORS.
|
||||
webSecurity: !isDev,
|
||||
plugins: true,
|
||||
},
|
||||
};
|
||||
|
||||
const rendererURL = isDev
|
||||
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
|
||||
|
|
|
@ -11,6 +11,7 @@ import isDev from 'electron-is-dev';
|
|||
import Daemon from './Daemon';
|
||||
import createTray from './createTray';
|
||||
import createWindow from './createWindow';
|
||||
import pjson from '../../package.json';
|
||||
|
||||
autoUpdater.autoDownload = true;
|
||||
|
||||
|
@ -58,16 +59,18 @@ app.on('ready', async () => {
|
|||
if (!isDaemonRunning) {
|
||||
daemon = new Daemon();
|
||||
daemon.on('exit', () => {
|
||||
daemon = null;
|
||||
if (!appState.isQuitting) {
|
||||
dialog.showErrorBox(
|
||||
'Daemon has Exited',
|
||||
'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
|
||||
'For more information please visit: \n' +
|
||||
'https://lbry.io/faq/startup-troubleshooting'
|
||||
);
|
||||
if (!isDev) {
|
||||
daemon = null;
|
||||
if (!appState.isQuitting) {
|
||||
dialog.showErrorBox(
|
||||
'Daemon has Exited',
|
||||
'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
|
||||
'For more information please visit: \n' +
|
||||
'https://lbry.io/faq/startup-troubleshooting'
|
||||
);
|
||||
}
|
||||
app.quit();
|
||||
}
|
||||
app.quit();
|
||||
});
|
||||
daemon.launch();
|
||||
}
|
||||
|
@ -82,7 +85,9 @@ app.on('ready', async () => {
|
|||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
rendererWindow.show();
|
||||
if (rendererWindow) {
|
||||
rendererWindow.show();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('will-quit', event => {
|
||||
|
@ -119,6 +124,10 @@ app.on('will-quit', event => {
|
|||
daemon.quit();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (rendererWindow) {
|
||||
rendererWindow = null;
|
||||
}
|
||||
});
|
||||
|
||||
// https://electronjs.org/docs/api/app#event-will-finish-launching
|
||||
|
@ -171,7 +180,7 @@ ipcMain.on('version-info-requested', () => {
|
|||
return ver.replace(/([^-])rc/, '$1-rc');
|
||||
}
|
||||
|
||||
const localVersion = app.getVersion();
|
||||
const localVersion = pjson.version;
|
||||
const latestReleaseAPIURL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
|
||||
const opts = {
|
||||
headers: {
|
||||
|
|
|
@ -23,6 +23,7 @@ type Props = {
|
|||
noPadding: ?boolean, // to remove padding and allow circular buttons
|
||||
uppercase: ?boolean,
|
||||
iconColor: ?string,
|
||||
tourniquet: ?boolean, // to shorten the button and ellipsis, only use for links
|
||||
};
|
||||
|
||||
class Button extends React.PureComponent<Props> {
|
||||
|
@ -50,6 +51,7 @@ class Button extends React.PureComponent<Props> {
|
|||
noPadding,
|
||||
uppercase,
|
||||
iconColor,
|
||||
tourniquet,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
|
@ -69,6 +71,7 @@ class Button extends React.PureComponent<Props> {
|
|||
'btn--link': button === 'link',
|
||||
'btn--external-link': button === 'link' && href,
|
||||
'btn--uppercase': uppercase,
|
||||
'btn--tourniquet': tourniquet,
|
||||
}
|
||||
: 'btn--no-style',
|
||||
className
|
||||
|
|
|
@ -5,10 +5,20 @@ type Props = {
|
|||
message: ?string,
|
||||
};
|
||||
|
||||
const BusyIndicator = (props: Props) => (
|
||||
<span className="busy-indicator">
|
||||
{props.message} <span className="busy-indicator__loader" />
|
||||
</span>
|
||||
);
|
||||
class BusyIndicator extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
|
||||
return (
|
||||
<span className="busy-indicator">
|
||||
{message} <span className="busy-indicator__loader" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BusyIndicator;
|
||||
|
|
|
@ -6,6 +6,7 @@ import MarkdownPreview from 'component/common/markdown-preview';
|
|||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import 'simplemde/dist/simplemde.min.css';
|
||||
import Toggle from 'react-toggle';
|
||||
import { openEditorMenu } from 'util/contextMenu';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
|
@ -54,11 +55,21 @@ export class FormField extends React.PureComponent<Props> {
|
|||
</select>
|
||||
);
|
||||
} else if (type === 'markdown') {
|
||||
const stopContextMenu = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
const handleEvents = {
|
||||
contextmenu(codeMirror, event) {
|
||||
openEditorMenu(event, codeMirror);
|
||||
},
|
||||
};
|
||||
input = (
|
||||
<div className="form-field--SimpleMDE">
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
options={{
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
|||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
spinner: boolean,
|
||||
status: string,
|
||||
spinner: boolean,
|
||||
};
|
||||
|
||||
class LoadingScreen extends React.PureComponent<Props> {
|
||||
|
@ -17,8 +17,7 @@ class LoadingScreen extends React.PureComponent<Props> {
|
|||
return (
|
||||
<div className="content__loading">
|
||||
{spinner && <Spinner light />}
|
||||
|
||||
<span className="content__loading-text">{status}</span>
|
||||
{status && <span className="content__loading-text">{status}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -152,7 +152,12 @@ class FileList extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo;
|
||||
const {
|
||||
name: claimName,
|
||||
claim_name: claimNameDownloaded,
|
||||
claim_id: claimId,
|
||||
outpoint,
|
||||
} = fileInfo;
|
||||
const uriParams = {};
|
||||
|
||||
// This is unfortunate
|
||||
|
@ -162,7 +167,8 @@ class FileList extends React.PureComponent<Props, State> {
|
|||
uriParams.claimId = claimId;
|
||||
const uri = buildURI(uriParams);
|
||||
|
||||
content.push(<FileCard key={uri} uri={uri} checkPending={checkPending} />);
|
||||
// See https://github.com/lbryio/lbry-app/issues/1327 for discussion around using outpoint as the key
|
||||
content.push(<FileCard key={outpoint} uri={uri} checkPending={checkPending} />);
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
16
src/renderer/component/fileRender/index.js
Normal file
16
src/renderer/component/fileRender/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { THEME } from 'constants/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
|
||||
import FileRender from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
currentTheme: makeSelectClientSetting(THEME)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({});
|
||||
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileRender);
|
47
src/renderer/component/fileRender/view.jsx
Normal file
47
src/renderer/component/fileRender/view.jsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import PdfViewer from 'component/viewers/pdfViewer';
|
||||
|
||||
type Props = {
|
||||
mediaType: string,
|
||||
source: {
|
||||
filePath: string,
|
||||
fileType: string,
|
||||
downloadPath: string,
|
||||
},
|
||||
currentTheme: string,
|
||||
};
|
||||
|
||||
class FileRender extends React.PureComponent<Props> {
|
||||
renderViewer() {
|
||||
const { source, mediaType, currentTheme } = this.props;
|
||||
const viewerProps = { source, theme: currentTheme };
|
||||
|
||||
// Supported mediaTypes
|
||||
const mediaTypes = {
|
||||
// '3D-file': <ThreeViewer {...viewerProps}/>,
|
||||
// Add routes to viewer...
|
||||
};
|
||||
|
||||
// Supported fileType
|
||||
const fileTypes = {
|
||||
pdf: <PdfViewer {...viewerProps} />,
|
||||
// Add routes to viewer...
|
||||
};
|
||||
|
||||
const { fileType } = source;
|
||||
const viewer = mediaType && source && (mediaTypes[mediaType] || fileTypes[fileType]);
|
||||
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
|
||||
const unsupported = <LoadingScreen status={unsupportedMessage} spinner={false} />;
|
||||
|
||||
// Return viewer
|
||||
return viewer || unsupported;
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="file-render">{this.renderViewer()}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default FileRender;
|
|
@ -17,7 +17,7 @@ import {
|
|||
import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings';
|
||||
import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media';
|
||||
import { selectPlayingUri } from 'redux/selectors/content';
|
||||
import Video from './view';
|
||||
import FileViewer from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
|
@ -45,4 +45,7 @@ const perform = dispatch => ({
|
|||
savePosition: (claimId, position) => dispatch(savePosition(claimId, position)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(Video);
|
||||
export default connect(
|
||||
select,
|
||||
perform
|
||||
)(FileViewer);
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
play: () => void,
|
||||
|
@ -14,8 +15,8 @@ class VideoPlayButton extends React.PureComponent<Props> {
|
|||
const { fileInfo, mediaType, isLoading, play } = this.props;
|
||||
const disabled = isLoading || fileInfo === undefined;
|
||||
const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||
const icon = doesPlayback ? 'Play' : 'Folder';
|
||||
const label = doesPlayback ? 'Play' : 'View';
|
||||
const icon = doesPlayback ? icons.PLAY : icons.EYE;
|
||||
const label = doesPlayback ? __('Play') : __('View');
|
||||
|
||||
return <Button button="primary" disabled={disabled} label={label} icon={icon} onClick={play} />;
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { remote } from 'electron';
|
||||
import Thumbnail from 'component/common/thumbnail';
|
||||
import player from 'render-media';
|
||||
import fs from 'fs';
|
||||
import LoadingScreen from './loading-screen';
|
||||
import path from 'path';
|
||||
import player from 'render-media';
|
||||
import toBlobURL from 'stream-to-blob-url';
|
||||
import FileRender from 'component/fileRender';
|
||||
import Thumbnail from 'component/common/thumbnail';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
|
||||
class VideoPlayer extends React.PureComponent {
|
||||
static MP3_CONTENT_TYPES = ['audio/mpeg3', 'audio/mpeg'];
|
||||
static FILE_MEDIA_TYPES = ['e-book', 'comic-book', 'document', '3D-file'];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -16,6 +20,7 @@ class VideoPlayer extends React.PureComponent {
|
|||
hasMetadata: false,
|
||||
startedPlaying: false,
|
||||
unplayable: false,
|
||||
fileSource: null,
|
||||
};
|
||||
|
||||
this.togglePlayListener = this.togglePlay.bind(this);
|
||||
|
@ -29,7 +34,7 @@ class VideoPlayer extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
const container = this.media;
|
||||
const { contentType, changeVolume, volume, position, claim } = this.props;
|
||||
const { downloadCompleted, contentType, changeVolume, volume, position, claim } = this.props;
|
||||
|
||||
const loadedMetadata = () => {
|
||||
this.setState({ hasMetadata: true, startedPlaying: true });
|
||||
|
@ -51,7 +56,13 @@ class VideoPlayer extends React.PureComponent {
|
|||
// use renderAudio override for mp3
|
||||
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
|
||||
this.renderAudio(container, null, false);
|
||||
} else {
|
||||
}
|
||||
// Render custom viewer: FileRender
|
||||
else if (this.fileType()) {
|
||||
downloadCompleted && this.renderFile();
|
||||
}
|
||||
// Render default viewer: render-media (video, audio, img, iframe)
|
||||
else {
|
||||
player.append(
|
||||
this.file(),
|
||||
container,
|
||||
|
@ -89,7 +100,7 @@ class VideoPlayer extends React.PureComponent {
|
|||
|
||||
componentDidUpdate() {
|
||||
const { contentType, downloadCompleted } = this.props;
|
||||
const { startedPlaying } = this.state;
|
||||
const { startedPlaying, fileSource } = this.state;
|
||||
|
||||
if (this.playableType() && !startedPlaying && downloadCompleted) {
|
||||
const container = this.media.children[0];
|
||||
|
@ -102,6 +113,8 @@ class VideoPlayer extends React.PureComponent {
|
|||
controls: true,
|
||||
});
|
||||
}
|
||||
} else if (this.fileType() && !fileSource && downloadCompleted) {
|
||||
this.renderFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,6 +172,40 @@ class VideoPlayer extends React.PureComponent {
|
|||
return ['audio', 'video'].indexOf(mediaType) !== -1;
|
||||
}
|
||||
|
||||
supportedType() {
|
||||
// Files supported by render-media
|
||||
const { contentType, mediaType } = this.props;
|
||||
|
||||
return Object.values(player.mime).indexOf(contentType) !== -1;
|
||||
}
|
||||
|
||||
fileType() {
|
||||
// This files are supported using a custom viewer
|
||||
const { mediaType } = this.props;
|
||||
|
||||
return VideoPlayer.FILE_MEDIA_TYPES.indexOf(mediaType) > -1;
|
||||
}
|
||||
|
||||
renderFile() {
|
||||
// This is what render-media does with unplayable files
|
||||
const { filename, downloadPath, contentType, mediaType } = this.props;
|
||||
|
||||
toBlobURL(fs.createReadStream(downloadPath), contentType, (err, url) => {
|
||||
if (err) {
|
||||
this.setState({ unsupported: true });
|
||||
return false;
|
||||
}
|
||||
// File to render
|
||||
const fileSource = {
|
||||
downloadPath,
|
||||
filePath: url,
|
||||
fileType: path.extname(filename).substring(1),
|
||||
};
|
||||
// Update state
|
||||
this.setState({ fileSource });
|
||||
});
|
||||
}
|
||||
|
||||
renderAudio(container, autoplay) {
|
||||
if (container.firstChild) {
|
||||
container.firstChild.remove();
|
||||
|
@ -173,25 +220,61 @@ class VideoPlayer extends React.PureComponent {
|
|||
container.appendChild(audio);
|
||||
}
|
||||
|
||||
showLoadingScreen(isFileType, isPlayableType) {
|
||||
const { mediaType } = this.props;
|
||||
const { hasMetadata, unplayable, unsupported, fileSource } = this.state;
|
||||
|
||||
const loader = {
|
||||
isLoading: false,
|
||||
loadingStatus: null,
|
||||
};
|
||||
|
||||
// Loading message
|
||||
const noFileMessage = __('Waiting for blob.');
|
||||
const noMetadataMessage = __('Waiting for metadata.');
|
||||
|
||||
// Error message
|
||||
const unplayableMessage = __("Sorry, looks like we can't play this file.");
|
||||
const unsupportedMessage = __("Sorry, looks like we can't preview this file.");
|
||||
|
||||
// Files
|
||||
const isLoadingFile = !fileSource && isFileType;
|
||||
const isUnsupported =
|
||||
(mediaType === 'application' || !this.supportedType()) && !isFileType && !isPlayableType;
|
||||
|
||||
// Media (audio, video)
|
||||
const isUnplayable = isPlayableType && unplayable;
|
||||
const isLoadingMetadata = isPlayableType && (!hasMetadata && !unplayable);
|
||||
|
||||
// Show loading message
|
||||
if (isLoadingFile || isLoadingMetadata) {
|
||||
loader.loadingStatus = isFileType ? noFileMessage : noMetadataMessage;
|
||||
loader.isLoading = true;
|
||||
|
||||
// Show unsupported error message
|
||||
} else if (isUnsupported || isUnplayable) {
|
||||
loader.loadingStatus = isUnsupported ? unsupportedMessage : unplayableMessage;
|
||||
}
|
||||
|
||||
return loader;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mediaType, poster } = this.props;
|
||||
const { hasMetadata, unplayable } = this.state;
|
||||
const noMetadataMessage = 'Waiting for metadata.';
|
||||
const unplayableMessage = "Sorry, looks like we can't play this file.";
|
||||
const hideMedia = this.playableType() && !hasMetadata && !unplayable;
|
||||
const { mediaType } = this.props;
|
||||
const { fileSource } = this.state;
|
||||
|
||||
const isFileType = this.fileType();
|
||||
const isFileReady = fileSource && isFileType;
|
||||
const isPlayableType = this.playableType();
|
||||
const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{['audio', 'application'].indexOf(mediaType) !== -1 &&
|
||||
(!this.playableType() || hasMetadata) &&
|
||||
!unplayable && <Thumbnail src={poster} />}
|
||||
{this.playableType() &&
|
||||
!hasMetadata &&
|
||||
!unplayable && <LoadingScreen status={noMetadataMessage} />}
|
||||
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
|
||||
{loadingStatus && <LoadingScreen status={loadingStatus} spinner={isLoading} />}
|
||||
{isFileReady && <FileRender source={fileSource} mediaType={mediaType} />}
|
||||
<div
|
||||
className={'content__view--container'}
|
||||
style={{ opacity: hideMedia ? 0 : 1 }}
|
||||
style={{ opacity: isLoading ? 0 : 1 }}
|
||||
ref={container => {
|
||||
this.media = container;
|
||||
}}
|
|
@ -1,11 +1,10 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import classnames from 'classnames';
|
||||
import type { Claim } from 'types/claim';
|
||||
import VideoPlayer from './internal/player';
|
||||
import VideoPlayButton from './internal/play-button';
|
||||
import LoadingScreen from './internal/loading-screen';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import Player from './internal/player';
|
||||
import PlayButton from './internal/play-button';
|
||||
|
||||
const SPACE_BAR_KEYCODE = 32;
|
||||
|
||||
|
@ -40,9 +39,10 @@ type Props = {
|
|||
obscureNsfw: boolean,
|
||||
play: string => void,
|
||||
searchBarFocused: boolean,
|
||||
mediaType: string,
|
||||
};
|
||||
|
||||
class Video extends React.PureComponent<Props> {
|
||||
class FileViewer extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
@ -123,12 +123,12 @@ class Video extends React.PureComponent<Props> {
|
|||
mediaPosition,
|
||||
className,
|
||||
obscureNsfw,
|
||||
mediaType,
|
||||
} = this.props;
|
||||
|
||||
const isPlaying = playingUri === uri;
|
||||
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
|
||||
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
|
||||
const mediaType = Lbry.getMediaType(contentType, fileInfo && fileInfo.file_name);
|
||||
|
||||
let loadStatusMessage = '';
|
||||
|
||||
|
@ -156,7 +156,7 @@ class Video extends React.PureComponent<Props> {
|
|||
<LoadingScreen status={loadStatusMessage} />
|
||||
</div>
|
||||
) : (
|
||||
<VideoPlayer
|
||||
<Player
|
||||
filename={fileInfo.file_name}
|
||||
poster={poster}
|
||||
downloadPath={fileInfo.download_path}
|
||||
|
@ -183,7 +183,7 @@ class Video extends React.PureComponent<Props> {
|
|||
className={layoverClass}
|
||||
style={layoverStyle}
|
||||
>
|
||||
<VideoPlayButton
|
||||
<PlayButton
|
||||
play={e => {
|
||||
e.stopPropagation();
|
||||
this.playContent();
|
||||
|
@ -200,4 +200,4 @@ class Video extends React.PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default Video;
|
||||
export default FileViewer;
|
|
@ -25,11 +25,13 @@ class BidHelpText extends React.PureComponent<Props> {
|
|||
} = this.props;
|
||||
|
||||
if (!uri) {
|
||||
return __('Create a URL for this content');
|
||||
return __('Create a URL for this content.');
|
||||
}
|
||||
|
||||
if (isStillEditing) {
|
||||
return __('You are currently editing this claim');
|
||||
return __(
|
||||
'You are currently editing this claim. If you change the URL, you will need reselect a file.'
|
||||
);
|
||||
}
|
||||
|
||||
if (isResolvingUri) {
|
||||
|
@ -61,10 +63,10 @@ class BidHelpText extends React.PureComponent<Props> {
|
|||
<React.Fragment>
|
||||
{__('A deposit greater than')} {winningBidForClaimUri} {__('is needed to win')}
|
||||
{` ${uri}. `}
|
||||
{__('However, you can still get this URL for any amount')}
|
||||
{__('However, you can still get this URL for any amount.')}
|
||||
</React.Fragment>
|
||||
) : (
|
||||
__('Any amount will give you the winning bid')
|
||||
__('Any amount will give you the winning bid.')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,14 +152,21 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
handleBidChange(bid: number) {
|
||||
const { balance, updatePublishForm } = this.props;
|
||||
const { balance, updatePublishForm, myClaimForUri } = this.props;
|
||||
|
||||
let previousBidAmount = 0;
|
||||
if (myClaimForUri) {
|
||||
previousBidAmount = myClaimForUri.amount;
|
||||
}
|
||||
|
||||
const totalAvailableBidAmount = previousBidAmount + balance;
|
||||
|
||||
let bidError;
|
||||
if (bid === 0) {
|
||||
bidError = __('Deposit cannot be 0');
|
||||
} else if (balance === bid) {
|
||||
} else if (totalAvailableBidAmount === bid) {
|
||||
bidError = __('Please decrease your deposit to account for transaction fees');
|
||||
} else if (balance < bid) {
|
||||
} else if (totalAvailableBidAmount < bid) {
|
||||
bidError = __('Deposit cannot be higher than your balance');
|
||||
} else if (bid <= MINIMUM_PUBLISH_BID) {
|
||||
bidError = __('Your deposit must be higher');
|
||||
|
@ -237,32 +244,54 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
checkIsFormValid() {
|
||||
const { name, nameError, title, bid, bidError, tosAccepted } = this.props;
|
||||
return name && !nameError && title && bid && !bidError && tosAccepted;
|
||||
const {
|
||||
name,
|
||||
nameError,
|
||||
title,
|
||||
bid,
|
||||
bidError,
|
||||
tosAccepted,
|
||||
editingURI,
|
||||
isStillEditing,
|
||||
filePath,
|
||||
} = this.props;
|
||||
|
||||
// If they are editing, they don't need a new file chosen
|
||||
const formValidLessFile = name && !nameError && title && bid && !bidError && tosAccepted;
|
||||
return editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
|
||||
}
|
||||
|
||||
renderFormErrors() {
|
||||
const { name, nameError, title, bid, bidError, tosAccepted } = this.props;
|
||||
const {
|
||||
name,
|
||||
nameError,
|
||||
title,
|
||||
bid,
|
||||
bidError,
|
||||
tosAccepted,
|
||||
editingURI,
|
||||
filePath,
|
||||
isStillEditing,
|
||||
} = this.props;
|
||||
|
||||
if (nameError || bidError) {
|
||||
// There will be inline errors if either of these exist
|
||||
// These are just extra help at the bottom of the screen
|
||||
// There could be multiple bid errors, so just duplicate it at the bottom
|
||||
return (
|
||||
<div className="card__subtitle form-field__error">
|
||||
{nameError && <div>{__('The URL you created is not valid.')}</div>}
|
||||
{bidError && <div>{bidError}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const isFormValid = this.checkIsFormValid();
|
||||
|
||||
// These are extra help
|
||||
// If there is an error it will be presented as an inline error as well
|
||||
return (
|
||||
<div className="card__content card__subtitle card__subtitle--block form-field__error">
|
||||
{!title && <div>{__('A title is required')}</div>}
|
||||
{!name && <div>{__('A URL is required')}</div>}
|
||||
{!bid && <div>{__('A bid amount is required')}</div>}
|
||||
{!tosAccepted && <div>{__('You must agree to the terms of service')}</div>}
|
||||
</div>
|
||||
!isFormValid && (
|
||||
<div className="card__content card__subtitle form-field__error">
|
||||
{!title && <div>{__('A title is required')}</div>}
|
||||
{!name && <div>{__('A URL is required')}</div>}
|
||||
{name && nameError && <div>{__('The URL you created is not valid')}</div>}
|
||||
{!bid && <div>{__('A bid amount is required')}</div>}
|
||||
{!!bid && bidError && <div>{bidError}</div>}
|
||||
{!tosAccepted && <div>{__('You must agree to the terms of service')}</div>}
|
||||
{!!editingURI &&
|
||||
!isStillEditing &&
|
||||
!filePath && <div>{__('You need to reselect a file after changing the LBRY URL')}</div>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -312,10 +341,10 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<Form onSubmit={this.handlePublish}>
|
||||
<section className={classnames('card card--section')}>
|
||||
<section className={classnames('card card--section', { 'card--disabled': publishing })}>
|
||||
<div className="card__title">{__('Content')}</div>
|
||||
<div className="card__subtitle">
|
||||
{editingURI ? __('Editing a claim') : __('What are you publishing?')}
|
||||
{isStillEditing ? __('Editing a claim') : __('What are you publishing?')}
|
||||
</div>
|
||||
{(filePath || !!editingURI) && (
|
||||
<div className="card-media__internal-links">
|
||||
|
@ -328,7 +357,7 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
</div>
|
||||
)}
|
||||
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
|
||||
{!!editingURI && (
|
||||
{!!isStillEditing && (
|
||||
<p className="card__content card__subtitle">
|
||||
{__("If you don't choose a file, the file from your existing claim")}
|
||||
{` "${name}" `}
|
||||
|
@ -368,11 +397,11 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
<div className="card__title">{__('Thumbnail')}</div>
|
||||
<div className="card__subtitle">
|
||||
{uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
|
||||
__('Enter a url for your thumbnail.')
|
||||
__('Enter a URL for your thumbnail.')
|
||||
) : (
|
||||
<React.Fragment>
|
||||
{__(
|
||||
'Upload your thumbnail to spee.ch, or enter the url manually. Learn more about spee.ch '
|
||||
'Upload your thumbnail (.png/.jpg/.jpeg/.gif) to spee.ch, or enter the URL manually. Learn more about spee.ch '
|
||||
)}
|
||||
<Button button="link" label={__('here')} href="https://spee.ch/about" />.
|
||||
</React.Fragment>
|
||||
|
|
|
@ -35,7 +35,7 @@ class SelectThumbnail extends React.PureComponent<Props> {
|
|||
stretch
|
||||
type="text"
|
||||
name="content_thumbnail"
|
||||
label={__('Url')}
|
||||
label={__('URL')}
|
||||
placeholder="http://spee.ch/mylogo"
|
||||
value={thumbnail}
|
||||
disabled={formDisabled}
|
||||
|
|
|
@ -56,7 +56,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
continousFetch: ?number;
|
||||
continousFetch: ?IntervalID;
|
||||
|
||||
render() {
|
||||
const {
|
||||
|
@ -135,8 +135,8 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
{shiftState === statuses.NO_DEPOSITS &&
|
||||
shiftReturnAddress && (
|
||||
<div className="help">
|
||||
If the transaction doesn't go through, ShapeShift will return your {shiftCoinType}{' '}
|
||||
back to {shiftReturnAddress}
|
||||
{__("If the transaction doesn't go through, ShapeShift will return your")}{' '}
|
||||
{shiftCoinType} {__('back to')} {shiftReturnAddress}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -72,6 +72,7 @@ class TransactionListItem extends React.PureComponent<Props> {
|
|||
{name &&
|
||||
claimId && (
|
||||
<Button
|
||||
tourniquet
|
||||
button="link"
|
||||
navigate="/show"
|
||||
navigateParams={{ uri: buildURI({ claimName: name, claimId }) }}
|
||||
|
|
37
src/renderer/component/viewers/pdfViewer.jsx
Normal file
37
src/renderer/component/viewers/pdfViewer.jsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
source: {
|
||||
fileType: string,
|
||||
filePath: string,
|
||||
downloadPath: string,
|
||||
},
|
||||
};
|
||||
|
||||
class PdfViewer extends React.PureComponent<Props> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.viewer = React.createRef();
|
||||
}
|
||||
|
||||
// TODO: Enable context-menu
|
||||
stopContextMenu = event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { source } = this.props;
|
||||
return (
|
||||
<div className="file-render__viewer" onContextMenu={this.stopContextMenu}>
|
||||
<webview
|
||||
ref={this.viewer}
|
||||
src={`chrome://pdf-viewer/index.html?src=file://${source.downloadPath}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PdfViewer;
|
|
@ -28,3 +28,5 @@ export const CHECK_SIMPLE = 'Check';
|
|||
export const GLOBE = 'Globe';
|
||||
export const EXTERNAL_LINK = 'ExternalLink';
|
||||
export const GIFT = 'Gift';
|
||||
export const EYE = 'Eye';
|
||||
export const PLAY = 'Play';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
export const NO_DEPOSITS = 'no_deposits';
|
||||
export const RECEIVED = 'received';
|
||||
export const COMPLETE = 'complete';
|
||||
export const AVAILABLE = 'available';
|
||||
export const UNAVAILABLE = 'unavailable';
|
||||
|
|
|
@ -48,7 +48,7 @@ class ChannelPage extends React.PureComponent<Props> {
|
|||
this.props.navigate('/show', newParams);
|
||||
}
|
||||
|
||||
paginate(e, totalPages: number) {
|
||||
paginate(e: SyntheticKeyboardEvent<*>, totalPages: number) {
|
||||
// Change page if enter was pressed, and the given page is between
|
||||
// the first and the last.
|
||||
const pageFromInput = Number(e.target.value);
|
||||
|
@ -67,22 +67,21 @@ class ChannelPage extends React.PureComponent<Props> {
|
|||
const { fetching, claimsInChannel, claim, page, totalPages } = this.props;
|
||||
const { name, permanent_url: permanentUrl, claim_id: claimId } = claim;
|
||||
const currentPage = parseInt((page || 1) - 1, 10);
|
||||
let contentList;
|
||||
if (fetching) {
|
||||
contentList = <BusyIndicator message={__('Fetching content')} />;
|
||||
} else {
|
||||
contentList =
|
||||
claimsInChannel && claimsInChannel.length ? (
|
||||
<FileList sortByHeight hideFilter fileInfos={claimsInChannel} />
|
||||
) : (
|
||||
<span className="empty">{__('No content found.')}</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentList =
|
||||
claimsInChannel && claimsInChannel.length ? (
|
||||
<FileList sortByHeight hideFilter fileInfos={claimsInChannel} />
|
||||
) : (
|
||||
!fetching && <span className="empty">{__('No content found.')}</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page notContained>
|
||||
<section className="card__channel-info card__channel-info--large">
|
||||
<h1>{name}</h1>
|
||||
<h1>
|
||||
{name}
|
||||
{fetching && <BusyIndicator />}
|
||||
</h1>
|
||||
<div className="card__actions card__actions--no-margin">
|
||||
<SubscribeButton uri={permanentUrl} channelName={name} />
|
||||
<ViewOnWebButton claimId={claimId} claimName={name} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Lbry, buildURI, normalizeURI, MODALS } from 'lbry-redux';
|
||||
import Video from 'component/video';
|
||||
import { buildURI, normalizeURI, MODALS } from 'lbry-redux';
|
||||
import FileViewer from 'component/fileViewer';
|
||||
import Thumbnail from 'component/common/thumbnail';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import FileDetails from 'component/fileDetails';
|
||||
|
@ -14,7 +14,6 @@ import Button from 'component/button';
|
|||
import SubscribeButton from 'component/subscribeButton';
|
||||
import ViewOnWebButton from 'component/viewOnWebButton';
|
||||
import Page from 'component/page';
|
||||
import player from 'render-media';
|
||||
import * as settings from 'constants/settings';
|
||||
import type { Claim } from 'types/claim';
|
||||
import type { Subscription } from 'types/subscription';
|
||||
|
@ -22,6 +21,7 @@ import FileDownloadLink from 'component/fileDownloadLink';
|
|||
import classnames from 'classnames';
|
||||
import { FormField, FormRow } from 'component/common/form';
|
||||
import ToolTip from 'component/common/tooltip';
|
||||
import getMediaType from 'util/getMediaType';
|
||||
|
||||
type Props = {
|
||||
claim: Claim,
|
||||
|
@ -29,6 +29,7 @@ type Props = {
|
|||
metadata: {
|
||||
title: string,
|
||||
thumbnail: string,
|
||||
file_name: string,
|
||||
nsfw: boolean,
|
||||
},
|
||||
contentType: string,
|
||||
|
@ -49,6 +50,18 @@ type Props = {
|
|||
};
|
||||
|
||||
class FilePage extends React.Component<Props> {
|
||||
static PLAYABLE_MEDIA_TYPES = ['audio', 'video'];
|
||||
static PREVIEW_MEDIA_TYPES = [
|
||||
'text',
|
||||
'model',
|
||||
'image',
|
||||
'3D-file',
|
||||
'document',
|
||||
// Bypass unplayable files
|
||||
// TODO: Find a better way to detect supported types
|
||||
'application',
|
||||
];
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
|
@ -108,15 +121,19 @@ class FilePage extends React.Component<Props> {
|
|||
navigate,
|
||||
autoplay,
|
||||
costInfo,
|
||||
fileInfo,
|
||||
} = this.props;
|
||||
|
||||
// File info
|
||||
const { title, thumbnail } = metadata;
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
|
||||
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
|
||||
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const mediaType = Lbry.getMediaType(contentType);
|
||||
const isPlayable = Object.values(player.mime).includes(contentType) || mediaType === 'audio';
|
||||
const fileName = fileInfo ? fileInfo.file_name : null;
|
||||
const mediaType = getMediaType(contentType, fileName);
|
||||
const showFile =
|
||||
PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType);
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
let subscriptionUri;
|
||||
|
@ -150,8 +167,10 @@ class FilePage extends React.Component<Props> {
|
|||
</section>
|
||||
) : (
|
||||
<section className="card">
|
||||
{isPlayable && <Video className="content__embedded" uri={uri} />}
|
||||
{!isPlayable &&
|
||||
{showFile && (
|
||||
<FileViewer className="content__embedded" uri={uri} mediaType={mediaType} />
|
||||
)}
|
||||
{!showFile &&
|
||||
(thumbnail ? (
|
||||
<Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} />
|
||||
) : (
|
||||
|
@ -160,7 +179,9 @@ class FilePage extends React.Component<Props> {
|
|||
'content__empty--nsfw': shouldObscureThumbnail,
|
||||
})}
|
||||
>
|
||||
<div className="card__media-text">{__('This content is not playable.')}</div>
|
||||
<div className="card__media-text">
|
||||
{__("Sorry, looks like we can't preview this file.")}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="card__content">
|
||||
|
@ -182,34 +203,32 @@ class FilePage extends React.Component<Props> {
|
|||
<UriIndicator uri={uri} link />
|
||||
</div>
|
||||
<div className="card__actions card__actions--no-margin card__actions--between">
|
||||
{(claimIsMine || subscriptionUri || speechSharable) && (
|
||||
<div className="card__actions">
|
||||
{claimIsMine ? (
|
||||
<Button
|
||||
button="primary"
|
||||
icon={icons.EDIT}
|
||||
label={__('Edit')}
|
||||
onClick={() => {
|
||||
prepareEdit(claim, editUri);
|
||||
navigate('/publish');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
||||
)}
|
||||
{!claimIsMine && (
|
||||
<Button
|
||||
button="alt"
|
||||
icon={icons.GIFT}
|
||||
label={__('Enjoy this? Send a tip')}
|
||||
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
|
||||
/>
|
||||
)}
|
||||
{speechSharable && (
|
||||
<ViewOnWebButton claimId={claim.claim_id} claimName={claim.name} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="card__actions">
|
||||
{claimIsMine ? (
|
||||
<Button
|
||||
button="primary"
|
||||
icon={icons.EDIT}
|
||||
label={__('Edit')}
|
||||
onClick={() => {
|
||||
prepareEdit(claim, editUri);
|
||||
navigate('/publish');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
|
||||
)}
|
||||
{!claimIsMine && (
|
||||
<Button
|
||||
button="alt"
|
||||
icon={icons.GIFT}
|
||||
label={__('Enjoy this? Send a tip')}
|
||||
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
|
||||
/>
|
||||
)}
|
||||
{speechSharable && (
|
||||
<ViewOnWebButton claimId={claim.claim_id} claimName={claim.name} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card__actions">
|
||||
<FileDownloadLink uri={uri} />
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { FileTile } from 'component/fileTile';
|
||||
import FileList from 'component/fileList';
|
||||
import Page from 'component/page';
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import ChannelPage from 'page/channel';
|
||||
import FilePage from 'page/file';
|
||||
|
@ -39,10 +40,11 @@ class ShowPage extends React.PureComponent<Props> {
|
|||
let innerContent = '';
|
||||
|
||||
if ((isResolvingUri && !claim) || !claim) {
|
||||
const { claimName } = parseURI(uri);
|
||||
innerContent = (
|
||||
<Page>
|
||||
<section className="card">
|
||||
<h1>{uri}</h1>
|
||||
<h1>{claimName}</h1>
|
||||
<div className="card__content">
|
||||
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
|
||||
{claim === null &&
|
||||
|
|
|
@ -253,17 +253,19 @@ export function doCheckUpgradeSubscribe() {
|
|||
export function doCheckDaemonVersion() {
|
||||
return dispatch => {
|
||||
Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => {
|
||||
if (config.lbrynetDaemonVersion === lbrynetVersion) {
|
||||
dispatch({
|
||||
// Avoid the incompatible daemon modal if running in dev mode
|
||||
// Lets you run a different daemon than the one specified in package.json
|
||||
if (isDev || config.lbrynetDaemonVersion === lbrynetVersion) {
|
||||
return dispatch({
|
||||
type: ACTIONS.DAEMON_VERSION_MATCH,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.DAEMON_VERSION_MISMATCH,
|
||||
});
|
||||
dispatch(
|
||||
|
||||
return dispatch(
|
||||
doNotify({
|
||||
id: MODALS.INCOMPATIBLE_DAEMON,
|
||||
})
|
||||
|
|
|
@ -74,6 +74,7 @@ export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction
|
|||
export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => {
|
||||
const thumbnail = fs.readFileSync(filePath);
|
||||
const fileExt = path.extname(filePath);
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
const makeid = () => {
|
||||
let text = '';
|
||||
|
@ -100,9 +101,9 @@ export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch:
|
|||
|
||||
const data = new FormData();
|
||||
const name = makeid();
|
||||
const blob = new Blob([thumbnail], { type: `image/${fileExt.slice(1)}` });
|
||||
const file = new File([thumbnail], fileName, { type: `image/${fileExt.slice(1)}` });
|
||||
data.append('name', name);
|
||||
data.append('file', blob);
|
||||
data.append('file', file);
|
||||
data.append('nsfw', nsfw.toString());
|
||||
return fetch('https://spee.ch/api/claim/publish', {
|
||||
method: 'POST',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import Promise from 'bluebird';
|
||||
import * as SHAPESHIFT_STATUSES from 'constants/shape_shift';
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { coinRegexPatterns } from 'util/shape_shift';
|
||||
import type {
|
||||
|
@ -65,9 +66,15 @@ export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => {
|
|||
return shapeShift
|
||||
.coinsAsync()
|
||||
.then(coinData => {
|
||||
if (coinData.LBC.status === SHAPESHIFT_STATUSES.UNAVAILABLE) {
|
||||
return dispatch({
|
||||
type: ACTIONS.GET_SUPPORTED_COINS_FAIL,
|
||||
});
|
||||
}
|
||||
|
||||
let supportedCoins = [];
|
||||
Object.keys(coinData).forEach(symbol => {
|
||||
if (coinData[symbol].status === 'available') {
|
||||
if (coinData[symbol].status === SHAPESHIFT_STATUSES.UNAVAILABLE) {
|
||||
supportedCoins.push(coinData[symbol]);
|
||||
}
|
||||
});
|
||||
|
@ -81,7 +88,7 @@ export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => {
|
|||
type: ACTIONS.GET_SUPPORTED_COINS_SUCCESS,
|
||||
data: supportedCoins,
|
||||
});
|
||||
dispatch(getCoinStats(supportedCoins[0]));
|
||||
return dispatch(getCoinStats(supportedCoins[0]));
|
||||
})
|
||||
.catch(err => dispatch({ type: ACTIONS.GET_SUPPORTED_COINS_FAIL, data: err }));
|
||||
};
|
||||
|
|
|
@ -117,7 +117,7 @@ export default handleActions(
|
|||
[ACTIONS.GET_SUPPORTED_COINS_FAIL]: (state: ShapeShiftState): ShapeShiftState => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: 'Error getting available coins',
|
||||
error: __('There was an error. Please try again later.'),
|
||||
}),
|
||||
|
||||
[ACTIONS.GET_COIN_STATS_START]: (
|
||||
|
|
|
@ -23,5 +23,6 @@
|
|||
@import 'component/_spinner.scss';
|
||||
@import 'component/_nav.scss';
|
||||
@import 'component/_file-list.scss';
|
||||
@import 'component/_file-render.scss';
|
||||
@import 'component/_search.scss';
|
||||
@import 'component/_toggle.scss';
|
||||
|
|
|
@ -62,7 +62,7 @@ button:disabled {
|
|||
font-size: 1em;
|
||||
color: var(--btn-color-inverse);
|
||||
border-radius: 0;
|
||||
display: inline-block;
|
||||
display: inline;
|
||||
min-width: 0;
|
||||
box-shadow: none;
|
||||
text-align: left;
|
||||
|
@ -76,6 +76,13 @@ button:disabled {
|
|||
background-color: var(--btn-bg-secondary);
|
||||
}
|
||||
|
||||
.btn.btn--tourniquet {
|
||||
max-width: 20vw;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.btn.btn--no-style {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
|
|
|
@ -150,10 +150,6 @@
|
|||
padding-top: $spacing-vertical * 1/3;
|
||||
}
|
||||
|
||||
.card__subtitle--block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card__meta {
|
||||
color: var(--color-help);
|
||||
font-size: 14px;
|
||||
|
|
|
@ -96,6 +96,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-render {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.file-render__viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
|
|
25
src/renderer/scss/component/_file-render.scss
Normal file
25
src/renderer/scss/component/_file-render.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
.file-render {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.file-render__viewer {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: black;
|
||||
|
||||
iframe,
|
||||
webview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,10 @@
|
|||
border: solid 1px var(--color-divider);
|
||||
margin: $spacing-vertical $spacing-vertical * 2/3;
|
||||
}
|
||||
|
||||
@media (min-width: $large-breakpoint) {
|
||||
width: calc(var(--side-nav-width) * 1.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar links
|
||||
|
|
|
@ -80,6 +80,14 @@ table.table--help {
|
|||
font-family: 'metropolis-semibold';
|
||||
min-width: 130px;
|
||||
}
|
||||
td:nth-of-type(2) {
|
||||
/*Tourniquets text over 20VW*/
|
||||
max-width: 20vw;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color-help);
|
||||
}
|
||||
}
|
||||
|
||||
table.table--transactions {
|
||||
|
|
|
@ -21,11 +21,11 @@ function injectDevelopmentTemplate(event, templates) {
|
|||
return templates;
|
||||
}
|
||||
|
||||
export function openContextMenu(event, templates = []) {
|
||||
const isSomethingSelected = window.getSelection().toString().length > 0;
|
||||
const { type } = event.target;
|
||||
export function openContextMenu(event, templates = [], canEdit = false, selection = '') {
|
||||
const { type, value } = event.target;
|
||||
const isSomethingSelected = selection.length > 0 || window.getSelection().toString().length > 0;
|
||||
const isInput = event.target.matches('input') && (type === 'text' || type === 'number');
|
||||
const { value } = event.target;
|
||||
const isTextField = canEdit || isInput || event.target.matches('textarea');
|
||||
|
||||
templates.push({
|
||||
label: 'Copy',
|
||||
|
@ -36,12 +36,12 @@ export function openContextMenu(event, templates = []) {
|
|||
|
||||
// If context menu is opened on Input and there is text on the input and something is selected.
|
||||
const { selectionStart, selectionEnd } = event.target;
|
||||
if (!!value && isInput && selectionStart !== selectionEnd) {
|
||||
if (!!value && isTextField && selectionStart !== selectionEnd) {
|
||||
templates.push({ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' });
|
||||
}
|
||||
|
||||
// If context menu is opened on Input and text is present on clipboard
|
||||
if (clipboard.readText().length > 0 && isInput) {
|
||||
if (clipboard.readText().length > 0 && isTextField) {
|
||||
templates.push({
|
||||
label: 'Paste',
|
||||
accelerator: 'CmdOrCtrl+V',
|
||||
|
@ -50,7 +50,7 @@ export function openContextMenu(event, templates = []) {
|
|||
}
|
||||
|
||||
// If context menu is opened on Input
|
||||
if (isInput && value) {
|
||||
if (isTextField && value) {
|
||||
templates.push({
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
|
@ -61,6 +61,31 @@ export function openContextMenu(event, templates = []) {
|
|||
injectDevelopmentTemplate(event, templates);
|
||||
remote.Menu.buildFromTemplate(templates).popup();
|
||||
}
|
||||
|
||||
// This function is used for the markdown description on the publish page
|
||||
export function openEditorMenu(event, codeMirror) {
|
||||
const value = codeMirror.doc.getValue();
|
||||
const selection = codeMirror.doc.getSelection();
|
||||
const templates = [
|
||||
{
|
||||
label: 'Select All',
|
||||
accelerator: 'CmdOrCtrl+A',
|
||||
role: 'selectall',
|
||||
click: () => {
|
||||
codeMirror.execCommand('selectAll');
|
||||
},
|
||||
enabled: value.length > 0,
|
||||
},
|
||||
{
|
||||
label: 'Cut',
|
||||
accelerator: 'CmdOrCtrl+X',
|
||||
role: 'cut',
|
||||
enabled: selection.length > 0,
|
||||
},
|
||||
];
|
||||
openContextMenu(event, templates, true, selection);
|
||||
}
|
||||
|
||||
export function openCopyLinkMenu(text, event) {
|
||||
const templates = [
|
||||
{
|
||||
|
@ -70,5 +95,5 @@ export function openCopyLinkMenu(text, event) {
|
|||
},
|
||||
},
|
||||
];
|
||||
openContextMenu(event, templates, false);
|
||||
openContextMenu(event, templates);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ export const validateSendTx = (formValues: DraftTxValues) => {
|
|||
|
||||
// All we need to check is if the address is valid
|
||||
// If values are missing, users wont' be able to submit the form
|
||||
if (address && !regexAddress.test(address)) {
|
||||
if (!process.env.NO_ADDRESS_VALIDATION && !regexAddress.test(address)) {
|
||||
errors.address = __('Not a valid LBRY address');
|
||||
}
|
||||
|
||||
|
|
32
src/renderer/util/getMediaType.js
Normal file
32
src/renderer/util/getMediaType.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import mime from 'mime';
|
||||
|
||||
const formats = [
|
||||
[/\.(mp4|m4v|webm|flv|f4v|ogv)$/i, 'video'],
|
||||
[/\.(mp3|m4a|aac|wav|flac|ogg|opus)$/i, 'audio'],
|
||||
[/\.(html|htm|xml|pdf|odf|doc|docx|md|markdown|txt|epub|org)$/i, 'document'],
|
||||
[/\.(stl|obj|fbx|gcode)$/i, '3D-file'],
|
||||
];
|
||||
|
||||
export default function getMediaType(contentType, fileName) {
|
||||
const extName = mime.getExtension(contentType);
|
||||
const fileExt = extName ? `.${extName}` : null;
|
||||
const testString = fileName || fileExt;
|
||||
|
||||
// Get mediaType from file extension
|
||||
if (testString) {
|
||||
const res = formats.reduce((ret, testpair) => {
|
||||
const [regex, mediaType] = testpair;
|
||||
|
||||
return regex.test(ret) ? mediaType : ret;
|
||||
}, testString);
|
||||
|
||||
if (res !== testString) return res;
|
||||
}
|
||||
|
||||
// Get mediaType from contentType
|
||||
if (contentType) {
|
||||
return /^[^/]+/.exec(contentType)[0];
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
16
yarn.lock
16
yarn.lock
|
@ -5564,9 +5564,9 @@ lazy-val@^1.0.3:
|
|||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#201d78b68a329065ee5d2a03bfb1607ea0666588:
|
||||
lbry-redux@lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/201d78b68a329065ee5d2a03bfb1607ea0666588"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/a0d2d1ac532ade639d39c92f79678ac26e904dfd"
|
||||
dependencies:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
@ -7626,9 +7626,9 @@ react-redux@^5.0.3:
|
|||
loose-envify "^1.1.0"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-simplemde-editor@^3.6.15:
|
||||
version "3.6.15"
|
||||
resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-3.6.15.tgz#b4991304c7e1cac79258bb225579d008c13b5991"
|
||||
react-simplemde-editor@^3.6.16:
|
||||
version "3.6.16"
|
||||
resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-3.6.16.tgz#33633259478d3395f2c7b70deb56a1a40e863bea"
|
||||
dependencies:
|
||||
simplemde "^1.11.2"
|
||||
|
||||
|
@ -8726,6 +8726,12 @@ stream-to-blob-url@^2.0.0:
|
|||
dependencies:
|
||||
stream-to-blob "^1.0.0"
|
||||
|
||||
stream-to-blob-url@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/stream-to-blob-url/-/stream-to-blob-url-2.1.1.tgz#e1ac97f86ca8e9f512329a48e7830ce9a50beef2"
|
||||
dependencies:
|
||||
stream-to-blob "^1.0.0"
|
||||
|
||||
stream-to-blob@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-1.0.0.tgz#9f7a1ada39e16ea282ebb7e4cda307edabde658d"
|
||||
|
|
Loading…
Add table
Reference in a new issue