Support drag-and-drop file publishing #4170
|
@ -16,6 +16,7 @@ import usePrevious from 'effects/use-previous';
|
|||
import Nag from 'component/common/nag';
|
||||
import { rewards as REWARDS } from 'lbryinc';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import FileDrop from 'component/fileDrop';
|
||||
// @if TARGET='web'
|
||||
import OpenInAppLink from 'lbrytv/component/openInAppLink';
|
||||
import YoutubeWelcome from 'lbrytv/component/youtubeReferralWelcome';
|
||||
|
@ -284,6 +285,7 @@ function App(props: Props) {
|
|||
<React.Fragment>
|
||||
<Router />
|
||||
<ModalRouter />
|
||||
<FileDrop />
|
||||
<FileRenderFloating />
|
||||
{isEnhancedLayout && <Yrbl className="yrbl--enhanced" />}
|
||||
|
||||
|
|
32
ui/component/fileDrop/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectBalance,
|
||||
selectIsStillEditing,
|
||||
makeSelectPublishFormValue,
|
||||
doUpdatePublishForm,
|
||||
doToast,
|
||||
doClearPublish,
|
||||
} from 'lbry-redux';
|
||||
import { selectFfmpegStatus } from 'redux/selectors/settings';
|
||||
import FileDrop from './view';
|
||||
|
||||
const select = state => ({
|
||||
name: makeSelectPublishFormValue('name')(state),
|
||||
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||
optimize: makeSelectPublishFormValue('optimize')(state),
|
||||
isStillEditing: selectIsStillEditing(state),
|
||||
balance: selectBalance(state),
|
||||
publishing: makeSelectPublishFormValue('publishing')(state),
|
||||
ffmpegStatus: selectFfmpegStatus(state),
|
||||
size: makeSelectPublishFormValue('fileSize')(state),
|
||||
duration: makeSelectPublishFormValue('fileDur')(state),
|
||||
isVid: makeSelectPublishFormValue('fileVid')(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
clearPublish: () => dispatch(doClearPublish()),
|
||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||
showToast: message => dispatch(doToast({ message, isError: true })),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileDrop);
|
45
ui/component/fileDrop/view.jsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
![]() ok, done. ok, done.
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
// @flow
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
import React from 'react';
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
import useDragDrop from 'effects/use-drag-drop';
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
import classnames from 'classnames';
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
import { getTree } from 'util/web-file-system';
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
type Props = {
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
filePath: string | WebFile,
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
clearPublish: () => void,
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
updatePublishForm: ({}) => void,
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
};
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
function FileDrop(props: Props) {
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
const { drag, dropData } = useDragDrop();
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
const [files, setFiles] = React.useState([]);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
const [error, setError] = React.useState(false);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
const showDropArea = drag || (files && files.length > 0 && loading && !error);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
React.useEffect(() => {
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
// Handle drop...
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
if (dropData && !loading) {
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
setLoading(true);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
getTree(dropData)
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
.then(entries => {
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
setLoading(false);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
setFiles(entries);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
})
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
.catch(error => {
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
// Invalid entry / entries
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
setError(true);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
setLoading(false);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
});
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
}
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
}, [dropData, loading]);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
return (
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
<div className={classnames('file-drop', showDropArea && 'file-drop--show')}>
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
<p>Drop your files</p>
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
</div>
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
);
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
}
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
||||
export default FileDrop;
|
||||
![]() Please wrap this in Please wrap this in `__()`
![]() ok, done. ok, done.
|
|
@ -25,7 +25,7 @@ const DRAG_STATE = {
|
|||
// Returns simple detection for global drag-drop
|
||||
export default function useFetched() {
|
||||
const [drag, setDrag] = React.useState(false);
|
||||
const [drop, setDrop] = React.useState(null);
|
||||
const [dropData, setDropData] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
let dragCount = 0;
|
||||
|
@ -39,9 +39,9 @@ export default function useFetched() {
|
|||
event.preventDefault();
|
||||
// Get files
|
||||
const files = event.dataTransfer.files;
|
||||
// Store files in state
|
||||
if (files.length && files.length > 0) {
|
||||
setDrop(files);
|
||||
// Check for files
|
||||
if (files.length > 0) {
|
||||
setDropData(event.dataTransfer);
|
||||
}
|
||||
}
|
||||
// Reset state ( hide drop zone )
|
||||
|
@ -93,5 +93,5 @@ export default function useFetched() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
return { drag, drop };
|
||||
return { drag, dropData };
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
@import 'component/embed-player';
|
||||
@import 'component/expandable';
|
||||
@import 'component/expanding-details';
|
||||
@import 'component/file-drop';
|
||||
@import 'component/file-properties';
|
||||
@import 'component/file-render';
|
||||
@import 'component/footer';
|
||||
|
|
18
ui/scss/component/_file-drop.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
.file-drop {
|
||||
height: 0;
|
||||
width: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
background: var(--color-background-overlay);
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.file-drop--show {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
96
ui/util/web-file-system.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
// Some functions to work with the new html5 file system API:
|
||||
// - Used for the fileDrop component
|
||||
|
||||
// Wrapper for webkitGetAsEntry
|
||||
// Note: webkitGetAsEntry might be renamed to GetAsEntry
|
||||
const getAsEntry = item => {
|
||||
if (item.kind === 'file' && item.webkitGetAsEntry) {
|
||||
return item.webkitGetAsEntry();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Read entries from directory
|
||||
const readDirectory = directory => {
|
||||
let dirReader = directory.createReader();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(
|
||||
results => {
|
||||
if (results.length) {
|
||||
resolve(results);
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
},
|
||||
error => reject(error)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Some files hide more that one dataTransferItem:
|
||||
// This is a safe way to get the absolute path on electron
|
||||
const getFilePath = (name, files) => {
|
||||
let filePath = null;
|
||||
for (let file of files) {
|
||||
if (file.name === name) {
|
||||
filePath = file.path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return filePath;
|
||||
};
|
||||
|
||||
// Get only files from the dataTransfer items list
|
||||
export const getFiles = dataTransfer => {
|
||||
let entries = [];
|
||||
const { items, files } = dataTransfer;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const entry = getAsEntry(items[i]);
|
||||
if (entry !== null && entry.isFile) {
|
||||
// Has valid path
|
||||
const filePath = getFilePath(entry.name, files);
|
||||
if (filePath) {
|
||||
entries.push({ entry, filePath });
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
};
|
||||
|
||||
// Generate a valid file tree from dataTransfer:
|
||||
// - Ignores directory entries
|
||||
// - Ignores recursive search
|
||||
export const getTree = async dataTransfer => {
|
||||
let tree = [];
|
||||
if (dataTransfer) {
|
||||
const { items, files } = dataTransfer;
|
||||
// Handle single item drop
|
||||
if (items.length === 1) {
|
||||
const { path } = files[0];
|
||||
const entry = getAsEntry(items[0]);
|
||||
// Handle entry
|
||||
if (entry) {
|
||||
const root = { entry, path };
|
||||
// Handle directory
|
||||
if (root.entry.isDirectory) {
|
||||
const directoryEntries = await readDirectory(root.entry);
|
||||
directoryEntries.forEach(item => {
|
||||
if (item.isFile) {
|
||||
tree.push({ entry: item, path: `root.path/${item.name}` });
|
||||
}
|
||||
});
|
||||
}
|
||||
// Hanlde file
|
||||
if (root.entry.isFile) {
|
||||
tree.push(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle multiple items drop
|
||||
if (items.length > 1) {
|
||||
tree = tree.concat(getFiles(dataTransfer));
|
||||
}
|
||||
}
|
||||
return tree;
|
||||
};
|
Please wrap this in
__()