add file selector

refactor web file system utils

update styles
This commit is contained in:
btzr-io 2020-05-12 23:05:37 -05:00 committed by Sean Yesmunt
parent 5d0faa740e
commit ec40a4f8ab
5 changed files with 160 additions and 49 deletions

View file

@ -623,4 +623,10 @@ export const icons = {
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</g>
),
[ICONS.COMPLETED]: buildIcon(
<g>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</g>
),
};

View file

@ -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<WebFile>,
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 (
<RadioGroup {...radio} aria-label="fruits">
{files.map((entry, index) => {
const item = radio.stops[index];
const selected = item && item.id === radio.currentId;
return (
<label key={entry.name} className={classnames(selected && 'selected')}>
<Radio {...radio} value={entry.name} />
<Icon size={18} selected={selected} icon={selected ? ICONS.COMPLETED : ICONS.CIRCLE} />
<span>{entry.name}</span>
</label>
);
})}
</RadioGroup>
);
}
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 (
<div className={classnames('file-drop', show && 'file-drop--show')}>
<p>Drop your files here!</p>
{files.map(({ entry }) => (
<div key={entry.name}>{entry.name}</div>
))}
<div className={classnames('card', 'file-drop__area')}>
<Icon size={64} icon={multipleFiles ? ICONS.ALERT : ICONS.PUBLISH} className={'main-icon'} />
<p>{multipleFiles ? `Only one file is allowed choose wisely` : `Drop here to publish!`} </p>
{files && files.length > 0 && (
<div className="file-drop__list">
<FileList files={files} onSelected={handleFileSelected} />
</div>
)}
</div>
</div>
);
}

View file

@ -100,3 +100,4 @@ export const SLIDERS = 'Sliders';
export const SCIENCE = 'Science';
export const ANALYTICS = 'BarChart2';
export const PURCHASED = 'Key';
export const CIRCLE = 'Circle';

View file

@ -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);
}
}
}

View file

@ -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;
};