From a7939974e62b6b8daa1e42023752c197fc66d954 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Sun, 10 May 2020 17:55:27 -0500 Subject: [PATCH 01/14] create effect for drag-drop detection --- static/app-strings.json | 2 +- ui/effects/use-drag-drop.js | 97 +++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 ui/effects/use-drag-drop.js diff --git a/static/app-strings.json b/static/app-strings.json index 1ced8769f..9d0121e3a 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1218,4 +1218,4 @@ "Check your rewards page to see if you qualify for paid content reimbursement. Only content in this section qualifies.": "Check your rewards page to see if you qualify for paid content reimbursement. Only content in this section qualifies.", "blocked channels": "blocked channels", "%count% %channels%. ": "%count% %channels%. " -} \ No newline at end of file +} diff --git a/ui/effects/use-drag-drop.js b/ui/effects/use-drag-drop.js new file mode 100644 index 000000000..78d524a0b --- /dev/null +++ b/ui/effects/use-drag-drop.js @@ -0,0 +1,97 @@ +import React from 'react'; + +const LISTENER = { + ADD: 'add', + REMOVE: 'remove', +}; + +const DRAG_TYPES = { + END: 'dragend', + START: 'dragstart', + ENTER: 'dragenter', + LEAVE: 'dragleave', +}; + +const DRAG_SCORE = { + [DRAG_TYPES.ENTER]: 1, + [DRAG_TYPES.LEAVE]: -1, +}; + +const DRAG_STATE = { + [DRAG_TYPES.END]: false, + [DRAG_TYPES.START]: true, +}; + +// Returns simple detection for global drag-drop +export default function useFetched() { + const [drag, setDrag] = React.useState(false); + const [drop, setDrop] = React.useState(null); + + React.useEffect(() => { + let dragCount = 0; + let draggingElement = false; + + // Handle file drop + const handleDropEvent = event => { + // Ignore non file types ( html elements / text ) + if (!draggingElement) { + event.stopPropagation(); + event.preventDefault(); + // Get files + const files = event.dataTransfer.files; + // Store files in state + if (files.length && files.length > 0) { + setDrop(files); + } + } + // Reset state ( hide drop zone ) + dragCount = 0; + setDrag(false); + }; + + // Drag event for non files type ( html elements / text ) + const handleDragElementEvent = event => { + draggingElement = DRAG_STATE[event.type]; + }; + + // Drag events + const handleDragEvent = event => { + event.stopPropagation(); + event.preventDefault(); + // Prevent multiple drop areas + dragCount += DRAG_SCORE[event.type]; + // Dragged file enters the drop area + if (dragCount === 1 && !draggingElement && event.type === DRAG_TYPES.ENTER) { + setDrag(true); + } + // Dragged file leaves the drop area + if (dragCount === 0 && event.type === DRAG_TYPES.LEAVE) { + setDrag(false); + } + }; + + // Register / Unregister listeners + const handleEventListeners = event => { + const action = `${event}EventListener`; + // Handle drop event + document[action]('drop', handleDropEvent); + // Handle drag events + document[action](DRAG_TYPES.ENTER, handleDragEvent); + document[action](DRAG_TYPES.LEAVE, handleDragEvent); + // Handle non files drag events + document[action](DRAG_TYPES.END, handleDragElementEvent); + document[action](DRAG_TYPES.START, handleDragElementEvent); + }; + // On component mounted: + // Register event listeners + handleEventListeners(LISTENER.ADD); + + // On component unmounted: + return () => { + // Unregister event listeners + handleEventListeners(LISTENER.REMOVE); + }; + }, []); + + return { drag, drop }; +} -- 2.45.2 From bdbe50ea418b3558d7e25edd2f10c9ce88392a25 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Mon, 11 May 2020 00:56:31 -0500 Subject: [PATCH 02/14] initial drop area component --- ui/component/app/view.jsx | 2 + ui/component/fileDrop/index.js | 32 +++++++++++ ui/component/fileDrop/view.jsx | 45 +++++++++++++++ ui/effects/use-drag-drop.js | 10 ++-- ui/scss/all.scss | 1 + ui/scss/component/_file-drop.scss | 18 ++++++ ui/util/web-file-system.js | 96 +++++++++++++++++++++++++++++++ 7 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 ui/component/fileDrop/index.js create mode 100644 ui/component/fileDrop/view.jsx create mode 100644 ui/scss/component/_file-drop.scss create mode 100644 ui/util/web-file-system.js diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index a1d5fef0c..5c834d2e8 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -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) { + {isEnhancedLayout && } diff --git a/ui/component/fileDrop/index.js b/ui/component/fileDrop/index.js new file mode 100644 index 000000000..eab7a806e --- /dev/null +++ b/ui/component/fileDrop/index.js @@ -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); diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx new file mode 100644 index 000000000..8cde77285 --- /dev/null +++ b/ui/component/fileDrop/view.jsx @@ -0,0 +1,45 @@ +// @flow +import React from 'react'; +import useDragDrop from 'effects/use-drag-drop'; +import classnames from 'classnames'; +import { getTree } from 'util/web-file-system'; + +type Props = { + filePath: string | WebFile, + clearPublish: () => void, + updatePublishForm: ({}) => void, +}; + +function FileDrop(props: Props) { + const { drag, dropData } = useDragDrop(); + const [files, setFiles] = React.useState([]); + const [error, setError] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + const showDropArea = drag || (files && files.length > 0 && loading && !error); + + React.useEffect(() => { + // Handle drop... + if (dropData && !loading) { + setLoading(true); + getTree(dropData) + .then(entries => { + setLoading(false); + setFiles(entries); + }) + .catch(error => { + // Invalid entry / entries + setError(true); + setLoading(false); + }); + } + }, [dropData, loading]); + + return ( +
+

Drop your files

+
+ ); +} + +export default FileDrop; diff --git a/ui/effects/use-drag-drop.js b/ui/effects/use-drag-drop.js index 78d524a0b..110593e3d 100644 --- a/ui/effects/use-drag-drop.js +++ b/ui/effects/use-drag-drop.js @@ -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 }; } diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 06aa1bd8f..78229d2ba 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -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'; diff --git a/ui/scss/component/_file-drop.scss b/ui/scss/component/_file-drop.scss new file mode 100644 index 000000000..9347da4a8 --- /dev/null +++ b/ui/scss/component/_file-drop.scss @@ -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; + } +} diff --git a/ui/util/web-file-system.js b/ui/util/web-file-system.js new file mode 100644 index 000000000..d2418c234 --- /dev/null +++ b/ui/util/web-file-system.js @@ -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; +}; -- 2.45.2 From b1ca99be91112c22d016c08eb9ec7b422e71cf00 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Mon, 11 May 2020 01:34:20 -0500 Subject: [PATCH 03/14] fix file path format --- ui/component/fileDrop/view.jsx | 31 ++++++++++++++++++++----------- ui/util/web-file-system.js | 11 +++++------ 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index 8cde77285..c10292324 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -12,32 +12,41 @@ type Props = { function FileDrop(props: Props) { const { drag, dropData } = useDragDrop(); + const [show, setShow] = React.useState(false); const [files, setFiles] = React.useState([]); const [error, setError] = React.useState(false); - const [loading, setLoading] = React.useState(false); - - const showDropArea = drag || (files && files.length > 0 && loading && !error); React.useEffect(() => { // Handle drop... - if (dropData && !loading) { - setLoading(true); + if (dropData) { getTree(dropData) .then(entries => { - setLoading(false); setFiles(entries); }) .catch(error => { // Invalid entry / entries - setError(true); - setLoading(false); + setError(error || true); }); } - }, [dropData, loading]); + }, [dropData]); + + React.useEffect(() => { + // Files are drag over or already dropped + if (drag || files.length) { + setShow(true); + // No drag over or files dropped + } else if (!drag && !files.length) { + setShow(false); + } + // Handle files + }, [drag, files, error]); return ( -
-

