Merge branch 'master' into master

This commit is contained in:
Thomas Zarebczan 2018-07-12 07:53:05 -07:00 committed by GitHub
commit 455188dff3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 640 additions and 192 deletions

View file

@ -5,6 +5,42 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
## [Unreleased] ## [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 ### Added
* Ability to upload thumbnails through spee.ch while publishing ([#1248](https://github.com/lbryio/lbry-app/pull/1248)) * 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)) * QR code for wallet address to Send and Receive page ([#1582](https://github.com/lbryio/lbry-app/pull/1582))

View file

@ -3,7 +3,7 @@
[![Build Status](https://travis-ci.org/lbryio/lbry-app.svg?branch=master)](https://travis-ci.org/lbryio/lbry-app) [![Build Status](https://travis-ci.org/lbryio/lbry-app.svg?branch=master)](https://travis-ci.org/lbryio/lbry-app)
[![Dependencies](https://david-dm.org/lbryio/lbry-app/status.svg)](https://david-dm.org/lbryio/lbry-app) [![Dependencies](https://david-dm.org/lbryio/lbry-app/status.svg)](https://david-dm.org/lbryio/lbry-app)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/78b627d4f5524792adc48719835e1523)](https://www.codacy.com/app/LBRY/lbry-app?utm_source=github.com&utm_medium=referral&utm_content=lbryio/lbry-app&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/78b627d4f5524792adc48719835e1523)](https://www.codacy.com/app/LBRY/lbry-app?utm_source=github.com&utm_medium=referral&utm_content=lbryio/lbry-app&utm_campaign=Badge_Grade)
[![chat on Discord](https://img.shields.io/discord/362322208485277697.svg?logo=discord)](https://discord.gg/U5aRyN6) [![chat on Discord](https://img.shields.io/discord/362322208485277697.svg?logo=discord)](https://chat.lbry.io)
The LBRY app is a graphical browser for the decentralized content marketplace provided by the The LBRY app is a graphical browser for the decentralized content marketplace provided by the
[LBRY](https://lbry.io) protocol. It is essentially 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) | 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 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. 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. **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).* *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/) | 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 ## Usage
Double click the installed application to browse with the LBRY network. 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 #### 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` 2. Change directories into the downloaded folder: `cd lbry-app`
3. Install the dependencies: `yarn` 3. Install the dependencies: `yarn`
4. Run the app: `yarn dev` 4. Run the app: `yarn dev`

View file

@ -1,6 +1,6 @@
{ {
"name": "LBRY", "name": "LBRY",
"version": "0.22.0", "version": "0.22.2",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [ "keywords": [
"lbry" "lbry"
@ -30,8 +30,8 @@
"flow-defs": "flow-typed install", "flow-defs": "flow-typed install",
"release": "yarn compile && electron-builder build", "release": "yarn compile && electron-builder build",
"precommit": "lint-staged", "precommit": "lint-staged",
"postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js", "preinstall": "yarn cache clean lbry-redux",
"clean": "rm -r node_modules && yarn cache clean lbry-redux && yarn" "postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js"
}, },
"dependencies": { "dependencies": {
"bluebird": "^3.5.1", "bluebird": "^3.5.1",
@ -48,8 +48,9 @@
"formik": "^0.10.4", "formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2", "hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1", "keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#201d78b68a329065ee5d2a03bfb1607ea0666588", "lbry-redux": "lbryio/lbry-redux#a0d2d1ac532ade639d39c92f79678ac26e904dfd",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"mime": "^2.3.1",
"mixpanel-browser": "^2.17.1", "mixpanel-browser": "^2.17.1",
"moment": "^2.22.0", "moment": "^2.22.0",
"qrcode.react": "^0.8.0", "qrcode.react": "^0.8.0",
@ -60,7 +61,7 @@
"react-modal": "^3.1.7", "react-modal": "^3.1.7",
"react-paginate": "^5.2.1", "react-paginate": "^5.2.1",
"react-redux": "^5.0.3", "react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.15", "react-simplemde-editor": "^3.6.16",
"react-toggle": "^4.0.2", "react-toggle": "^4.0.2",
"react-transition-group": "1.x", "react-transition-group": "1.x",
"redux": "^3.6.0", "redux": "^3.6.0",
@ -76,6 +77,7 @@
"semver": "^5.3.0", "semver": "^5.3.0",
"shapeshift.io": "^1.3.1", "shapeshift.io": "^1.3.1",
"source-map-support": "^0.5.4", "source-map-support": "^0.5.4",
"stream-to-blob-url": "^2.1.1",
"tree-kill": "^1.1.0", "tree-kill": "^1.1.0",
"y18n": "^4.0.0" "y18n": "^4.0.0"
}, },
@ -127,7 +129,7 @@
"yarn": "^1.3" "yarn": "^1.3"
}, },
"lbrySettings": { "lbrySettings": {
"lbrynetDaemonVersion": "0.20.2", "lbrynetDaemonVersion": "0.20.3",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon", "lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet-daemon" "lbrynetDaemonFileName": "lbrynet-daemon"

View file

@ -14,7 +14,7 @@ export default appState => {
defaultHeight: height, defaultHeight: height,
}); });
let windowConfiguration = { const windowConfiguration = {
backgroundColor: '#44b098', backgroundColor: '#44b098',
minWidth: 950, minWidth: 950,
minHeight: 600, minHeight: 600,
@ -26,17 +26,13 @@ export default appState => {
// If state is undefined, create window as maximized. // If state is undefined, create window as maximized.
width: windowState.width === undefined ? width : windowState.width, width: windowState.width === undefined ? width : windowState.width,
height: windowState.height === undefined ? height : windowState.height, height: windowState.height === undefined ? height : windowState.height,
};
// Disable renderer process's webSecurity on development to enable CORS. webPreferences: {
windowConfiguration = isDev // Disable renderer process's webSecurity on development to enable CORS.
? { webSecurity: !isDev,
...windowConfiguration, plugins: true,
webPreferences: { },
webSecurity: false, };
},
}
: windowConfiguration;
const rendererURL = isDev const rendererURL = isDev
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}` ? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`

View file

@ -11,6 +11,7 @@ import isDev from 'electron-is-dev';
import Daemon from './Daemon'; import Daemon from './Daemon';
import createTray from './createTray'; import createTray from './createTray';
import createWindow from './createWindow'; import createWindow from './createWindow';
import pjson from '../../package.json';
autoUpdater.autoDownload = true; autoUpdater.autoDownload = true;
@ -58,16 +59,18 @@ app.on('ready', async () => {
if (!isDaemonRunning) { if (!isDaemonRunning) {
daemon = new Daemon(); daemon = new Daemon();
daemon.on('exit', () => { daemon.on('exit', () => {
daemon = null; if (!isDev) {
if (!appState.isQuitting) { daemon = null;
dialog.showErrorBox( if (!appState.isQuitting) {
'Daemon has Exited', dialog.showErrorBox(
'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' + 'Daemon has Exited',
'For more information please visit: \n' + 'The daemon may have encountered an unexpected error, or another daemon instance is already running. \n\n' +
'https://lbry.io/faq/startup-troubleshooting' 'For more information please visit: \n' +
); 'https://lbry.io/faq/startup-troubleshooting'
);
}
app.quit();
} }
app.quit();
}); });
daemon.launch(); daemon.launch();
} }
@ -82,7 +85,9 @@ app.on('ready', async () => {
}); });
app.on('activate', () => { app.on('activate', () => {
rendererWindow.show(); if (rendererWindow) {
rendererWindow.show();
}
}); });
app.on('will-quit', event => { app.on('will-quit', event => {
@ -119,6 +124,10 @@ app.on('will-quit', event => {
daemon.quit(); daemon.quit();
event.preventDefault(); event.preventDefault();
} }
if (rendererWindow) {
rendererWindow = null;
}
}); });
// https://electronjs.org/docs/api/app#event-will-finish-launching // 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'); 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 latestReleaseAPIURL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
const opts = { const opts = {
headers: { headers: {

View file

@ -23,6 +23,7 @@ type Props = {
noPadding: ?boolean, // to remove padding and allow circular buttons noPadding: ?boolean, // to remove padding and allow circular buttons
uppercase: ?boolean, uppercase: ?boolean,
iconColor: ?string, iconColor: ?string,
tourniquet: ?boolean, // to shorten the button and ellipsis, only use for links
}; };
class Button extends React.PureComponent<Props> { class Button extends React.PureComponent<Props> {
@ -50,6 +51,7 @@ class Button extends React.PureComponent<Props> {
noPadding, noPadding,
uppercase, uppercase,
iconColor, iconColor,
tourniquet,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -69,6 +71,7 @@ class Button extends React.PureComponent<Props> {
'btn--link': button === 'link', 'btn--link': button === 'link',
'btn--external-link': button === 'link' && href, 'btn--external-link': button === 'link' && href,
'btn--uppercase': uppercase, 'btn--uppercase': uppercase,
'btn--tourniquet': tourniquet,
} }
: 'btn--no-style', : 'btn--no-style',
className className

View file

@ -5,10 +5,20 @@ type Props = {
message: ?string, message: ?string,
}; };
const BusyIndicator = (props: Props) => ( class BusyIndicator extends React.PureComponent<Props> {
<span className="busy-indicator"> static defaultProps = {
{props.message} <span className="busy-indicator__loader" /> message: '',
</span> };
);
render() {
const { message } = this.props;
return (
<span className="busy-indicator">
{message} <span className="busy-indicator__loader" />
</span>
);
}
}
export default BusyIndicator; export default BusyIndicator;

View file

@ -6,6 +6,7 @@ import MarkdownPreview from 'component/common/markdown-preview';
import SimpleMDE from 'react-simplemde-editor'; import SimpleMDE from 'react-simplemde-editor';
import 'simplemde/dist/simplemde.min.css'; import 'simplemde/dist/simplemde.min.css';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import { openEditorMenu } from 'util/contextMenu';
type Props = { type Props = {
name: string, name: string,
@ -54,11 +55,21 @@ export class FormField extends React.PureComponent<Props> {
</select> </select>
); );
} else if (type === 'markdown') { } else if (type === 'markdown') {
const stopContextMenu = event => {
event.preventDefault();
event.stopPropagation();
};
const handleEvents = {
contextmenu(codeMirror, event) {
openEditorMenu(event, codeMirror);
},
};
input = ( input = (
<div className="form-field--SimpleMDE"> <div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<SimpleMDE <SimpleMDE
{...inputProps} {...inputProps}
type="textarea" type="textarea"
events={handleEvents}
options={{ options={{
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'], hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
previewRender(plainText) { previewRender(plainText) {

View file

@ -3,8 +3,8 @@ import React from 'react';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
type Props = { type Props = {
spinner: boolean,
status: string, status: string,
spinner: boolean,
}; };
class LoadingScreen extends React.PureComponent<Props> { class LoadingScreen extends React.PureComponent<Props> {
@ -17,8 +17,7 @@ class LoadingScreen extends React.PureComponent<Props> {
return ( return (
<div className="content__loading"> <div className="content__loading">
{spinner && <Spinner light />} {spinner && <Spinner light />}
{status && <span className="content__loading-text">{status}</span>}
<span className="content__loading-text">{status}</span>
</div> </div>
); );
} }

View file

@ -152,7 +152,12 @@ class FileList extends React.PureComponent<Props, State> {
} }
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => { 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 = {}; const uriParams = {};
// This is unfortunate // This is unfortunate
@ -162,7 +167,8 @@ class FileList extends React.PureComponent<Props, State> {
uriParams.claimId = claimId; uriParams.claimId = claimId;
const uri = buildURI(uriParams); 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 ( return (

View 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);

View 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;

View file

@ -17,7 +17,7 @@ import {
import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings';
import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media'; import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media';
import { selectPlayingUri } from 'redux/selectors/content'; import { selectPlayingUri } from 'redux/selectors/content';
import Video from './view'; import FileViewer from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
@ -45,4 +45,7 @@ const perform = dispatch => ({
savePosition: (claimId, position) => dispatch(savePosition(claimId, position)), savePosition: (claimId, position) => dispatch(savePosition(claimId, position)),
}); });
export default connect(select, perform)(Video); export default connect(
select,
perform
)(FileViewer);

View file

@ -1,6 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import * as icons from 'constants/icons';
type Props = { type Props = {
play: () => void, play: () => void,
@ -14,8 +15,8 @@ class VideoPlayButton extends React.PureComponent<Props> {
const { fileInfo, mediaType, isLoading, play } = this.props; const { fileInfo, mediaType, isLoading, play } = this.props;
const disabled = isLoading || fileInfo === undefined; const disabled = isLoading || fileInfo === undefined;
const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1; const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1;
const icon = doesPlayback ? 'Play' : 'Folder'; const icon = doesPlayback ? icons.PLAY : icons.EYE;
const label = doesPlayback ? 'Play' : 'View'; const label = doesPlayback ? __('Play') : __('View');
return <Button button="primary" disabled={disabled} label={label} icon={icon} onClick={play} />; return <Button button="primary" disabled={disabled} label={label} icon={icon} onClick={play} />;
} }

View file

@ -1,13 +1,17 @@
/* eslint-disable */ /* eslint-disable */
import React from 'react'; import React from 'react';
import { remote } from 'electron'; import { remote } from 'electron';
import Thumbnail from 'component/common/thumbnail';
import player from 'render-media';
import fs from 'fs'; 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 { class VideoPlayer extends React.PureComponent {
static MP3_CONTENT_TYPES = ['audio/mpeg3', 'audio/mpeg']; static MP3_CONTENT_TYPES = ['audio/mpeg3', 'audio/mpeg'];
static FILE_MEDIA_TYPES = ['e-book', 'comic-book', 'document', '3D-file'];
constructor(props) { constructor(props) {
super(props); super(props);
@ -16,6 +20,7 @@ class VideoPlayer extends React.PureComponent {
hasMetadata: false, hasMetadata: false,
startedPlaying: false, startedPlaying: false,
unplayable: false, unplayable: false,
fileSource: null,
}; };
this.togglePlayListener = this.togglePlay.bind(this); this.togglePlayListener = this.togglePlay.bind(this);
@ -29,7 +34,7 @@ class VideoPlayer extends React.PureComponent {
componentDidMount() { componentDidMount() {
const container = this.media; const container = this.media;
const { contentType, changeVolume, volume, position, claim } = this.props; const { downloadCompleted, contentType, changeVolume, volume, position, claim } = this.props;
const loadedMetadata = () => { const loadedMetadata = () => {
this.setState({ hasMetadata: true, startedPlaying: true }); this.setState({ hasMetadata: true, startedPlaying: true });
@ -51,7 +56,13 @@ class VideoPlayer extends React.PureComponent {
// use renderAudio override for mp3 // use renderAudio override for mp3
if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) { if (VideoPlayer.MP3_CONTENT_TYPES.indexOf(contentType) > -1) {
this.renderAudio(container, null, false); 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( player.append(
this.file(), this.file(),
container, container,
@ -89,7 +100,7 @@ class VideoPlayer extends React.PureComponent {
componentDidUpdate() { componentDidUpdate() {
const { contentType, downloadCompleted } = this.props; const { contentType, downloadCompleted } = this.props;
const { startedPlaying } = this.state; const { startedPlaying, fileSource } = this.state;
if (this.playableType() && !startedPlaying && downloadCompleted) { if (this.playableType() && !startedPlaying && downloadCompleted) {
const container = this.media.children[0]; const container = this.media.children[0];
@ -102,6 +113,8 @@ class VideoPlayer extends React.PureComponent {
controls: true, 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; 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) { renderAudio(container, autoplay) {
if (container.firstChild) { if (container.firstChild) {
container.firstChild.remove(); container.firstChild.remove();
@ -173,25 +220,61 @@ class VideoPlayer extends React.PureComponent {
container.appendChild(audio); 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() { render() {
const { mediaType, poster } = this.props; const { mediaType } = this.props;
const { hasMetadata, unplayable } = this.state; const { fileSource } = this.state;
const noMetadataMessage = 'Waiting for metadata.';
const unplayableMessage = "Sorry, looks like we can't play this file."; const isFileType = this.fileType();
const hideMedia = this.playableType() && !hasMetadata && !unplayable; const isFileReady = fileSource && isFileType;
const isPlayableType = this.playableType();
const { isLoading, loadingStatus } = this.showLoadingScreen(isFileType, isPlayableType);
return ( return (
<React.Fragment> <React.Fragment>
{['audio', 'application'].indexOf(mediaType) !== -1 && {loadingStatus && <LoadingScreen status={loadingStatus} spinner={isLoading} />}
(!this.playableType() || hasMetadata) && {isFileReady && <FileRender source={fileSource} mediaType={mediaType} />}
!unplayable && <Thumbnail src={poster} />}
{this.playableType() &&
!hasMetadata &&
!unplayable && <LoadingScreen status={noMetadataMessage} />}
{unplayable && <LoadingScreen status={unplayableMessage} spinner={false} />}
<div <div
className={'content__view--container'} className={'content__view--container'}
style={{ opacity: hideMedia ? 0 : 1 }} style={{ opacity: isLoading ? 0 : 1 }}
ref={container => { ref={container => {
this.media = container; this.media = container;
}} }}

View file

@ -1,11 +1,10 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { Lbry } from 'lbry-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import VideoPlayer from './internal/player'; import LoadingScreen from 'component/common/loading-screen';
import VideoPlayButton from './internal/play-button'; import Player from './internal/player';
import LoadingScreen from './internal/loading-screen'; import PlayButton from './internal/play-button';
const SPACE_BAR_KEYCODE = 32; const SPACE_BAR_KEYCODE = 32;
@ -40,9 +39,10 @@ type Props = {
obscureNsfw: boolean, obscureNsfw: boolean,
play: string => void, play: string => void,
searchBarFocused: boolean, searchBarFocused: boolean,
mediaType: string,
}; };
class Video extends React.PureComponent<Props> { class FileViewer extends React.PureComponent<Props> {
constructor() { constructor() {
super(); super();
@ -123,12 +123,12 @@ class Video extends React.PureComponent<Props> {
mediaPosition, mediaPosition,
className, className,
obscureNsfw, obscureNsfw,
mediaType,
} = this.props; } = this.props;
const isPlaying = playingUri === uri; const isPlaying = playingUri === uri;
const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0; const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0;
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw; const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
const mediaType = Lbry.getMediaType(contentType, fileInfo && fileInfo.file_name);
let loadStatusMessage = ''; let loadStatusMessage = '';
@ -156,7 +156,7 @@ class Video extends React.PureComponent<Props> {
<LoadingScreen status={loadStatusMessage} /> <LoadingScreen status={loadStatusMessage} />
</div> </div>
) : ( ) : (
<VideoPlayer <Player
filename={fileInfo.file_name} filename={fileInfo.file_name}
poster={poster} poster={poster}
downloadPath={fileInfo.download_path} downloadPath={fileInfo.download_path}
@ -183,7 +183,7 @@ class Video extends React.PureComponent<Props> {
className={layoverClass} className={layoverClass}
style={layoverStyle} style={layoverStyle}
> >
<VideoPlayButton <PlayButton
play={e => { play={e => {
e.stopPropagation(); e.stopPropagation();
this.playContent(); this.playContent();
@ -200,4 +200,4 @@ class Video extends React.PureComponent<Props> {
} }
} }
export default Video; export default FileViewer;

View file

@ -25,11 +25,13 @@ class BidHelpText extends React.PureComponent<Props> {
} = this.props; } = this.props;
if (!uri) { if (!uri) {
return __('Create a URL for this content'); return __('Create a URL for this content.');
} }
if (isStillEditing) { 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) { if (isResolvingUri) {
@ -61,10 +63,10 @@ class BidHelpText extends React.PureComponent<Props> {
<React.Fragment> <React.Fragment>
{__('A deposit greater than')} {winningBidForClaimUri} {__('is needed to win')} {__('A deposit greater than')} {winningBidForClaimUri} {__('is needed to win')}
{` ${uri}. `} {` ${uri}. `}
{__('However, you can still get this URL for any amount')} {__('However, you can still get this URL for any amount.')}
</React.Fragment> </React.Fragment>
) : ( ) : (
__('Any amount will give you the winning bid') __('Any amount will give you the winning bid.')
); );
} }
} }

View file

@ -152,14 +152,21 @@ class PublishForm extends React.PureComponent<Props> {
} }
handleBidChange(bid: number) { 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; let bidError;
if (bid === 0) { if (bid === 0) {
bidError = __('Deposit cannot be 0'); bidError = __('Deposit cannot be 0');
} else if (balance === bid) { } else if (totalAvailableBidAmount === bid) {
bidError = __('Please decrease your deposit to account for transaction fees'); 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'); bidError = __('Deposit cannot be higher than your balance');
} else if (bid <= MINIMUM_PUBLISH_BID) { } else if (bid <= MINIMUM_PUBLISH_BID) {
bidError = __('Your deposit must be higher'); bidError = __('Your deposit must be higher');
@ -237,32 +244,54 @@ class PublishForm extends React.PureComponent<Props> {
} }
checkIsFormValid() { checkIsFormValid() {
const { name, nameError, title, bid, bidError, tosAccepted } = this.props; const {
return name && !nameError && title && bid && !bidError && tosAccepted; 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() { 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) { const isFormValid = this.checkIsFormValid();
// 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>
);
}
// These are extra help
// If there is an error it will be presented as an inline error as well
return ( return (
<div className="card__content card__subtitle card__subtitle--block form-field__error"> !isFormValid && (
{!title && <div>{__('A title is required')}</div>} <div className="card__content card__subtitle form-field__error">
{!name && <div>{__('A URL is required')}</div>} {!title && <div>{__('A title is required')}</div>}
{!bid && <div>{__('A bid amount is required')}</div>} {!name && <div>{__('A URL is required')}</div>}
{!tosAccepted && <div>{__('You must agree to the terms of service')}</div>} {name && nameError && <div>{__('The URL you created is not valid')}</div>}
</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 ( return (
<Form onSubmit={this.handlePublish}> <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__title">{__('Content')}</div>
<div className="card__subtitle"> <div className="card__subtitle">
{editingURI ? __('Editing a claim') : __('What are you publishing?')} {isStillEditing ? __('Editing a claim') : __('What are you publishing?')}
</div> </div>
{(filePath || !!editingURI) && ( {(filePath || !!editingURI) && (
<div className="card-media__internal-links"> <div className="card-media__internal-links">
@ -328,7 +357,7 @@ class PublishForm extends React.PureComponent<Props> {
</div> </div>
)} )}
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} /> <FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
{!!editingURI && ( {!!isStillEditing && (
<p className="card__content card__subtitle"> <p className="card__content card__subtitle">
{__("If you don't choose a file, the file from your existing claim")} {__("If you don't choose a file, the file from your existing claim")}
{` "${name}" `} {` "${name}" `}
@ -368,11 +397,11 @@ class PublishForm extends React.PureComponent<Props> {
<div className="card__title">{__('Thumbnail')}</div> <div className="card__title">{__('Thumbnail')}</div>
<div className="card__subtitle"> <div className="card__subtitle">
{uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? ( {uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
__('Enter a url for your thumbnail.') __('Enter a URL for your thumbnail.')
) : ( ) : (
<React.Fragment> <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" />. <Button button="link" label={__('here')} href="https://spee.ch/about" />.
</React.Fragment> </React.Fragment>

View file

@ -35,7 +35,7 @@ class SelectThumbnail extends React.PureComponent<Props> {
stretch stretch
type="text" type="text"
name="content_thumbnail" name="content_thumbnail"
label={__('Url')} label={__('URL')}
placeholder="http://spee.ch/mylogo" placeholder="http://spee.ch/mylogo"
value={thumbnail} value={thumbnail}
disabled={formDisabled} disabled={formDisabled}

View file

@ -56,7 +56,7 @@ class ActiveShapeShift extends React.PureComponent<Props> {
} }
} }
continousFetch: ?number; continousFetch: ?IntervalID;
render() { render() {
const { const {
@ -135,8 +135,8 @@ class ActiveShapeShift extends React.PureComponent<Props> {
{shiftState === statuses.NO_DEPOSITS && {shiftState === statuses.NO_DEPOSITS &&
shiftReturnAddress && ( shiftReturnAddress && (
<div className="help"> <div className="help">
If the transaction doesn't go through, ShapeShift will return your {shiftCoinType}{' '} {__("If the transaction doesn't go through, ShapeShift will return your")}{' '}
back to {shiftReturnAddress} {shiftCoinType} {__('back to')} {shiftReturnAddress}
</div> </div>
)} )}
</div> </div>

View file

@ -72,6 +72,7 @@ class TransactionListItem extends React.PureComponent<Props> {
{name && {name &&
claimId && ( claimId && (
<Button <Button
tourniquet
button="link" button="link"
navigate="/show" navigate="/show"
navigateParams={{ uri: buildURI({ claimName: name, claimId }) }} navigateParams={{ uri: buildURI({ claimName: name, claimId }) }}

View 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;

View file

@ -28,3 +28,5 @@ export const CHECK_SIMPLE = 'Check';
export const GLOBE = 'Globe'; export const GLOBE = 'Globe';
export const EXTERNAL_LINK = 'ExternalLink'; export const EXTERNAL_LINK = 'ExternalLink';
export const GIFT = 'Gift'; export const GIFT = 'Gift';
export const EYE = 'Eye';
export const PLAY = 'Play';

View file

@ -1,3 +1,5 @@
export const NO_DEPOSITS = 'no_deposits'; export const NO_DEPOSITS = 'no_deposits';
export const RECEIVED = 'received'; export const RECEIVED = 'received';
export const COMPLETE = 'complete'; export const COMPLETE = 'complete';
export const AVAILABLE = 'available';
export const UNAVAILABLE = 'unavailable';

View file

@ -48,7 +48,7 @@ class ChannelPage extends React.PureComponent<Props> {
this.props.navigate('/show', newParams); 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 // Change page if enter was pressed, and the given page is between
// the first and the last. // the first and the last.
const pageFromInput = Number(e.target.value); 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 { fetching, claimsInChannel, claim, page, totalPages } = this.props;
const { name, permanent_url: permanentUrl, claim_id: claimId } = claim; const { name, permanent_url: permanentUrl, claim_id: claimId } = claim;
const currentPage = parseInt((page || 1) - 1, 10); const currentPage = parseInt((page || 1) - 1, 10);
let contentList;
if (fetching) { const contentList =
contentList = <BusyIndicator message={__('Fetching content')} />; claimsInChannel && claimsInChannel.length ? (
} else { <FileList sortByHeight hideFilter fileInfos={claimsInChannel} />
contentList = ) : (
claimsInChannel && claimsInChannel.length ? ( !fetching && <span className="empty">{__('No content found.')}</span>
<FileList sortByHeight hideFilter fileInfos={claimsInChannel} /> );
) : (
<span className="empty">{__('No content found.')}</span>
);
}
return ( return (
<Page notContained> <Page notContained>
<section className="card__channel-info card__channel-info--large"> <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"> <div className="card__actions card__actions--no-margin">
<SubscribeButton uri={permanentUrl} channelName={name} /> <SubscribeButton uri={permanentUrl} channelName={name} />
<ViewOnWebButton claimId={claimId} claimName={name} /> <ViewOnWebButton claimId={claimId} claimName={name} />

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { Lbry, buildURI, normalizeURI, MODALS } from 'lbry-redux'; import { buildURI, normalizeURI, MODALS } from 'lbry-redux';
import Video from 'component/video'; import FileViewer from 'component/fileViewer';
import Thumbnail from 'component/common/thumbnail'; import Thumbnail from 'component/common/thumbnail';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import FileDetails from 'component/fileDetails'; import FileDetails from 'component/fileDetails';
@ -14,7 +14,6 @@ import Button from 'component/button';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import ViewOnWebButton from 'component/viewOnWebButton'; import ViewOnWebButton from 'component/viewOnWebButton';
import Page from 'component/page'; import Page from 'component/page';
import player from 'render-media';
import * as settings from 'constants/settings'; import * as settings from 'constants/settings';
import type { Claim } from 'types/claim'; import type { Claim } from 'types/claim';
import type { Subscription } from 'types/subscription'; import type { Subscription } from 'types/subscription';
@ -22,6 +21,7 @@ import FileDownloadLink from 'component/fileDownloadLink';
import classnames from 'classnames'; import classnames from 'classnames';
import { FormField, FormRow } from 'component/common/form'; import { FormField, FormRow } from 'component/common/form';
import ToolTip from 'component/common/tooltip'; import ToolTip from 'component/common/tooltip';
import getMediaType from 'util/getMediaType';
type Props = { type Props = {
claim: Claim, claim: Claim,
@ -29,6 +29,7 @@ type Props = {
metadata: { metadata: {
title: string, title: string,
thumbnail: string, thumbnail: string,
file_name: string,
nsfw: boolean, nsfw: boolean,
}, },
contentType: string, contentType: string,
@ -49,6 +50,18 @@ type Props = {
}; };
class FilePage extends React.Component<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) { constructor(props: Props) {
super(props); super(props);
@ -108,15 +121,19 @@ class FilePage extends React.Component<Props> {
navigate, navigate,
autoplay, autoplay,
costInfo, costInfo,
fileInfo,
} = this.props; } = this.props;
// File info // File info
const { title, thumbnail } = metadata; 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 isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw; const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
const { height, channel_name: channelName, value } = claim; const fileName = fileInfo ? fileInfo.file_name : null;
const mediaType = Lbry.getMediaType(contentType); const mediaType = getMediaType(contentType, fileName);
const isPlayable = Object.values(player.mime).includes(contentType) || mediaType === 'audio'; const showFile =
PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType);
const channelClaimId = const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId; value && value.publisherSignature && value.publisherSignature.certificateId;
let subscriptionUri; let subscriptionUri;
@ -150,8 +167,10 @@ class FilePage extends React.Component<Props> {
</section> </section>
) : ( ) : (
<section className="card"> <section className="card">
{isPlayable && <Video className="content__embedded" uri={uri} />} {showFile && (
{!isPlayable && <FileViewer className="content__embedded" uri={uri} mediaType={mediaType} />
)}
{!showFile &&
(thumbnail ? ( (thumbnail ? (
<Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} /> <Thumbnail shouldObscure={shouldObscureThumbnail} src={thumbnail} />
) : ( ) : (
@ -160,7 +179,9 @@ class FilePage extends React.Component<Props> {
'content__empty--nsfw': shouldObscureThumbnail, '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>
))} ))}
<div className="card__content"> <div className="card__content">
@ -182,34 +203,32 @@ class FilePage extends React.Component<Props> {
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
</div> </div>
<div className="card__actions card__actions--no-margin card__actions--between"> <div className="card__actions card__actions--no-margin card__actions--between">
{(claimIsMine || subscriptionUri || speechSharable) && ( <div className="card__actions">
<div className="card__actions"> {claimIsMine ? (
{claimIsMine ? ( <Button
<Button button="primary"
button="primary" icon={icons.EDIT}
icon={icons.EDIT} label={__('Edit')}
label={__('Edit')} onClick={() => {
onClick={() => { prepareEdit(claim, editUri);
prepareEdit(claim, editUri); navigate('/publish');
navigate('/publish'); }}
}} />
/> ) : (
) : ( <SubscribeButton uri={subscriptionUri} channelName={channelName} />
<SubscribeButton uri={subscriptionUri} channelName={channelName} /> )}
)} {!claimIsMine && (
{!claimIsMine && ( <Button
<Button button="alt"
button="alt" icon={icons.GIFT}
icon={icons.GIFT} label={__('Enjoy this? Send a tip')}
label={__('Enjoy this? Send a tip')} onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })}
onClick={() => openModal({ id: MODALS.SEND_TIP }, { uri })} />
/> )}
)} {speechSharable && (
{speechSharable && ( <ViewOnWebButton claimId={claim.claim_id} claimName={claim.name} />
<ViewOnWebButton claimId={claim.claim_id} claimName={claim.name} /> )}
)} </div>
</div>
)}
<div className="card__actions"> <div className="card__actions">
<FileDownloadLink uri={uri} /> <FileDownloadLink uri={uri} />

View file

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { FileTile } from 'component/fileTile';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import Page from 'component/page'; import Page from 'component/page';

View file

@ -1,5 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { parseURI } from 'lbry-redux';
import BusyIndicator from 'component/common/busy-indicator'; import BusyIndicator from 'component/common/busy-indicator';
import ChannelPage from 'page/channel'; import ChannelPage from 'page/channel';
import FilePage from 'page/file'; import FilePage from 'page/file';
@ -39,10 +40,11 @@ class ShowPage extends React.PureComponent<Props> {
let innerContent = ''; let innerContent = '';
if ((isResolvingUri && !claim) || !claim) { if ((isResolvingUri && !claim) || !claim) {
const { claimName } = parseURI(uri);
innerContent = ( innerContent = (
<Page> <Page>
<section className="card"> <section className="card">
<h1>{uri}</h1> <h1>{claimName}</h1>
<div className="card__content"> <div className="card__content">
{isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />} {isResolvingUri && <BusyIndicator message={__('Loading decentralized data...')} />}
{claim === null && {claim === null &&

View file

@ -253,17 +253,19 @@ export function doCheckUpgradeSubscribe() {
export function doCheckDaemonVersion() { export function doCheckDaemonVersion() {
return dispatch => { return dispatch => {
Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => { Lbry.version().then(({ lbrynet_version: lbrynetVersion }) => {
if (config.lbrynetDaemonVersion === lbrynetVersion) { // Avoid the incompatible daemon modal if running in dev mode
dispatch({ // 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, type: ACTIONS.DAEMON_VERSION_MATCH,
}); });
return;
} }
dispatch({ dispatch({
type: ACTIONS.DAEMON_VERSION_MISMATCH, type: ACTIONS.DAEMON_VERSION_MISMATCH,
}); });
dispatch(
return dispatch(
doNotify({ doNotify({
id: MODALS.INCOMPATIBLE_DAEMON, id: MODALS.INCOMPATIBLE_DAEMON,
}) })

View file

@ -74,6 +74,7 @@ export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction
export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => { export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => {
const thumbnail = fs.readFileSync(filePath); const thumbnail = fs.readFileSync(filePath);
const fileExt = path.extname(filePath); const fileExt = path.extname(filePath);
const fileName = path.basename(filePath);
const makeid = () => { const makeid = () => {
let text = ''; let text = '';
@ -100,9 +101,9 @@ export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch:
const data = new FormData(); const data = new FormData();
const name = makeid(); 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('name', name);
data.append('file', blob); data.append('file', file);
data.append('nsfw', nsfw.toString()); data.append('nsfw', nsfw.toString());
return fetch('https://spee.ch/api/claim/publish', { return fetch('https://spee.ch/api/claim/publish', {
method: 'POST', method: 'POST',

View file

@ -1,5 +1,6 @@
// @flow // @flow
import Promise from 'bluebird'; import Promise from 'bluebird';
import * as SHAPESHIFT_STATUSES from 'constants/shape_shift';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import { coinRegexPatterns } from 'util/shape_shift'; import { coinRegexPatterns } from 'util/shape_shift';
import type { import type {
@ -65,9 +66,15 @@ export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => {
return shapeShift return shapeShift
.coinsAsync() .coinsAsync()
.then(coinData => { .then(coinData => {
if (coinData.LBC.status === SHAPESHIFT_STATUSES.UNAVAILABLE) {
return dispatch({
type: ACTIONS.GET_SUPPORTED_COINS_FAIL,
});
}
let supportedCoins = []; let supportedCoins = [];
Object.keys(coinData).forEach(symbol => { Object.keys(coinData).forEach(symbol => {
if (coinData[symbol].status === 'available') { if (coinData[symbol].status === SHAPESHIFT_STATUSES.UNAVAILABLE) {
supportedCoins.push(coinData[symbol]); supportedCoins.push(coinData[symbol]);
} }
}); });
@ -81,7 +88,7 @@ export const shapeShiftInit = () => (dispatch: Dispatch): ThunkAction => {
type: ACTIONS.GET_SUPPORTED_COINS_SUCCESS, type: ACTIONS.GET_SUPPORTED_COINS_SUCCESS,
data: supportedCoins, data: supportedCoins,
}); });
dispatch(getCoinStats(supportedCoins[0])); return dispatch(getCoinStats(supportedCoins[0]));
}) })
.catch(err => dispatch({ type: ACTIONS.GET_SUPPORTED_COINS_FAIL, data: err })); .catch(err => dispatch({ type: ACTIONS.GET_SUPPORTED_COINS_FAIL, data: err }));
}; };

View file

@ -117,7 +117,7 @@ export default handleActions(
[ACTIONS.GET_SUPPORTED_COINS_FAIL]: (state: ShapeShiftState): ShapeShiftState => ({ [ACTIONS.GET_SUPPORTED_COINS_FAIL]: (state: ShapeShiftState): ShapeShiftState => ({
...state, ...state,
loading: false, loading: false,
error: 'Error getting available coins', error: __('There was an error. Please try again later.'),
}), }),
[ACTIONS.GET_COIN_STATS_START]: ( [ACTIONS.GET_COIN_STATS_START]: (

View file

@ -23,5 +23,6 @@
@import 'component/_spinner.scss'; @import 'component/_spinner.scss';
@import 'component/_nav.scss'; @import 'component/_nav.scss';
@import 'component/_file-list.scss'; @import 'component/_file-list.scss';
@import 'component/_file-render.scss';
@import 'component/_search.scss'; @import 'component/_search.scss';
@import 'component/_toggle.scss'; @import 'component/_toggle.scss';

View file

@ -62,7 +62,7 @@ button:disabled {
font-size: 1em; font-size: 1em;
color: var(--btn-color-inverse); color: var(--btn-color-inverse);
border-radius: 0; border-radius: 0;
display: inline-block; display: inline;
min-width: 0; min-width: 0;
box-shadow: none; box-shadow: none;
text-align: left; text-align: left;
@ -76,6 +76,13 @@ button:disabled {
background-color: var(--btn-bg-secondary); background-color: var(--btn-bg-secondary);
} }
.btn.btn--tourniquet {
max-width: 20vw;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.btn.btn--no-style { .btn.btn--no-style {
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;

View file

@ -150,10 +150,6 @@
padding-top: $spacing-vertical * 1/3; padding-top: $spacing-vertical * 1/3;
} }
.card__subtitle--block {
display: block;
}
.card__meta { .card__meta {
color: var(--color-help); color: var(--color-help);
font-size: 14px; font-size: 14px;

View file

@ -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 { img {
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;

View 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%;
}
}
}

View file

@ -10,6 +10,10 @@
border: solid 1px var(--color-divider); border: solid 1px var(--color-divider);
margin: $spacing-vertical $spacing-vertical * 2/3; margin: $spacing-vertical $spacing-vertical * 2/3;
} }
@media (min-width: $large-breakpoint) {
width: calc(var(--side-nav-width) * 1.1);
}
} }
// Sidebar links // Sidebar links

View file

@ -80,6 +80,14 @@ table.table--help {
font-family: 'metropolis-semibold'; font-family: 'metropolis-semibold';
min-width: 130px; 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 { table.table--transactions {

View file

@ -21,11 +21,11 @@ function injectDevelopmentTemplate(event, templates) {
return templates; return templates;
} }
export function openContextMenu(event, templates = []) { export function openContextMenu(event, templates = [], canEdit = false, selection = '') {
const isSomethingSelected = window.getSelection().toString().length > 0; const { type, value } = event.target;
const { type } = event.target; const isSomethingSelected = selection.length > 0 || window.getSelection().toString().length > 0;
const isInput = event.target.matches('input') && (type === 'text' || type === 'number'); const isInput = event.target.matches('input') && (type === 'text' || type === 'number');
const { value } = event.target; const isTextField = canEdit || isInput || event.target.matches('textarea');
templates.push({ templates.push({
label: 'Copy', 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. // If context menu is opened on Input and there is text on the input and something is selected.
const { selectionStart, selectionEnd } = event.target; const { selectionStart, selectionEnd } = event.target;
if (!!value && isInput && selectionStart !== selectionEnd) { if (!!value && isTextField && selectionStart !== selectionEnd) {
templates.push({ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }); templates.push({ label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' });
} }
// If context menu is opened on Input and text is present on clipboard // 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({ templates.push({
label: 'Paste', label: 'Paste',
accelerator: 'CmdOrCtrl+V', accelerator: 'CmdOrCtrl+V',
@ -50,7 +50,7 @@ export function openContextMenu(event, templates = []) {
} }
// If context menu is opened on Input // If context menu is opened on Input
if (isInput && value) { if (isTextField && value) {
templates.push({ templates.push({
label: 'Select All', label: 'Select All',
accelerator: 'CmdOrCtrl+A', accelerator: 'CmdOrCtrl+A',
@ -61,6 +61,31 @@ export function openContextMenu(event, templates = []) {
injectDevelopmentTemplate(event, templates); injectDevelopmentTemplate(event, templates);
remote.Menu.buildFromTemplate(templates).popup(); 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) { export function openCopyLinkMenu(text, event) {
const templates = [ const templates = [
{ {
@ -70,5 +95,5 @@ export function openCopyLinkMenu(text, event) {
}, },
}, },
]; ];
openContextMenu(event, templates, false); openContextMenu(event, templates);
} }

View file

@ -13,7 +13,7 @@ export const validateSendTx = (formValues: DraftTxValues) => {
// All we need to check is if the address is valid // All we need to check is if the address is valid
// If values are missing, users wont' be able to submit the form // 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'); errors.address = __('Not a valid LBRY address');
} }

View 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';
}

View file

@ -5564,9 +5564,9 @@ lazy-val@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc" 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" 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: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
@ -7626,9 +7626,9 @@ react-redux@^5.0.3:
loose-envify "^1.1.0" loose-envify "^1.1.0"
prop-types "^15.6.0" prop-types "^15.6.0"
react-simplemde-editor@^3.6.15: react-simplemde-editor@^3.6.16:
version "3.6.15" version "3.6.16"
resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-3.6.15.tgz#b4991304c7e1cac79258bb225579d008c13b5991" resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-3.6.16.tgz#33633259478d3395f2c7b70deb56a1a40e863bea"
dependencies: dependencies:
simplemde "^1.11.2" simplemde "^1.11.2"
@ -8726,6 +8726,12 @@ stream-to-blob-url@^2.0.0:
dependencies: dependencies:
stream-to-blob "^1.0.0" 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: stream-to-blob@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-1.0.0.tgz#9f7a1ada39e16ea282ebb7e4cda307edabde658d" resolved "https://registry.yarnpkg.com/stream-to-blob/-/stream-to-blob-1.0.0.tgz#9f7a1ada39e16ea282ebb7e4cda307edabde658d"