This commit is contained in:
Sean Yesmunt 2019-08-05 23:48:41 -04:00
parent ba2ccd45fe
commit 90bcde49e7
8 changed files with 16 additions and 507 deletions

View file

@ -34,7 +34,6 @@ function FileDownloadLink(props: Props) {
return ( return (
<ToolTip label={__('Open file')}> <ToolTip label={__('Open file')}>
<Button <Button
title="Remove from library"
button="link" button="link"
icon={ICONS.EXTERNAL} icon={ICONS.EXTERNAL}
onClick={() => { onClick={() => {

View file

@ -8,14 +8,6 @@ import AppViewer from 'component/viewers/appViewer';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
// This is half complete, the video viewer works fine for audio, it just doesn't look pretty
// const AudioViewer = React.lazy<*>(() =>
// import(
// /* webpackChunkName: "audioViewer" */
// 'component/viewers/audioViewer'
// )
// );
const DocumentViewer = React.lazy<*>(() => const DocumentViewer = React.lazy<*>(() =>
import( import(
/* webpackChunkName: "documentViewer" */ /* webpackChunkName: "documentViewer" */
@ -131,7 +123,7 @@ class FileRender extends React.PureComponent<Props> {
}; };
// Check for a valid fileType or mediaType // Check for a valid fileType or mediaType
let viewer = fileType ? fileTypes[fileType] : mediaTypes[mediaType]; let viewer = (fileType && fileTypes[fileType]) || mediaTypes[mediaType];
// Check for Human-readable files // Check for Human-readable files
if (!viewer && readableFiles.includes(mediaType)) { if (!viewer && readableFiles.includes(mediaType)) {

View file

@ -34,8 +34,6 @@ export default function FileViewer(props: Props) {
thumbnail, thumbnail,
streamingUrl, streamingUrl,
isStreamable, isStreamable,
// Add this back for full-screen support
// viewerContainer,
} = props; } = props;
const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1; const isPlayable = ['audio', 'video'].indexOf(mediaType) !== -1;

View file

@ -1,300 +0,0 @@
import React from 'react';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
import Tooltip from 'component/common/tooltip';
import { stopContextMenu } from 'util/context-menu';
import butterchurn from 'butterchurn';
import detectButterchurnSupport from 'butterchurn/lib/isSupported.min';
import butterchurnPresets from 'butterchurn-presets';
import jsmediatags from 'jsmediatags/dist/jsmediatags';
import WaveSurfer from 'wavesurfer.js';
import styles from './audioViewer.module.scss';
const isButterchurnSupported = detectButterchurnSupport();
const EQ_BANDS_SIMPLE = [55, 150, 250, 400, 500, 1000, 2000, 4000, 8000, 16000];
/*
const EQ_LOWSHELF = EQ_BANDS_SIMPLE.shift();
const EQ_HIGHSHELF = EQ_BANDS_SIMPLE.pop();
const eqFilters = EQ.map(function(band) {
var filter = wavesurfer.backend.ac.createBiquadFilter();
filter.type = 'peaking';
filter.gain.value = 0;
filter.Q.value = 1;
filter.frequency.value = band.f;
return filter;
});
*/
type Props = {
source: {
url: string,
stream: string => void,
downloadCompleted: string,
downloadPath: string,
status: string,
},
contentType: string,
poster?: string,
claim: StreamClaim,
};
const presets = [
require('butterchurn-presets/presets/converted/Flexi - when monopolies were the future [simple warp + non-reactive moebius].json'),
require('butterchurn-presets/presets/converted/Rovastar & Loadus - FractalDrop (Active Sparks Mix).json'),
require('butterchurn-presets/presets/converted/shifter - tumbling cubes (ripples).json'),
require('butterchurn-presets/presets/converted/ORB - Blue Emotion.json'),
require('butterchurn-presets/presets/converted/shifter - urchin mod.json'),
require('butterchurn-presets/presets/converted/Stahlregen & fishbrain + flexi + geiss - The Machine that conquered the Aether.json'),
require('butterchurn-presets/presets/converted/Zylot - Crosshair Dimension (Light of Ages).json'),
];
class AudioVideoViewer extends React.PureComponent {
// audioNode: ?HTMLAudioElement;
// player: ?{ dispose: () => void };
state = {
playing: false,
enableMilkdrop: isButterchurnSupported,
showEqualizer: false,
showSongDetails: true,
enableArt: true,
artLoaded: false,
artist: null,
title: null,
album: null,
};
componentDidMount() {
const me = this;
const { contentType, poster, claim, source } = me.props;
const path = source.downloadCompleted ? source.downloadPath : source.url;
const sources = [
{
src: path,
type: contentType,
},
];
const audioNode = this.audioNode;
audioNode.crossOrigin = 'anonymous';
audioNode.autostart = true;
const canvasHeight = me.canvasNode.offsetHeight;
const canvasWidth = me.canvasNode.offsetWidth;
// Required for canvas, nuance of rendering
me.canvasNode.height = canvasHeight;
me.canvasNode.width = canvasWidth;
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
const audioSource = audioContext.createMediaElementSource(audioNode);
audioSource.connect(audioContext.destination);
if (isButterchurnSupported) {
const visualizer = (me.visualizer = butterchurn.createVisualizer(audioContext, me.canvasNode, {
height: canvasHeight,
width: canvasWidth,
pixelRatio: window.devicePixelRatio || 1,
textureRatio: 1,
}));
visualizer.connectAudio(audioSource);
visualizer.loadPreset(presets[Math.floor(Math.random() * presets.length)], 2.0);
me._frameCycle = () => {
requestAnimationFrame(me._frameCycle);
if (me.state.enableMilkdrop === true) {
visualizer.render();
}
};
me._frameCycle();
}
const wavesurfer = WaveSurfer.create({
barWidth: 3,
container: this.waveNode,
waveColor: '#000',
progressColor: '#fff',
mediaControls: true,
responsive: true,
normalize: true,
backend: 'MediaElement',
minPxPerSec: 100,
height: this.waveNode.offsetHeight,
});
wavesurfer.load(audioNode);
jsmediatags.Config.setDisallowedXhrHeaders(['If-Modified-Since', 'Range']);
jsmediatags.read(path, {
onSuccess: function(result) {
const { album, artist, title, picture } = result.tags;
if (picture) {
const byteArray = new Uint8Array(picture.data);
const blob = new Blob([byteArray], { type: picture.type });
const albumArtUrl = URL.createObjectURL(blob);
me.artNode.src = albumArtUrl;
me.setState({ artLoaded: true });
}
me.setState({
album,
artist,
title,
});
},
onError: function(error) {
console.log(':(', error.type, error.info);
},
});
}
componentWillUnmount() {
if (this.player) {
this.player.dispose();
}
// Kill the render loop
this._frameCycle = () => {};
}
render() {
const me = this;
const { contentType, poster, claim, source } = me.props;
const {
album,
artist,
title,
enableMilkdrop,
showEqualizer,
showSongDetails,
enableArt,
artLoaded,
playing,
userActive,
} = this.state;
const renderArt = enableArt && artLoaded;
const path = source.downloadCompleted ? source.downloadPath : source.url;
const playButton = (
<div
onClick={() => {
const audioNode = this.audioNode;
if (audioNode.paused) {
audioNode.play();
} else {
audioNode.pause();
}
}}
className={playing ? styles.playButtonPause : styles.playButtonPlay}
/>
);
return (
<div
className={userActive ? styles.userActive : styles.wrapper}
onMouseEnter={() => me.setState({ userActive: true })}
onMouseLeave={() => me.setState({ userActive: false })}
onContextMenu={stopContextMenu}
>
<div className={enableMilkdrop ? styles.containerWithMilkdrop : styles.container}>
<div style={{ position: 'absolute', top: 0, right: 0 }}>
<Tooltip onComponent body={__('Toggle Visualizer')}>
<Button
icon={enableMilkdrop ? ICONS.VISUALIZER_ON : ICONS.VISUALIZER_OFF}
onClick={() => {
if (!isButterchurnSupported) {
return;
}
// Get new preset
this.visualizer.loadPreset(presets[Math.floor(Math.random() * presets.length)], 2.0);
this.setState({ enableMilkdrop: !enableMilkdrop });
}}
/>
</Tooltip>
<Tooltip onComponent body={__('Toggle Album Art')}>
<Button
icon={enableArt ? ICONS.MUSIC_ART_ON : ICONS.MUSIC_ART_OFF}
onClick={() => this.setState({ enableArt: !enableArt })}
/>
</Tooltip>
<Tooltip onComponent body={__('Toggle Details')}>
<Button
icon={showSongDetails ? ICONS.MUSIC_DETAILS_ON : ICONS.MUSIC_DETAILS_OFF}
onClick={() => this.setState({ showSongDetails: !showSongDetails })}
/>
</Tooltip>
<Tooltip onComponent body={__('Equalizer')}>
<Button icon={ICONS.MUSIC_EQUALIZER} onClick={() => this.setState({ showEqualizer: !showEqualizer })} />
</Tooltip>
</div>
<div ref={node => (this.waveNode = node)} className={styles.wave} />
<div className={styles.infoContainer}>
<div className={renderArt ? styles.infoArtContainer : styles.infoArtContainerHidden}>
<img className={styles.infoArtImage} ref={node => (this.artNode = node)} />
{renderArt && playButton}
</div>
<div
className={
showSongDetails
? renderArt
? styles.songDetailsContainer
: styles.songDetailsContainerNoArt
: styles.songDetailsContainerHidden
}
>
<div className={renderArt ? styles.songDetails : styles.songDetailsNoArt}>
{artist && (
<div className={styles.detailsLineArtist}>
<Button icon={ICONS.MUSIC_ARTIST} className={styles.detailsIconArtist} />
{artist}
</div>
)}
{title && (
<div className={styles.detailsLineSong}>
<Button icon={ICONS.MUSIC_SONG} className={styles.detailsIconSong} />
{title}
</div>
)}
{album && (
<div className={styles.detailsLineAlbum}>
<Button icon={ICONS.MUSIC_ALBUM} className={styles.detailsIconAlbum} />
{album}
</div>
)}
</div>
</div>
</div>
{!renderArt && <div className={styles.playButtonDetachedContainer}>{playButton}</div>}
</div>
<canvas
ref={node => (this.canvasNode = node)}
className={enableMilkdrop ? styles.milkdrop : styles.milkdropDisabled}
/>
<audio
ref={node => (this.audioNode = node)}
src={path}
style={{ position: 'absolute', top: '-100px' }}
onPlay={() => this.setState({ playing: true })}
onPause={() => this.setState({ playing: false })}
/>
</div>
);
}
}
export default AudioVideoViewer;

View file

@ -1,193 +0,0 @@
.wrapper {
composes: 'file-render__viewer' from global;
}
.userActive {
composes: wrapper;
}
.container {
background: #212529;
position: absolute;
height: 100%;
width: 100%;
display: flex;
}
.containerWithMilkdrop {
composes: container;
background: rgba(50, 50, 55, 0.7);
}
.wave {
position: absolute;
bottom: -20%;
height: 40%;
opacity: 0.5;
overflow: hidden;
width: 100%;
}
.infoContainer {
padding: 0 20%;
display: flex;
align-items: center;
justify-content: center;
min-height: 42%;
align-self: center;
width: 100%;
margin-top: -10%;
}
.infoArtContainer {
align-self: flex-start;
width: 40%;
float: left;
position: relative;
background: rgba(0, 0, 0, 0.4);
}
.infoArtContainerHidden {
display: none;
}
.infoArtImage {
display: block;
opacity: 1;
transition: opacity 0.7s;
.userActive & {
opacity: 0.2;
}
}
.songDetailsContainer {
text-align: left;
padding: 3%;
width: 50%;
}
.songDetailsContainerHidden {
display: none;
}
.songDetailsContainerNoArt {
composes: songDetailsContainer;
text-align: center;
}
.songDetails {
width: 150%;
text-shadow: 2px 2px 3px #000;
}
.songDetailsNoArt {
composes: songDetails;
width: 200%;
margin-left: -50%;
}
.detailsIcon {
color: rgba(255, 255, 255, 0.5);
top: -3px;
padding-right: 10px;
width: 30px;
}
.detailsIconArtist {
composes: detailsIcon;
top: -3px;
}
.detailsIconSong {
composes: detailsIcon;
top: -5px;
}
.detailsIconAlbum {
composes: detailsIcon;
}
.detailsLineArtist {
font-size: 26px;
padding-bottom: 5px;
}
.detailsLineSong {
font-size: 34px;
line-height: 36px;
}
.detailsLineAlbum {
font-size: 20px;
padding-top: 8px;
}
.playButton {
position: absolute;
border: 5px solid #fff;
border-radius: 45px;
color: #fff;
font-family: arial;
font-size: 60px;
left: 50%;
line-height: 80px;
margin-left: -45px;
padding-left: 20px;
bottom: 50%;
margin-bottom: -45px;
height: 90px;
width: 90px;
opacity: 0;
transition: opacity 0.7s;
.userActive & {
opacity: 0.6;
}
}
.playButtonPlay {
composes: playButton;
&::after {
display: block;
content: '';
}
}
.playButtonPause {
composes: playButton;
font-size: 50px;
line-height: 75px;
padding-left: 20px;
letter-spacing: -24px;
&::after {
display: block;
content: '▎▎';
}
}
.playButtonDetachedContainer {
bottom: 35%;
position: absolute;
left: 50%;
}
.milkdrop {
top: 0;
z-index: 100;
height: 100%;
width: 100%;
display: block;
}
.milkdropDisabled {
display: none;
}

View file

@ -39,7 +39,7 @@ export default history =>
content: contentReducer, content: contentReducer,
costInfo: costInfoReducer, costInfo: costInfoReducer,
fileInfo: fileInfoReducer, fileInfo: fileInfoReducer,
file: fileReducer, // Why is this not in `fileInfoReducer`? file: fileReducer,
homepage: homepageReducer, homepage: homepageReducer,
notifications: notificationsReducer, notifications: notificationsReducer,
publish: publishReducer, publish: publishReducer,

View file

@ -193,12 +193,15 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
getState: GetState getState: GetState
) => { ) => {
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage> // no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
const state = getState(); const state = getState();
const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state); const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
const savedSubscription = state.subscriptions.subscriptions.find(sub => sub.uri === subscriptionUri); const savedSubscription = state.subscriptions.subscriptions.find(sub => sub.uri === subscriptionUri);
if (!savedSubscription) { if (!savedSubscription) {
throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`); throw Error(`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`);
} }
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel? // We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
Lbry.claim_search({ Lbry.claim_search({
channel: subscriptionUri, channel: subscriptionUri,
@ -208,34 +211,42 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
page_size: PAGE_SIZE, page_size: PAGE_SIZE,
}).then(claimListByChannel => { }).then(claimListByChannel => {
const { items: claimsInChannel } = claimListByChannel; const { items: claimsInChannel } = claimListByChannel;
// may happen if subscribed to an abandoned channel or an empty channel // may happen if subscribed to an abandoned channel or an empty channel
if (!claimsInChannel || !claimsInChannel.length) { if (!claimsInChannel || !claimsInChannel.length) {
return; return;
} }
// Determine if the latest subscription currently saved is actually the latest subscription // Determine if the latest subscription currently saved is actually the latest subscription
const latestIndex = claimsInChannel.findIndex( const latestIndex = claimsInChannel.findIndex(
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
); );
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed // If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex; const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
// If latest is 0, nothing has changed // If latest is 0, nothing has changed
// Do not download/notify about new content, it would download/notify 10 claims per channel // Do not download/notify about new content, it would download/notify 10 claims per channel
if (latestIndex !== 0 && savedSubscription.latest) { if (latestIndex !== 0 && savedSubscription.latest) {
let downloadCount = 0; let downloadCount = 0;
const newUnread = []; const newUnread = [];
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => { claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true); const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
const shouldDownload = const shouldDownload =
shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee); shouldAutoDownload && Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.fee);
// Add the new content to the list of "un-read" subscriptions // Add the new content to the list of "un-read" subscriptions
if (shouldNotify) { if (shouldNotify) {
newUnread.push(uri); newUnread.push(uri);
} }
if (shouldDownload) { if (shouldDownload) {
downloadCount += 1; downloadCount += 1;
dispatch(doPurchaseUri(uri, { cost: 0 }, true)); dispatch(doPurchaseUri(uri, { cost: 0 }, true));
} }
}); });
dispatch( dispatch(
doUpdateUnreadSubscriptions( doUpdateUnreadSubscriptions(
subscriptionUri, subscriptionUri,
@ -244,6 +255,7 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
) )
); );
} }
// Set the latest piece of content for a channel // Set the latest piece of content for a channel
// This allows the app to know if there has been new content since it was last set // This allows the app to know if there has been new content since it was last set
dispatch( dispatch(
@ -261,6 +273,7 @@ export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: bool
buildURI({ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false) buildURI({ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id }, false)
) )
); );
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED // calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
// means it will delete a non-existant fetchingChannelClaims[uri] // means it will delete a non-existant fetchingChannelClaims[uri]
dispatch({ dispatch({

View file

@ -208,7 +208,7 @@ reducers[ACTIONS.WINDOW_FOCUSED] = state =>
reducers[ACTIONS.VOLUME_CHANGED] = (state, action) => reducers[ACTIONS.VOLUME_CHANGED] = (state, action) =>
Object.assign({}, state, { Object.assign({}, state, {
muted: action.data.volume, volume: action.data.volume,
}); });
reducers[ACTIONS.VOLUME_MUTED] = (state, action) => reducers[ACTIONS.VOLUME_MUTED] = (state, action) =>