import "fs"; import React = require("react"); import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; import { RouteStore } from "../../../server/RouteStore"; import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } 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"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; import "./DirectoryImportBox.scss"; import { Networking } from "../../Network"; import { BatchedArray } from "array-batcher"; const unsupported = ["text/html", "text/plain"]; interface ImageUploadResponse { name: string; path: string; type: string; exif: any; } @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 phase = ""; private disposer: Opt; @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; this.phase = "Initializing download..."; }); 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[] = []; runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`); const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync(async batch => { const formData = new FormData(); batch.forEach(file => { sizes.push(file.size); modifiedDates.push(file.lastModified); formData.append(Utils.GenerateGuid(), file); }); const responses = await Networking.PostFormDataToServer(RouteStore.upload, formData); runInAction(() => this.completed += batch.length); return responses as ImageUploadResponse[]; }); 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); const { data, error } = upload.exif; if (document) { Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data); 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 { const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")]; importContainer = Docs.Create.SchemaDocument(headers, docs, options); } runInAction(() => this.phase = 'External: uploading files to Google Photos...'); importContainer.singleColumn = false; await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer }); 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", ""); this.disposer = reaction( () => this.completed, completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`) ); } componentWillUnmount() { this.disposer && this.disposer(); } @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; const message = {this.phase}; const centerPiece = this.phase.includes("Google Photos") ? :
{percent}%
; return ( {({ measureRef }) =>
{message}