import "fs"; import React = require("react"); import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { DocServer } from "../../DocServer"; import { RouteStore } from "../../../server/RouteStore"; import { action, observable, autorun, runInAction, computed } from "mobx"; import { FieldViewProps, FieldView } from "../../views/nodes/FieldView"; import Measure, { ContentRect } from "react-measure"; import { library } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons'; import { Docs, DocumentOptions } from "../../documents/Documents"; import { observer } from "mobx-react"; import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry"; import { Utils } from "../../../Utils"; import { DocumentManager } from "../DocumentManager"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { Cast, BoolCast, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils"; const unsupported = ["text/html", "text/plain"]; interface FileResponse { name: string; path: string; type: string; } @observer export default class DirectoryImportBox extends React.Component { private selector = React.createRef(); @observable private top = 0; @observable private left = 0; private dimensions = 50; @observable private entries: ImportMetadataEntry[] = []; @observable private quota = 1; @observable private completed = 0; @observable private uploading = false; @observable private removeHover = false; public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); } constructor(props: FieldViewProps) { super(props); library.add(faTag, faPlus); let doc = this.props.Document; this.editingMetadata = this.editingMetadata || false; this.persistent = this.persistent || false; !Cast(doc.data, listSpec(Doc)) && (doc.data = new List()); } @computed private get editingMetadata() { return BoolCast(this.props.Document.editingMetadata); } private set editingMetadata(value: boolean) { this.props.Document.editingMetadata = value; } @computed private get persistent() { return BoolCast(this.props.Document.persistent); } private set persistent(value: boolean) { this.props.Document.persistent = value; } handleSelection = async (e: React.ChangeEvent) => { runInAction(() => this.uploading = true); let docs: Doc[] = []; let files = e.target.files; if (!files || files.length === 0) return; let directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0]; let validated: File[] = []; for (let i = 0; i < files.length; i++) { let file = files.item(i); file && !unsupported.includes(file.type) && validated.push(file); } runInAction(() => { this.quota = validated.length; this.completed = 0; }); let sizes: number[] = []; let modifiedDates: number[] = []; const localUpload = async (batch: File[]) => { sizes.push(...batch.map(file => file.size)); modifiedDates.push(...batch.map(file => file.lastModified)); let formData = new FormData(); batch.forEach(file => formData.append(Utils.GenerateGuid(), file)); const parameters = { method: 'POST', body: formData }; runInAction(() => this.completed += batch.length); return (await fetch(Utils.prepend(RouteStore.upload), parameters)).json(); }; const uploads = await validated.batchAction(15, localUpload); await Promise.all(uploads.map(async upload => { const type = upload.type; const path = Utils.prepend(upload.path); const options = { nativeWidth: 300, width: 300, title: upload.name }; const document = await Docs.Get.DocumentFromType(type, path, options); document && docs.push(document); })); for (let i = 0; i < docs.length; i++) { let doc = docs[i]; doc.size = sizes[i]; doc.modified = modifiedDates[i]; this.entries.forEach(entry => { let target = entry.onDataDoc ? Doc.GetProto(doc) : doc; target[entry.key] = entry.value; }); } let doc = this.props.Document; let height: number = NumCast(doc.height) || 0; let offset: number = this.persistent ? (height === 0 ? 0 : height + 30) : 0; let options: DocumentOptions = { title: `Import of ${directory}`, width: 1105, height: 500, x: NumCast(doc.x), y: NumCast(doc.y) + offset }; let parent = this.props.ContainingCollectionView; if (parent) { let importContainer: Doc; if (docs.length < 50) { importContainer = Docs.Create.MasonryDocument(docs, options); } else { importContainer = Docs.Create.SchemaDocument([], docs, options); } await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); importContainer.singleColumn = false; Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); DocumentManager.Instance.jumpToDocument(importContainer, true); } runInAction(() => { this.uploading = false; this.quota = 1; this.completed = 0; }); } componentDidMount() { this.selector.current!.setAttribute("directory", ""); this.selector.current!.setAttribute("webkitdirectory", ""); } @action preserveCentering = (rect: ContentRect) => { let bounds = rect.offset!; if (bounds.width === 0 || bounds.height === 0) { return; } let offset = this.dimensions / 2; this.left = bounds.width / 2 - offset; this.top = bounds.height / 2 - offset; } @action addMetadataEntry = async () => { let entryDoc = new Doc(); entryDoc.checked = false; entryDoc.key = keyPlaceholder; entryDoc.value = valuePlaceholder; Doc.AddDocToList(this.props.Document, "data", entryDoc); } @action remove = async (entry: ImportMetadataEntry) => { let metadata = await DocListCastAsync(this.props.Document.data); if (metadata) { let index = this.entries.indexOf(entry); if (index !== -1) { runInAction(() => this.entries.splice(index, 1)); index = metadata.indexOf(entry.props.Document); if (index !== -1) { metadata.splice(index, 1); } } } } render() { let dimensions = 50; let entries = DocListCast(this.props.Document.data); let isEditing = this.editingMetadata; let completed = this.completed; let quota = this.quota; let uploading = this.uploading; let showRemoveLabel = this.removeHover; let persistent = this.persistent; let percent = `${completed / quota * 100}`; percent = percent.split(".")[0]; percent = percent.startsWith("100") ? "99" : percent; let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6; return ( {({ measureRef }) =>