Drop your files

+
+

Drop your files here!

+ {files.map(file => ( +
{file.path}
+ ))}
); } diff --git a/ui/util/web-file-system.js b/ui/util/web-file-system.js index d2418c234..12e87ee96 100644 --- a/ui/util/web-file-system.js +++ b/ui/util/web-file-system.js @@ -1,5 +1,5 @@ // Some functions to work with the new html5 file system API: -// - Used for the fileDrop component +import path from 'path'; // Wrapper for webkitGetAsEntry // Note: webkitGetAsEntry might be renamed to GetAsEntry @@ -66,18 +66,17 @@ export const getTree = async dataTransfer => { if (dataTransfer) { const { items, files } = dataTransfer; // Handle single item drop - if (items.length === 1) { - const { path } = files[0]; + if (files.length === 1) { const entry = getAsEntry(items[0]); // Handle entry if (entry) { - const root = { entry, path }; + const root = { entry, path: files[0].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}` }); + tree.push({ entry: item, path: path.join(root.path, item.name) }); } }); } @@ -88,7 +87,7 @@ export const getTree = async dataTransfer => { } } // Handle multiple items drop - if (items.length > 1) { + if (files.length > 1) { tree = tree.concat(getFiles(dataTransfer)); } } -- 2.45.2 From 5d0faa740ed942130e92a28967d9f07c0295de53 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Mon, 11 May 2020 21:11:54 -0500 Subject: [PATCH 04/14] connect dropped files data to publish form --- ui/component/fileDrop/view.jsx | 60 ++++++++++++++++++++++++++++--- ui/component/publishFile/view.jsx | 35 ++++++++++++++++-- ui/constants/publish_types.js | 4 +++ ui/scss/component/_file-drop.scss | 11 +++--- ui/util/web-file-system.js | 34 +++++------------- 5 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 ui/constants/publish_types.js diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index c10292324..dd7fb986f 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -1,21 +1,48 @@ // @flow import React from 'react'; +import * as PAGES from 'constants/pages'; +import * as PUBLISH_TYPES from 'constants/publish_types'; import useDragDrop from 'effects/use-drag-drop'; import classnames from 'classnames'; import { getTree } from 'util/web-file-system'; +import { withRouter } from 'react-router'; type Props = { - filePath: string | WebFile, + // Lazy fix for flow errors: + // Todo -> add appropiate types + filePath: ?any, clearPublish: () => void, updatePublishForm: ({}) => void, + // React router + history: { + entities: {}[], + goBack: () => void, + goForward: () => void, + index: number, + length: number, + location: { pathname: string }, + push: string => void, + }, }; +const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; + function FileDrop(props: Props) { + const { history, filePath, updatePublishForm } = props; const { drag, dropData } = useDragDrop(); const [show, setShow] = React.useState(false); const [files, setFiles] = React.useState([]); + const [selectedFile, setSelectedFile] = React.useState(''); const [error, setError] = React.useState(false); + const navigateToPublish = React.useCallback(() => { + // Navigate only if location is not publish area: + // - Prevent spam in history + if (history.location.pathname !== PUBLISH_URL) { + history.push(PUBLISH_URL); + } + }, [history]); + React.useEffect(() => { // Handle drop... if (dropData) { @@ -38,17 +65,42 @@ function FileDrop(props: Props) { } else if (!drag && !files.length) { setShow(false); } + + // Filew dropped on drop area + if (!drag && files.length) { + if (files.length === 1) { + // Handle single file publish + files[0].entry.file(webFile => { + setSelectedFile(webFile); + updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile } }); + }); + } + } // Handle files }, [drag, files, error]); + // Wait for publish state update: + React.useEffect(() => { + // Publish form has a file + if (selectedFile && filePath && filePath.webFile !== undefined) { + // Update completed + if (selectedFile.path === filePath.webFile.path) { + // Done! close the drop area: + setFiles([]); + // Go to publish area + navigateToPublish(); + } + } + }, [filePath, selectedFile, navigateToPublish]); + return (

Drop your files here!

