-
Notifications
You must be signed in to change notification settings - Fork 413
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support drag-and-drop file publishing (#4170)
- Loading branch information
Showing
18 changed files
with
598 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
// @flow | ||
import React from 'react'; | ||
import { useRadioState, Radio, RadioGroup } from 'reakit/Radio'; | ||
|
||
type Props = { | ||
files: Array<WebFile>, | ||
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) => ( | ||
<span className="radio"> | ||
<input {...props} type="radio" ref={ref} /> | ||
<label htmlFor={props.id}>{props.label}</label> | ||
</span> | ||
)); | ||
|
||
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) { | ||
const file = getFile(first.value); | ||
// Update state and select new file | ||
onChange(file); | ||
radio.setState(first.value); | ||
} | ||
|
||
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); | ||
} | ||
} | ||
} | ||
} | ||
}, [radio, onChange]); | ||
|
||
return ( | ||
<div className={'file-list'}> | ||
<RadioGroup {...radio} aria-label="files"> | ||
{files.map(({ name }) => { | ||
return <Radio key={name} {...radio} value={name} label={name} as={ForwardedRadio} />; | ||
})} | ||
</RadioGroup> | ||
</div> | ||
); | ||
} | ||
|
||
export default FileList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { connect } from 'react-redux'; | ||
|
||
import { doUpdatePublishForm, makeSelectPublishFormValue } from 'lbry-redux'; | ||
|
||
import { selectModal } from 'redux/selectors/app'; | ||
import { doOpenModal } from 'redux/actions/app'; | ||
|
||
import FileDrop from './view'; | ||
|
||
const select = state => ({ | ||
modal: selectModal(state), | ||
filePath: makeSelectPublishFormValue('filePath')(state), | ||
}); | ||
|
||
const perform = dispatch => ({ | ||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), | ||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)), | ||
}); | ||
|
||
export default connect(select, perform)(FileDrop); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// @flow | ||
import React from 'react'; | ||
import * as ICONS from 'constants/icons'; | ||
import * as PAGES from 'constants/pages'; | ||
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 Icon from 'component/common/icon'; | ||
|
||
type Props = { | ||
modal: { id: string, modalProps: {} }, | ||
filePath: string | WebFile, | ||
clearPublish: () => void, | ||
updatePublishForm: ({}) => void, | ||
openModal: (id: string, { files: Array<WebFile> }) => void, | ||
// React router | ||
history: { | ||
entities: {}[], | ||
goBack: () => void, | ||
goForward: () => void, | ||
index: number, | ||
length: number, | ||
location: { pathname: string }, | ||
push: string => void, | ||
}, | ||
}; | ||
|
||
const HIDE_TIME_OUT = 600; | ||
const TARGET_TIME_OUT = 300; | ||
const NAVIGATE_TIME_OUT = 400; | ||
const PUBLISH_URL = `/$/${PAGES.PUBLISH}`; | ||
|
||
function FileDrop(props: Props) { | ||
const { modal, history, openModal, updatePublishForm } = props; | ||
const { drag, dropData } = useDragDrop(); | ||
const [files, setFiles] = React.useState([]); | ||
const [error, setError] = React.useState(false); | ||
const [target, setTarget] = React.useState(false); | ||
const hideTimer = React.useRef(null); | ||
const targetTimer = React.useRef(null); | ||
const navigationTimer = React.useRef(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]); | ||
|
||
// Delay hide and navigation for a smooth transition | ||
const hideDropArea = () => { | ||
hideTimer.current = setTimeout(() => { | ||
setFiles([]); | ||
// Navigate to publish area | ||
navigationTimer.current = setTimeout(() => { | ||
navigateToPublish(); | ||
}, NAVIGATE_TIME_OUT); | ||
}, HIDE_TIME_OUT); | ||
}; | ||
|
||
const handleFileSelected = selectedFile => { | ||
updatePublishForm({ filePath: selectedFile }); | ||
hideDropArea(); | ||
}; | ||
|
||
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 | ||
React.useEffect(() => { | ||
return () => { | ||
// Clear hide timer | ||
if (hideTimer.current) { | ||
clearTimeout(hideTimer.current); | ||
} | ||
// Clear target timer | ||
if (targetTimer.current) { | ||
clearTimeout(targetTimer.current); | ||
} | ||
// Clear navigation timer | ||
if (navigationTimer.current) { | ||
clearTimeout(navigationTimer.current); | ||
} | ||
}; | ||
}, []); | ||
|
||
React.useEffect(() => { | ||
// Clear selected file after modal closed | ||
if ((target && !files) || !files.length) { | ||
// Small delay for a better transition | ||
targetTimer.current = setTimeout(() => { | ||
setTarget(null); | ||
}, TARGET_TIME_OUT); | ||
} | ||
}, [files, target]); | ||
|
||
React.useEffect(() => { | ||
// Handle drop... | ||
if (dropData && !files.length && (!modal || modal.id !== MODALS.FILE_SELECTION)) { | ||
getTree(dropData) | ||
.then(entries => { | ||
if (entries && entries.length) { | ||
setFiles(entries); | ||
} | ||
}) | ||
.catch(error => { | ||
setError(error || true); | ||
}); | ||
} | ||
}, [dropData, files, modal]); | ||
|
||
React.useEffect(() => { | ||
// Files or directory dropped: | ||
if (!drag && files.length) { | ||
// Handle multiple files selection | ||
if (files.length > 1) { | ||
openModal(MODALS.FILE_SELECTION, { files: files }); | ||
setFiles([]); | ||
} else if (files.length === 1) { | ||
// Handle single file selection | ||
setTarget(files[0]); | ||
handleFileSelected(files[0]); | ||
} | ||
} | ||
}, [drag, files, error, openModal]); | ||
|
||
// 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 ( | ||
<div className={classnames('file-drop', show && 'file-drop--show')}> | ||
<div className={classnames('card', 'file-drop__area')}> | ||
<Icon size={64} icon={icon} className={'main-icon'} /> | ||
<p>{target ? target.name : __(`Drop here to publish!`)} </p> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default withRouter(FileDrop); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.