- {files.map(file => ( -
{file.path}
+ {files.map(({ entry }) => ( +
{entry.name}
))}
); } -export default FileDrop; +export default withRouter(FileDrop); diff --git a/ui/component/publishFile/view.jsx b/ui/component/publishFile/view.jsx index 2ad301134..9d534bfbf 100644 --- a/ui/component/publishFile/view.jsx +++ b/ui/component/publishFile/view.jsx @@ -1,5 +1,6 @@ // @flow import * as ICONS from 'constants/icons'; +import * as PUBLISH_TYPES from 'constants/publish_types'; import React, { useState, useEffect } from 'react'; import { regexInvalidURI } from 'lbry-redux'; import FileSelector from 'component/common/file-selector'; @@ -11,7 +12,9 @@ import I18nMessage from '../i18nMessage'; type Props = { name: ?string, - filePath: string | WebFile, + // Lazy fix for flow errors: + // Todo -> add types back + filePath: ?any, // string || WebFile isStillEditing: boolean, balance: number, updatePublishForm: ({}) => void, @@ -47,6 +50,7 @@ function PublishFile(props: Props) { const { available } = ffmpegStatus; const [oversized, setOversized] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); const RECOMMENDED_BITRATE = 6000000; const TV_PUBLISH_SIZE_LIMIT: number = 1073741824; const UPLOAD_SIZE_MESSAGE = 'Lbry.tv uploads are limited to 1 GB. Download the app for unrestricted publishing.'; @@ -63,13 +67,36 @@ function PublishFile(props: Props) { updateOptimizeState(0, 0, false); setOversized(false); } + + // Process dropped file + if (filePath && filePath.publish === PUBLISH_TYPES.DROP && filePath.webFile !== undefined) { + setSelectedFile(filePath.webFile); + } + + /* + // Process a post: + // See: https://github.com/lbryio/lbry-desktop/issues/4105 + + if(filePath && filePath.publish === PUBLISH_TYPES.POST) { + console.info("Writing a post...") + } + */ }, [filePath]); + // File selected by user + useEffect(() => { + if (selectedFile !== undefined || selectedFile !== null) { + handleFileChange(selectedFile); + } + }, [selectedFile]); + let currentFile = ''; if (filePath) { + // Desktiop publish if (typeof filePath === 'string') { currentFile = filePath; - } else { + } else if (filePath.webFile === undefined && typeof filePath.name === 'string') { + // Web publish currentFile = filePath.name; } } @@ -176,7 +203,9 @@ function PublishFile(props: Props) { // @endif } - function handleFileChange(file: WebFile) { + // Lazy fix for flow errors: + // Todo -> add types back: ( file: WebFile ) + function handleFileChange(file) { const { showToast } = props; window.URL = window.URL || window.webkitURL; // if electron, we'll set filePath to the path string because SDK is handling publishing. diff --git a/ui/constants/publish_types.js b/ui/constants/publish_types.js new file mode 100644 index 000000000..6037bd0c5 --- /dev/null +++ b/ui/constants/publish_types.js @@ -0,0 +1,4 @@ +// Publish from drag-drop UI +export const DROP = 'drop'; +// Publish a post ( text / markdown ) +export const POST = 'post'; diff --git a/ui/scss/component/_file-drop.scss b/ui/scss/component/_file-drop.scss index 9347da4a8..22b307187 100644 --- a/ui/scss/component/_file-drop.scss +++ b/ui/scss/component/_file-drop.scss @@ -1,18 +1,19 @@ .file-drop { - height: 0; - width: 0; top: 0; left: 0; position: fixed; - z-index: 5; background: var(--color-background-overlay); opacity: 0; + height: 100%; + width: 100%; overflow: hidden; + pointer-events: none; + z-index: 5; transition: opacity 0.3s ease; &.file-drop--show { - height: 100%; - width: 100%; opacity: 1; + pointer-events: all; + transition: opacity 0.3s ease; } } diff --git a/ui/util/web-file-system.js b/ui/util/web-file-system.js index 12e87ee96..fe73c6ac4 100644 --- a/ui/util/web-file-system.js +++ b/ui/util/web-file-system.js @@ -1,5 +1,4 @@ // Some functions to work with the new html5 file system API: -import path from 'path'; // Wrapper for webkitGetAsEntry // Note: webkitGetAsEntry might be renamed to GetAsEntry @@ -28,31 +27,14 @@ const readDirectory = directory => { }); }; -// 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 +// Get file system entries from the dataTransfer items list: export const getFiles = dataTransfer => { let entries = []; - const { items, files } = dataTransfer; + const { items } = 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 }); - } + entries.push({ entry }); } } return entries; @@ -70,18 +52,18 @@ export const getTree = async dataTransfer => { const entry = getAsEntry(items[0]); // Handle entry if (entry) { - const root = { entry, path: files[0].path }; + const root = { entry }; // Handle directory - if (root.entry.isDirectory) { - const directoryEntries = await readDirectory(root.entry); + if (entry.isDirectory) { + const directoryEntries = await readDirectory(entry); directoryEntries.forEach(item => { if (item.isFile) { - tree.push({ entry: item, path: path.join(root.path, item.name) }); + tree.push({ entry: item, rootPath: root.path }); } }); } // Hanlde file - if (root.entry.isFile) { + if (entry.isFile) { tree.push(root); } } -- 2.45.2 From ec40a4f8ab3ec65287dd2c9dcfa6661082c20d20 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Tue, 12 May 2020 23:05:37 -0500 Subject: [PATCH 05/14] add file selector refactor web file system utils update styles --- ui/component/common/icon-custom.jsx | 6 +++ ui/component/fileDrop/view.jsx | 79 ++++++++++++++++++++++++----- ui/constants/icons.js | 1 + ui/scss/component/_file-drop.scss | 57 +++++++++++++++++++++ ui/util/web-file-system.js | 66 +++++++++++------------- 5 files changed, 160 insertions(+), 49 deletions(-) diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index 59fc4cac0..0423b2aff 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -623,4 +623,10 @@ export const icons = { ), + [ICONS.COMPLETED]: buildIcon( + + + + + ), }; diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index dd7fb986f..cadfe068a 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -1,11 +1,14 @@ // @flow import React from 'react'; +import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import * as PUBLISH_TYPES from 'constants/publish_types'; -import useDragDrop from 'effects/use-drag-drop'; +import Icon from 'component/common/icon'; import classnames from 'classnames'; +import useDragDrop from 'effects/use-drag-drop'; import { getTree } from 'util/web-file-system'; import { withRouter } from 'react-router'; +import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; type Props = { // Lazy fix for flow errors: @@ -25,8 +28,45 @@ type Props = { }, }; +type FileListProps = { + files: Array, + onSelected: string => void, +}; + const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; +function FileList(props: FileListProps) { + const { files, onSelected } = props; + const radio = useRadioState(); + + React.useEffect(() => { + if (!radio.currentId) { + radio.first(); + } + + if (radio.state && radio.state !== '') { + onSelected(radio.state); + } + }, [radio, onSelected]); + + return ( + + {files.map((entry, index) => { + const item = radio.stops[index]; + const selected = item && item.id === radio.currentId; + + return ( + + ); + })} + + ); +} + function FileDrop(props: Props) { const { history, filePath, updatePublishForm } = props; const { drag, dropData } = useDragDrop(); @@ -43,12 +83,23 @@ function FileDrop(props: Props) { } }, [history]); + const handleFileSelected = name => { + if (files && files.length) { + const selected = files.find(file => file.name === name); + if (selected && selected.name !== (selectedFile && selectedFile.name)) { + setSelectedFile(selected); + } + } + }; + React.useEffect(() => { // Handle drop... if (dropData) { getTree(dropData) .then(entries => { - setFiles(entries); + if (entries && entries.length) { + setFiles(entries); + } }) .catch(error => { // Invalid entry / entries @@ -70,14 +121,12 @@ function FileDrop(props: Props) { if (!drag && files.length) { if (files.length === 1) { // Handle single file publish - files[0].entry.file(webFile => { - setSelectedFile(webFile); - updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile } }); - }); + setSelectedFile(files[0]); + updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile: files[0] } }); } } // Handle files - }, [drag, files, error]); + }, [drag, files, error, updatePublishForm, setSelectedFile]); // Wait for publish state update: React.useEffect(() => { @@ -91,14 +140,20 @@ function FileDrop(props: Props) { navigateToPublish(); } } - }, [filePath, selectedFile, navigateToPublish]); + }, [filePath, selectedFile, navigateToPublish, setFiles]); + const multipleFiles = files.length > 1; return (
-

Drop your files here!

- {files.map(({ entry }) => ( -
{entry.name}
- ))} +
+ +

{multipleFiles ? `Only one file is allowed choose wisely` : `Drop here to publish!`}

+ {files && files.length > 0 && ( +
+ +
+ )} +
); } diff --git a/ui/constants/icons.js b/ui/constants/icons.js index 8ab697305..71cc20b03 100644 --- a/ui/constants/icons.js +++ b/ui/constants/icons.js @@ -100,3 +100,4 @@ export const SLIDERS = 'Sliders'; export const SCIENCE = 'Science'; export const ANALYTICS = 'BarChart2'; export const PURCHASED = 'Key'; +export const CIRCLE = 'Circle'; diff --git a/ui/scss/component/_file-drop.scss b/ui/scss/component/_file-drop.scss index 22b307187..db87efdf5 100644 --- a/ui/scss/component/_file-drop.scss +++ b/ui/scss/component/_file-drop.scss @@ -11,9 +11,66 @@ z-index: 5; transition: opacity 0.3s ease; + display: flex; + align-items: center; + justify-content: center; + &.file-drop--show { opacity: 1; pointer-events: all; transition: opacity 0.3s ease; } + + .file-drop__area { + display: flex; + align-items: center; + flex-direction: column; + padding: var(--spacing-large); + min-width: 400px; + + .file-drop__list { + max-height: 200px; + overflow: auto; + width: 100%; + + fieldset { + display: flex; + align-items: center; + flex-direction: column; + background: var(--color-menu-background); + + label { + margin: 0; + display: flex; + align-items: center; + padding: var(--spacing-miniscule) var(--spacing-small); + + &.selected { + background: rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 0 3px var(--color-focus); + } + + .icon { + margin-right: var(--spacing-small); + opacity: 0.64; + } + } + + input { + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } + } + } + + .main-icon { + margin: var(--spacing-small); + } + p { + margin-bottom: var(--spacing-large); + } + } } diff --git a/ui/util/web-file-system.js b/ui/util/web-file-system.js index fe73c6ac4..d22b41b57 100644 --- a/ui/util/web-file-system.js +++ b/ui/util/web-file-system.js @@ -6,72 +6,64 @@ const getAsEntry = item => { if (item.kind === 'file' && item.webkitGetAsEntry) { return item.webkitGetAsEntry(); } - return null; }; +// Get file object from fileEntry +const getFile = fileEntry => new Promise((resolve, reject) => fileEntry.file(resolve, reject)); + // 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 browsers don't support this + if (directory.createReader !== undefined) { + let dirReader = directory.createReader(); + return new Promise((resolve, reject) => dirReader.readEntries(resolve, reject)); + } }; // Get file system entries from the dataTransfer items list: -export const getFiles = dataTransfer => { + +const getFiles = (items, directoryEntries) => { let entries = []; - const { items } = dataTransfer; - for (let i = 0; i < items.length; i++) { - const entry = getAsEntry(items[i]); - if (entry !== null && entry.isFile) { - entries.push({ entry }); + + for (let item of items) { + const entry = directoryEntries ? item : getAsEntry(item); + if (entry && entry.isFile) { + const file = getFile(entry); + entries.push(file); } } - return entries; + + return Promise.all(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 (files.length === 1) { - const entry = getAsEntry(items[0]); + const root = getAsEntry(items[0]); // Handle entry - if (entry) { - const root = { entry }; + if (root) { // Handle directory - if (entry.isDirectory) { - const directoryEntries = await readDirectory(entry); - directoryEntries.forEach(item => { - if (item.isFile) { - tree.push({ entry: item, rootPath: root.path }); - } - }); + if (root.isDirectory) { + const directoryEntries = await readDirectory(root); + // Get each file from the list + return getFiles(directoryEntries, true); } // Hanlde file - if (entry.isFile) { - tree.push(root); + if (root.isFile) { + const file = await getFile(root); + return [file]; } } } // Handle multiple items drop if (files.length > 1) { - tree = tree.concat(getFiles(dataTransfer)); + // Convert items to fileEntry and get each file + return getFiles(items); } } - return tree; }; -- 2.45.2 From fa9020340d732feb06317b22bb943170606576de Mon Sep 17 00:00:00 2001 From: btzr-io Date: Thu, 14 May 2020 16:32:35 -0500 Subject: [PATCH 06/14] refactor current file selection on publish area --- ui/component/fileDrop/view.jsx | 13 +++--- ui/component/publishFile/view.jsx | 75 ++++++++++--------------------- ui/constants/publish_types.js | 4 -- 3 files changed, 29 insertions(+), 63 deletions(-) delete mode 100644 ui/constants/publish_types.js diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index cadfe068a..dd90383b7 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -2,7 +2,6 @@ import React from 'react'; import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; -import * as PUBLISH_TYPES from 'constants/publish_types'; import Icon from 'component/common/icon'; import classnames from 'classnames'; import useDragDrop from 'effects/use-drag-drop'; @@ -11,9 +10,7 @@ import { withRouter } from 'react-router'; import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; type Props = { - // Lazy fix for flow errors: - // Todo -> add appropiate types - filePath: ?any, + filePath: string | WebFile, clearPublish: () => void, updatePublishForm: ({}) => void, // React router @@ -122,7 +119,7 @@ function FileDrop(props: Props) { if (files.length === 1) { // Handle single file publish setSelectedFile(files[0]); - updatePublishForm({ filePath: { publish: PUBLISH_TYPES.DROP, webFile: files[0] } }); + updatePublishForm({ filePath: files[0] }); } } // Handle files @@ -130,16 +127,18 @@ function FileDrop(props: Props) { // Wait for publish state update: React.useEffect(() => { + /* // Publish form has a file - if (selectedFile && filePath && filePath.webFile !== undefined) { + if (selectedFile && filePath) { // Update completed - if (selectedFile.path === filePath.webFile.path) { + if (selectedFile.path === filePath.path) { // Done! close the drop area: setFiles([]); // Go to publish area navigateToPublish(); } } + */ }, [filePath, selectedFile, navigateToPublish, setFiles]); const multipleFiles = files.length > 1; diff --git a/ui/component/publishFile/view.jsx b/ui/component/publishFile/view.jsx index 9d534bfbf..29ecc4008 100644 --- a/ui/component/publishFile/view.jsx +++ b/ui/component/publishFile/view.jsx @@ -1,6 +1,5 @@ // @flow import * as ICONS from 'constants/icons'; -import * as PUBLISH_TYPES from 'constants/publish_types'; import React, { useState, useEffect } from 'react'; import { regexInvalidURI } from 'lbry-redux'; import FileSelector from 'component/common/file-selector'; @@ -12,9 +11,7 @@ import I18nMessage from '../i18nMessage'; type Props = { name: ?string, - // Lazy fix for flow errors: - // Todo -> add types back - filePath: ?any, // string || WebFile + filePath: string | WebFile, isStillEditing: boolean, balance: number, updatePublishForm: ({}) => void, @@ -50,7 +47,8 @@ function PublishFile(props: Props) { const { available } = ffmpegStatus; const [oversized, setOversized] = useState(false); - const [selectedFile, setSelectedFile] = useState(null); + const [currentFile, setCurrentFile] = useState(null); + const RECOMMENDED_BITRATE = 6000000; const TV_PUBLISH_SIZE_LIMIT: number = 1073741824; const UPLOAD_SIZE_MESSAGE = 'Lbry.tv uploads are limited to 1 GB. Download the app for unrestricted publishing.'; @@ -64,42 +62,16 @@ function PublishFile(props: Props) { // clear warnings useEffect(() => { if (!filePath || filePath === '') { - updateOptimizeState(0, 0, false); + setCurrentFile(''); setOversized(false); + updateOptimizeState(0, 0, false); + } else if (typeof filePath !== 'string') { + // Update currentFile file + if (filePath.name !== currentFile && filePath.path !== currentFile) { + handleFileChange(filePath); + } } - - // Process dropped file - if (filePath && filePath.publish === PUBLISH_TYPES.DROP && filePath.webFile !== undefined) { - setSelectedFile(filePath.webFile); - } - - /* - // Process a post: - // See: https://github.com/lbryio/lbry-desktop/issues/4105 - - if(filePath && filePath.publish === PUBLISH_TYPES.POST) { - console.info("Writing a post...") - } - */ - }, [filePath]); - - // File selected by user - useEffect(() => { - if (selectedFile !== undefined || selectedFile !== null) { - handleFileChange(selectedFile); - } - }, [selectedFile]); - - let currentFile = ''; - if (filePath) { - // Desktiop publish - if (typeof filePath === 'string') { - currentFile = filePath; - } else if (filePath.webFile === undefined && typeof filePath.name === 'string') { - // Web publish - currentFile = filePath.name; - } - } + }, [filePath, currentFile, handleFileChange, updateOptimizeState]); function updateOptimizeState(duration, size, isvid) { updatePublishForm({ fileDur: duration, fileSize: size, fileVid: isvid }); @@ -204,13 +176,9 @@ function PublishFile(props: Props) { } // Lazy fix for flow errors: - // Todo -> add types back: ( file: WebFile ) - function handleFileChange(file) { + function handleFileChange(file: WebFile) { const { showToast } = props; window.URL = window.URL || window.webkitURL; - // if electron, we'll set filePath to the path string because SDK is handling publishing. - // if web, we set the filePath (dumb name) to the File() object - // File.path will be undefined from web due to browser security, so it will default to the File Object. setOversized(false); // select file, start to select a new one, then cancel @@ -218,7 +186,8 @@ function PublishFile(props: Props) { updatePublishForm({ filePath: '', name: '' }); return; } - // if video, extract duration so we can warn about bitrate + + // if video, extract duration so we can warn about bitrateif (typeof file !== 'string') { const contentType = file.type.split('/'); const isVideo = contentType[0] === 'video'; const isMp4 = contentType[1] === 'mp4'; @@ -241,17 +210,17 @@ function PublishFile(props: Props) { // @if TARGET='web' // we only need to enforce file sizes on 'web' - if (typeof file !== 'string') { - if (file && file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT) { - setOversized(true); - showToast(__(UPLOAD_SIZE_MESSAGE)); - updatePublishForm({ filePath: '', name: '' }); - return; - } + if (file.size && Number(file.size) > TV_PUBLISH_SIZE_LIMIT) { + setOversized(true); + showToast(__(UPLOAD_SIZE_MESSAGE)); + updatePublishForm({ filePath: '', name: '' }); + return; } // @endif const publishFormParams: { filePath: string | WebFile, name?: string, optimize?: boolean } = { + // if electron, we'll set filePath to the path string because SDK is handling publishing. + // File.path will be undefined from web due to browser security, so it will default to the File Object. filePath: file.path || file, }; // Strip off extention and replace invalid characters @@ -261,6 +230,8 @@ function PublishFile(props: Props) { if (!isStillEditing) { publishFormParams.name = parsedFileName; } + // File path is not supported on web for security reasons so we use the name instead. + setCurrentFile(file.path || file.name); updatePublishForm(publishFormParams); } diff --git a/ui/constants/publish_types.js b/ui/constants/publish_types.js deleted file mode 100644 index 6037bd0c5..000000000 --- a/ui/constants/publish_types.js +++ /dev/null @@ -1,4 +0,0 @@ -// Publish from drag-drop UI -export const DROP = 'drop'; -// Publish a post ( text / markdown ) -export const POST = 'post'; -- 2.45.2 From 83da11b91573202231fc3d8d895374cdd66a3695 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Thu, 14 May 2020 23:25:06 -0500 Subject: [PATCH 07/14] more improvements use modals better transiton to publish area --- ui/component/common/file-list.jsx | 62 +++++++++++++ ui/component/fileDrop/index.js | 28 ++---- ui/component/fileDrop/view.jsx | 126 ++++++++------------------- ui/constants/modal_types.js | 1 + ui/modal/modalFileSelection/index.js | 12 +++ ui/modal/modalFileSelection/view.jsx | 77 ++++++++++++++++ ui/modal/modalRouter/view.jsx | 3 + ui/scss/all.scss | 1 + ui/scss/component/_file-drop.scss | 41 +-------- ui/scss/component/_file-list.scss | 37 ++++++++ 10 files changed, 241 insertions(+), 147 deletions(-) create mode 100644 ui/component/common/file-list.jsx create mode 100644 ui/modal/modalFileSelection/index.js create mode 100644 ui/modal/modalFileSelection/view.jsx create mode 100644 ui/scss/component/_file-list.scss diff --git a/ui/component/common/file-list.jsx b/ui/component/common/file-list.jsx new file mode 100644 index 000000000..6736c5688 --- /dev/null +++ b/ui/component/common/file-list.jsx @@ -0,0 +1,62 @@ +// @flow +import React from 'react'; +import * as ICONS from 'constants/icons'; +import Icon from 'component/common/icon'; +import classnames from 'classnames'; +import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; + +type Props = { + files: Array, + onChange: (WebFile | void) => void, +}; + +function FileList(props: Props) { + const { files, onChange } = props; + const radio = useRadioState(); + + const getFile = (value?: string) => { + if (files && files.length) { + return files.find((file: WebFile) => file.name === value); + } + }; + + React.useEffect(() => { + if (radio.stops.length) { + if (!radio.currentId) { + radio.first(); + } else { + const first = radio.stops[0].ref.current; + // First auto-selection + if (first && first.id === radio.currentId && !radio.state) { + radio.setState(first.value); + } + + if (onChange && radio.state && radio.state !== '') { + const file = getFile(radio.state); + onChange(file); + } + } + } + }, [radio, onChange]); + + return ( +
+ + {files.map((entry, index) => { + const item = radio.stops[index]; + const selected = item && item.id === radio.currentId; + + return ( + + ); + })} + +
+ ); +} + +export default FileList; diff --git a/ui/component/fileDrop/index.js b/ui/component/fileDrop/index.js index eab7a806e..97b69d1fa 100644 --- a/ui/component/fileDrop/index.js +++ b/ui/component/fileDrop/index.js @@ -1,32 +1,22 @@ import { connect } from 'react-redux'; -import { - selectBalance, - selectIsStillEditing, - makeSelectPublishFormValue, - doUpdatePublishForm, - doToast, - doClearPublish, -} from 'lbry-redux'; -import { selectFfmpegStatus } from 'redux/selectors/settings'; + +import { doToast, doClearPublish, doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; + +import { selectModal } from 'redux/selectors/app'; +import { doOpenModal } from 'redux/actions/app'; + import FileDrop from './view'; const select = state => ({ - name: makeSelectPublishFormValue('name')(state), + modal: selectModal(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 => ({ + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), + showToast: message => dispatch(doToast({ message, isError: true })), clearPublish: () => dispatch(doClearPublish()), updatePublishForm: value => dispatch(doUpdatePublishForm(value)), - showToast: message => dispatch(doToast({ message, isError: true })), }); export default connect(select, perform)(FileDrop); diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index dd90383b7..b4c012ba3 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -2,17 +2,20 @@ import React from 'react'; import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; -import Icon from 'component/common/icon'; +import * as MODALS from 'constants/modal_types'; import classnames from 'classnames'; import useDragDrop from 'effects/use-drag-drop'; import { getTree } from 'util/web-file-system'; import { withRouter } from 'react-router'; -import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; +import Icon from 'component/common/icon'; +import FileList from 'component/common/file-list'; type Props = { + modal: { id: string, modalProps: {} }, filePath: string | WebFile, clearPublish: () => void, updatePublishForm: ({}) => void, + openModal: (id: string, { files: Array }) => void, // React router history: { entities: {}[], @@ -25,51 +28,14 @@ type Props = { }, }; -type FileListProps = { - files: Array, - onSelected: string => void, -}; - +const HIDE_TIME_OUT = 600; +const NAVIGATE_TIME_OUT = 300; const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; -function FileList(props: FileListProps) { - const { files, onSelected } = props; - const radio = useRadioState(); - - React.useEffect(() => { - if (!radio.currentId) { - radio.first(); - } - - if (radio.state && radio.state !== '') { - onSelected(radio.state); - } - }, [radio, onSelected]); - - return ( - - {files.map((entry, index) => { - const item = radio.stops[index]; - const selected = item && item.id === radio.currentId; - - return ( - - ); - })} - - ); -} - function FileDrop(props: Props) { - const { history, filePath, updatePublishForm } = props; + const { modal, history, openModal, updatePublishForm } = props; const { drag, dropData } = useDragDrop(); - const [show, setShow] = React.useState(false); const [files, setFiles] = React.useState([]); - const [selectedFile, setSelectedFile] = React.useState(''); const [error, setError] = React.useState(false); const navigateToPublish = React.useCallback(() => { @@ -80,18 +46,29 @@ function FileDrop(props: Props) { } }, [history]); - const handleFileSelected = name => { - if (files && files.length) { - const selected = files.find(file => file.name === name); - if (selected && selected.name !== (selectedFile && selectedFile.name)) { - setSelectedFile(selected); - } + // Delay hide and navigation for a smooth transition + const hideDropArea = () => { + setTimeout(() => { + setFiles([]); + setTimeout(() => navigateToPublish(), NAVIGATE_TIME_OUT); + }, HIDE_TIME_OUT); + }; + + const handleFileSelected = selectedFile => { + updatePublishForm({ filePath: selectedFile }); + hideDropArea(); + }; + + // Firt file will be selected by default: + const handleFileChange = (file?: WebFile) => { + if (files && files.length && file) { + handleFileSelected(file); } }; React.useEffect(() => { // Handle drop... - if (dropData) { + if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { getTree(dropData) .then(entries => { if (entries && entries.length) { @@ -99,59 +76,30 @@ function FileDrop(props: Props) { } }) .catch(error => { - // Invalid entry / entries setError(error || true); }); } - }, [dropData]); + }, [dropData, files, modal]); React.useEffect(() => { - // Files are drag over or already dropped - if (drag || files.length) { - setShow(true); - // No drag over or files dropped - } else if (!drag && !files.length) { - setShow(false); - } - - // Filew dropped on drop area + // Files or directory dropped: if (!drag && files.length) { - if (files.length === 1) { - // Handle single file publish - setSelectedFile(files[0]); - updatePublishForm({ filePath: files[0] }); - } - } - // Handle files - }, [drag, files, error, updatePublishForm, setSelectedFile]); - - // Wait for publish state update: - React.useEffect(() => { - /* - // Publish form has a file - if (selectedFile && filePath) { - // Update completed - if (selectedFile.path === filePath.path) { - // Done! close the drop area: + // Handle multiple files selection + if (files.length > 1) { + openModal(MODALS.FILE_SELECTION, { files: files }); setFiles([]); - // Go to publish area - navigateToPublish(); } } - */ - }, [filePath, selectedFile, navigateToPublish, setFiles]); + }, [drag, files, error]); + + const show = files.length === 1 || (drag && (!modal || modal.id !== MODALS.FILE_SELECTION)); - const multipleFiles = files.length > 1; return (
- -

{multipleFiles ? `Only one file is allowed choose wisely` : `Drop here to publish!`}

- {files && files.length > 0 && ( -
- -
- )} + +

{`Drop here to publish!`}

+ {files && files.length === 1 && }
); diff --git a/ui/constants/modal_types.js b/ui/constants/modal_types.js index 98fa706f5..e8e36c3a2 100644 --- a/ui/constants/modal_types.js +++ b/ui/constants/modal_types.js @@ -3,6 +3,7 @@ export const CONFIRM_EXTERNAL_RESOURCE = 'confirm_external_resource'; export const COMMENT_ACKNOWEDGEMENT = 'comment_acknowlegement'; export const INCOMPATIBLE_DAEMON = 'incompatible_daemon'; export const FILE_TIMEOUT = 'file_timeout'; +export const FILE_SELECTION = 'file_selection'; export const DOWNLOADING = 'downloading'; export const AUTO_GENERATE_THUMBNAIL = 'auto_generate_thumbnail'; export const AUTO_UPDATE_DOWNLOADED = 'auto_update_downloaded'; diff --git a/ui/modal/modalFileSelection/index.js b/ui/modal/modalFileSelection/index.js new file mode 100644 index 000000000..6d0629177 --- /dev/null +++ b/ui/modal/modalFileSelection/index.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { doHideModal } from 'redux/actions/app'; +import { doUpdatePublishForm } from 'lbry-redux'; + +import ModaFileSelection from './view'; + +const perform = dispatch => ({ + hideModal: props => dispatch(doHideModal(props)), + updatePublishForm: value => dispatch(doUpdatePublishForm(value)), +}); + +export default connect(null, perform)(ModaFileSelection); diff --git a/ui/modal/modalFileSelection/view.jsx b/ui/modal/modalFileSelection/view.jsx new file mode 100644 index 000000000..39c5c12f3 --- /dev/null +++ b/ui/modal/modalFileSelection/view.jsx @@ -0,0 +1,77 @@ +// @flow +import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; +import React from 'react'; +import { Modal } from 'modal/modal'; +import { withRouter } from 'react-router'; +import Card from 'component/common/card'; +import Button from 'component/button'; +import FileList from 'component/common/file-list'; + +type Props = { + files: Array, + hideModal: () => void, + updatePublishForm: ({}) => void, + history: { + location: { pathname: string }, + push: string => void, + }, +}; + +const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; + +const ModalFileSelection = (props: Props) => { + const { history, files, hideModal, updatePublishForm } = props; + const [selectedFile, setSelectedFile] = React.useState(null); + + const navigateToPublish = React.useCallback(() => { + // Navigate only if location is not publish area: + // - Prevent spam in history + if (history.location.pathname !== PUBLISH_URL) { + history.push(PUBLISH_URL); + } + }, [history]); + + function handleCloseModal() { + hideModal(); + setSelectedFile(null); + } + + function handleSubmit() { + updatePublishForm({ filePath: selectedFile }); + handleCloseModal(); + navigateToPublish(); + } + + const handleFileChange = (file?: WebFile) => { + setSelectedFile(file); + }; + + return ( + + +
+ +
+
+
+
+ } + /> + + ); +}; + +export default withRouter(ModalFileSelection); diff --git a/ui/modal/modalRouter/view.jsx b/ui/modal/modalRouter/view.jsx index 0e79030a0..70202e8ea 100644 --- a/ui/modal/modalRouter/view.jsx +++ b/ui/modal/modalRouter/view.jsx @@ -39,6 +39,7 @@ import ModalRepost from 'modal/modalRepost'; import ModalSignOut from 'modal/modalSignOut'; import ModalLiquidateSupports from 'modal/modalSupportsLiquidate'; import ModalConfirmAge from 'modal/modalConfirmAge'; +import ModalFileSelection from 'modal/modalFileSelection'; type Props = { modal: { id: string, modalProps: {} }, @@ -140,6 +141,8 @@ function ModalRouter(props: Props) { return ; case MODALS.CONFIRM_AGE: return ; + case MODALS.FILE_SELECTION: + return ; default: return null; } diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 78229d2ba..68d2bd5bc 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -19,6 +19,7 @@ @import 'component/expandable'; @import 'component/expanding-details'; @import 'component/file-drop'; +@import 'component/file-list'; @import 'component/file-properties'; @import 'component/file-render'; @import 'component/footer'; diff --git a/ui/scss/component/_file-drop.scss b/ui/scss/component/_file-drop.scss index db87efdf5..a0ca4fc91 100644 --- a/ui/scss/component/_file-drop.scss +++ b/ui/scss/component/_file-drop.scss @@ -23,52 +23,15 @@ .file-drop__area { display: flex; + min-width: 400px; align-items: center; flex-direction: column; padding: var(--spacing-large); - min-width: 400px; - - .file-drop__list { - max-height: 200px; - overflow: auto; - width: 100%; - - fieldset { - display: flex; - align-items: center; - flex-direction: column; - background: var(--color-menu-background); - - label { - margin: 0; - display: flex; - align-items: center; - padding: var(--spacing-miniscule) var(--spacing-small); - - &.selected { - background: rgba(0, 0, 0, 0.2); - box-shadow: inset 0 0 0 3px var(--color-focus); - } - - .icon { - margin-right: var(--spacing-small); - opacity: 0.64; - } - } - - input { - width: 0; - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } - } - } .main-icon { margin: var(--spacing-small); } + p { margin-bottom: var(--spacing-large); } diff --git a/ui/scss/component/_file-list.scss b/ui/scss/component/_file-list.scss new file mode 100644 index 000000000..dcc9cb5e0 --- /dev/null +++ b/ui/scss/component/_file-list.scss @@ -0,0 +1,37 @@ +.file-list { + max-height: 200px; + overflow: auto; + width: 100%; + + fieldset { + display: flex; + align-items: center; + flex-direction: column; + background: var(--color-menu-background); + + label { + margin: 0; + display: flex; + align-items: center; + padding: var(--spacing-miniscule) var(--spacing-small); + + &.selected { + background: rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 0 3px var(--color-focus); + } + + .icon { + margin-right: var(--spacing-small); + opacity: 0.64; + } + } + + input { + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } + } +} -- 2.45.2 From 2260f665c757cbfa99e885e10dbfe1866c7b00da Mon Sep 17 00:00:00 2001 From: btzr-io Date: Fri, 15 May 2020 00:00:25 -0500 Subject: [PATCH 08/14] reduce updates on fileList component fix issue for file selection modal --- ui/component/common/file-list.jsx | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ui/component/common/file-list.jsx b/ui/component/common/file-list.jsx index 6736c5688..31c9f385c 100644 --- a/ui/component/common/file-list.jsx +++ b/ui/component/common/file-list.jsx @@ -28,12 +28,23 @@ function FileList(props: Props) { const first = radio.stops[0].ref.current; // First auto-selection if (first && first.id === radio.currentId && !radio.state) { + const file = getFile(first.value); + // Update state and select new file + onChange(file); radio.setState(first.value); } - if (onChange && radio.state && radio.state !== '') { - const file = getFile(radio.state); - onChange(file); + if (radio.state) { + // Find selected element + const stop = radio.stops.find(item => item.id === radio.currentId); + const element = stop && stop.ref.current; + // Only update state if new item is selected + if (element && element.value !== radio.state) { + const file = getFile(element.value); + // Sselect new file and update state + onChange(file); + radio.setState(element.value); + } } } } -- 2.45.2 From 4aff629eaf18890207884242226faaf463859e00 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Fri, 15 May 2020 01:00:08 -0500 Subject: [PATCH 09/14] update timers and fix styles --- ui/component/fileDrop/view.jsx | 27 +++++++++++++++++++++++---- ui/scss/component/_file-list.scss | 3 ++- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index b4c012ba3..9a32e2f3e 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -29,7 +29,7 @@ type Props = { }; const HIDE_TIME_OUT = 600; -const NAVIGATE_TIME_OUT = 300; +const NAVIGATE_TIME_OUT = 200; const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; function FileDrop(props: Props) { @@ -37,6 +37,8 @@ function FileDrop(props: Props) { const { drag, dropData } = useDragDrop(); const [files, setFiles] = React.useState([]); const [error, setError] = React.useState(false); + const hideTimer = React.useRef(null); + const navigationTimer = React.useRef(null); const navigateToPublish = React.useCallback(() => { // Navigate only if location is not publish area: @@ -48,9 +50,12 @@ function FileDrop(props: Props) { // Delay hide and navigation for a smooth transition const hideDropArea = () => { - setTimeout(() => { + hideTimer.current = setTimeout(() => { setFiles([]); - setTimeout(() => navigateToPublish(), NAVIGATE_TIME_OUT); + // Navigate to publish area + navigationTimer.current = setTimeout(() => { + navigateToPublish(); + }, NAVIGATE_TIME_OUT); }, HIDE_TIME_OUT); }; @@ -66,6 +71,20 @@ function FileDrop(props: Props) { } }; + // Clear timers + React.useEffect(() => { + return () => { + // Clear hide timer + if (hideTimer.current) { + clearTimeout(hideTimer.current); + } + // Clear navigation timer + if (navigationTimer.current) { + clearTimeout(navigationTimer.current); + } + }; + }, []); + React.useEffect(() => { // Handle drop... if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { @@ -90,7 +109,7 @@ function FileDrop(props: Props) { setFiles([]); } } - }, [drag, files, error]); + }, [drag, files, error, openModal]); const show = files.length === 1 || (drag && (!modal || modal.id !== MODALS.FILE_SELECTION)); diff --git a/ui/scss/component/_file-list.scss b/ui/scss/component/_file-list.scss index dcc9cb5e0..b8752b114 100644 --- a/ui/scss/component/_file-list.scss +++ b/ui/scss/component/_file-list.scss @@ -11,12 +11,13 @@ label { margin: 0; + color: inherit; display: flex; align-items: center; padding: var(--spacing-miniscule) var(--spacing-small); &.selected { - background: rgba(0, 0, 0, 0.2); + background-color: rgba(10, 10, 10, 0.1); box-shadow: inset 0 0 0 3px var(--color-focus); } -- 2.45.2 From 944ee668b3eba6dae6550e2fc549121ccbf80e1c Mon Sep 17 00:00:00 2001 From: Baltazar Gomez Date: Fri, 15 May 2020 02:10:50 -0500 Subject: [PATCH 10/14] remove old comment --- ui/component/publishFile/view.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/component/publishFile/view.jsx b/ui/component/publishFile/view.jsx index 29ecc4008..290ff8a19 100644 --- a/ui/component/publishFile/view.jsx +++ b/ui/component/publishFile/view.jsx @@ -175,7 +175,6 @@ function PublishFile(props: Props) { // @endif } - // Lazy fix for flow errors: function handleFileChange(file: WebFile) { const { showToast } = props; window.URL = window.URL || window.webkitURL; -- 2.45.2 From 5e5ce1296b2af1654761972d95e3fe8a548e1d5e Mon Sep 17 00:00:00 2001 From: btzr-io Date: Sun, 17 May 2020 18:11:27 -0500 Subject: [PATCH 11/14] use same styles for radio input --- ui/component/common/file-list.jsx | 29 +++++++++++------------ ui/component/fileDrop/view.jsx | 2 +- ui/scss/component/_file-list.scss | 38 +++---------------------------- 3 files changed, 19 insertions(+), 50 deletions(-) diff --git a/ui/component/common/file-list.jsx b/ui/component/common/file-list.jsx index 31c9f385c..733eca3ad 100644 --- a/ui/component/common/file-list.jsx +++ b/ui/component/common/file-list.jsx @@ -1,8 +1,5 @@ // @flow import React from 'react'; -import * as ICONS from 'constants/icons'; -import Icon from 'component/common/icon'; -import classnames from 'classnames'; import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; type Props = { @@ -10,6 +7,19 @@ type Props = { onChange: (WebFile | void) => void, }; +type RadioProps = { + id: string, + label: string, +}; + +// Same as FormField type="radio" but it works with reakit: +const ForwardedRadio = React.forwardRef((props: RadioProps, ref) => ( + + + + +)); + function FileList(props: Props) { const { files, onChange } = props; const radio = useRadioState(); @@ -53,17 +63,8 @@ function FileList(props: Props) { return (
- {files.map((entry, index) => { - const item = radio.stops[index]; - const selected = item && item.id === radio.currentId; - - return ( - - ); + {files.map(({ name }) => { + return ; })}
diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index 9a32e2f3e..0ca9f0a50 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -117,7 +117,7 @@ function FileDrop(props: Props) {
-

{`Drop here to publish!`}

+

{__(`Drop here to publish!`)}

{files && files.length === 1 && }
diff --git a/ui/scss/component/_file-list.scss b/ui/scss/component/_file-list.scss index b8752b114..ed8647b65 100644 --- a/ui/scss/component/_file-list.scss +++ b/ui/scss/component/_file-list.scss @@ -1,38 +1,6 @@ .file-list { - max-height: 200px; - overflow: auto; width: 100%; - - fieldset { - display: flex; - align-items: center; - flex-direction: column; - background: var(--color-menu-background); - - label { - margin: 0; - color: inherit; - display: flex; - align-items: center; - padding: var(--spacing-miniscule) var(--spacing-small); - - &.selected { - background-color: rgba(10, 10, 10, 0.1); - box-shadow: inset 0 0 0 3px var(--color-focus); - } - - .icon { - margin-right: var(--spacing-small); - opacity: 0.64; - } - } - - input { - width: 0; - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } - } + overflow: auto; + max-height: 220px; + padding: var(--spacing-small); } -- 2.45.2 From 7fe90fc7c50932c11aa5f50df0dbc2b5d42159a8 Mon Sep 17 00:00:00 2001 From: btzr-io Date: Sun, 17 May 2020 22:21:56 -0500 Subject: [PATCH 12/14] improve single file selection transition and styles --- ui/component/fileDrop/index.js | 4 +-- ui/component/fileDrop/view.jsx | 52 +++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 13 deletions(-) diff --git a/ui/component/fileDrop/index.js b/ui/component/fileDrop/index.js index 97b69d1fa..9a75644b8 100644 --- a/ui/component/fileDrop/index.js +++ b/ui/component/fileDrop/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; -import { doToast, doClearPublish, doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; +import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; import { selectModal } from 'redux/selectors/app'; import { doOpenModal } from 'redux/actions/app'; @@ -14,8 +14,6 @@ const select = state => ({ const perform = dispatch => ({ openModal: (modal, props) => dispatch(doOpenModal(modal, props)), - showToast: message => dispatch(doToast({ message, isError: true })), - clearPublish: () => dispatch(doClearPublish()), updatePublishForm: value => dispatch(doUpdatePublishForm(value)), }); diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index 0ca9f0a50..4bd0efa97 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -8,7 +8,6 @@ import useDragDrop from 'effects/use-drag-drop'; import { getTree } from 'util/web-file-system'; import { withRouter } from 'react-router'; import Icon from 'component/common/icon'; -import FileList from 'component/common/file-list'; type Props = { modal: { id: string, modalProps: {} }, @@ -29,7 +28,8 @@ type Props = { }; const HIDE_TIME_OUT = 600; -const NAVIGATE_TIME_OUT = 200; +const CLEAR_TIME_OUT = 300; +const NAVIGATE_TIME_OUT = 400; const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; function FileDrop(props: Props) { @@ -37,7 +37,9 @@ function FileDrop(props: Props) { const { drag, dropData } = useDragDrop(); const [files, setFiles] = React.useState([]); const [error, setError] = React.useState(false); + const [target, updateTarget] = React.useState(false); const hideTimer = React.useRef(null); + const clearDataTimer = React.useRef(null); const navigationTimer = React.useRef(null); const navigateToPublish = React.useCallback(() => { @@ -64,11 +66,24 @@ function FileDrop(props: Props) { hideDropArea(); }; - // Firt file will be selected by default: - const handleFileChange = (file?: WebFile) => { - if (files && files.length && file) { - handleFileSelected(file); + const getFileIcon = type => { + // Not all files have a type + if (!type) { + return ICONS.FILE; } + // Detect common types + const contentType = type.split('/')[0]; + if (contentType === 'text') { + return ICONS.TEXT; + } else if (contentType === 'image') { + return ICONS.IMAGE; + } else if (contentType === 'video') { + return ICONS.VIDEO; + } else if (contentType === 'audio') { + return ICONS.AUDIO; + } + // Binary file + return ICONS.FILE; }; // Clear timers @@ -82,9 +97,20 @@ function FileDrop(props: Props) { if (navigationTimer.current) { clearTimeout(navigationTimer.current); } + // Clear clearData timer + if (navigationTimer.current) { + clearTimeout(clearDataTimer.current); + } }; }, []); + React.useEffect(() => { + // Clear selected file after modal closed + if ((target && !files) || !files.length) { + clearDataTimer.current = setTimeout(() => updateTarget(null), CLEAR_TIME_OUT); + } + }, [files, target]); + React.useEffect(() => { // Handle drop... if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { @@ -107,18 +133,24 @@ function FileDrop(props: Props) { if (files.length > 1) { openModal(MODALS.FILE_SELECTION, { files: files }); setFiles([]); + } else if (files.length === 1) { + // Handle single file selection + updateTarget(files[0]); + handleFileSelected(files[0]); } } }, [drag, files, error, openModal]); - const show = files.length === 1 || (drag && (!modal || modal.id !== MODALS.FILE_SELECTION)); + // Show icon based on file type + const icon = target ? getFileIcon(target.type) : ICONS.PUBLISH; + // Show drop area when files are dragged over or processing dropped file + const show = files.length === 1 || (!target && drag && (!modal || modal.id !== MODALS.FILE_SELECTION)); return (
- -

{__(`Drop here to publish!`)}

- {files && files.length === 1 && } + +

{target ? target.name : __(`Drop here to publish!`)}

); -- 2.45.2 From ce606c0511f7b6f60840b713da9032940c2fe8f5 Mon Sep 17 00:00:00 2001 From: Baltazar Gomez Date: Mon, 18 May 2020 15:33:42 -0500 Subject: [PATCH 13/14] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 840a4f839..a92dff733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Expose reflector status for publishes ([#4148](https://github.com/lbryio/lbry-desktop/pull/4148)) - More tooltip help texts _community pr!_ ([#4185](https://github.com/lbryio/lbry-desktop/pull/4185)) - Add footer on web ([#4159](https://github.com/lbryio/lbry-desktop/pull/4159)) +- Support drag-and-drop file publishing _community pr!_ ([#4170](https://github.com/lbryio/lbry-desktop/pull/4170)) ### Changed -- 2.45.2 From ddcdf9586e66cb11a79e16a9fe8a5e9c2bc6fbca Mon Sep 17 00:00:00 2001 From: btzr-io Date: Tue, 19 May 2020 01:20:29 -0500 Subject: [PATCH 14/14] minor patch --- ui/component/fileDrop/view.jsx | 21 ++++++++++++--------- ui/util/web-file-system.js | 8 ++++++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/ui/component/fileDrop/view.jsx b/ui/component/fileDrop/view.jsx index 4bd0efa97..6e031f879 100644 --- a/ui/component/fileDrop/view.jsx +++ b/ui/component/fileDrop/view.jsx @@ -28,7 +28,7 @@ type Props = { }; const HIDE_TIME_OUT = 600; -const CLEAR_TIME_OUT = 300; +const TARGET_TIME_OUT = 300; const NAVIGATE_TIME_OUT = 400; const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; @@ -37,9 +37,9 @@ function FileDrop(props: Props) { const { drag, dropData } = useDragDrop(); const [files, setFiles] = React.useState([]); const [error, setError] = React.useState(false); - const [target, updateTarget] = React.useState(false); + const [target, setTarget] = React.useState(false); const hideTimer = React.useRef(null); - const clearDataTimer = React.useRef(null); + const targetTimer = React.useRef(null); const navigationTimer = React.useRef(null); const navigateToPublish = React.useCallback(() => { @@ -93,21 +93,24 @@ function FileDrop(props: Props) { if (hideTimer.current) { clearTimeout(hideTimer.current); } + // Clear target timer + if (targetTimer.current) { + clearTimeout(targetTimer.current); + } // Clear navigation timer if (navigationTimer.current) { clearTimeout(navigationTimer.current); } - // Clear clearData timer - if (navigationTimer.current) { - clearTimeout(clearDataTimer.current); - } }; }, []); React.useEffect(() => { // Clear selected file after modal closed if ((target && !files) || !files.length) { - clearDataTimer.current = setTimeout(() => updateTarget(null), CLEAR_TIME_OUT); + // Small delay for a better transition + targetTimer.current = setTimeout(() => { + setTarget(null); + }, TARGET_TIME_OUT); } }, [files, target]); @@ -135,7 +138,7 @@ function FileDrop(props: Props) { setFiles([]); } else if (files.length === 1) { // Handle single file selection - updateTarget(files[0]); + setTarget(files[0]); handleFileSelected(files[0]); } } diff --git a/ui/util/web-file-system.js b/ui/util/web-file-system.js index d22b41b57..8855beb7b 100644 --- a/ui/util/web-file-system.js +++ b/ui/util/web-file-system.js @@ -22,7 +22,7 @@ const readDirectory = directory => { // Get file system entries from the dataTransfer items list: -const getFiles = (items, directoryEntries) => { +const getFiles = (items, directoryEntries = false) => { let entries = []; for (let item of items) { @@ -58,11 +58,15 @@ export const getTree = async dataTransfer => { const file = await getFile(root); return [file]; } + } else { + // Some files have hidden dataTransfer items: + // Use the default file object instead + return files; } } // Handle multiple items drop if (files.length > 1) { - // Convert items to fileEntry and get each file + // Convert items to fileEntry and filter files return getFiles(items); } } -- 2.45.2