aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/documents/Documents.ts57
-rw-r--r--src/client/util/Import & Export/ImageImporter.tsx67
-rw-r--r--src/client/util/Import & Export/ImportBox.tsx134
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/views/MainView.tsx3
-rw-r--r--src/client/views/collections/CollectionSubView.tsx46
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
7 files changed, 265 insertions, 47 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7d7a1f02a..5d637dd3a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -36,6 +36,7 @@ import { UndoManager } from "../util/UndoManager";
import { RouteStore } from "../../server/RouteStore";
import { LinkManager } from "../util/LinkManager";
import { DocumentManager } from "../util/DocumentManager";
+import ImportBox from "../util/Import & Export/ImportBox";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -51,7 +52,8 @@ export enum DocTypes {
KVP = "kvp",
VID = "video",
AUDIO = "audio",
- LINK = "link"
+ LINK = "link",
+ IMPORT = "import"
}
export interface DocumentOptions {
@@ -127,6 +129,7 @@ export namespace Docs {
let audioProto: Doc;
let pdfProto: Doc;
let iconProto: Doc;
+ let importProto: Doc;
// let linkProto: Doc;
const textProtoId = "textProto";
const histoProtoId = "histoProto";
@@ -138,6 +141,7 @@ export namespace Docs {
const videoProtoId = "videoProto";
const audioProtoId = "audioProto";
const iconProtoId = "iconProto";
+ const importProtoId = "importProto";
// const linkProtoId = "linkProto";
export function initProtos(): Promise<void> {
@@ -152,6 +156,7 @@ export namespace Docs {
audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
+ importProto = fields[importProtoId] as Doc || CreateImportPrototype();
});
}
@@ -174,6 +179,11 @@ export namespace Docs {
return imageProto;
}
+ function CreateImportPrototype(): Doc {
+ let importProto = setupPrototypeOptions(importProtoId, "IMPORT_PROTO", ImportBox.LayoutString(), { x: 0, y: 0, width: 600, height: 600, type: DocTypes.IMPORT });
+ return importProto;
+ }
+
function CreateHistogramPrototype(): Doc {
let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"),
{ x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString(), type: DocTypes.HIST });
@@ -261,6 +271,10 @@ export namespace Docs {
return CreateInstance(audioProto, new AudioField(new URL(url)), options);
}
+ export function DirectoryImportDocument(options: DocumentOptions = {}) {
+ return CreateInstance(importProto, "", options);
+ }
+
export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
return CreateInstance(histoProto, new HistogramField(histoOp), options);
}
@@ -333,6 +347,47 @@ export namespace Docs {
return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
+ export async function getDocumentFromType(type: string, path: string, options: DocumentOptions, addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean): Promise<Opt<Doc>> {
+ let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+ if (type.indexOf("image") !== -1) {
+ ctor = Docs.ImageDocument;
+ }
+ if (type.indexOf("video") !== -1) {
+ ctor = Docs.VideoDocument;
+ }
+ if (type.indexOf("audio") !== -1) {
+ ctor = Docs.AudioDocument;
+ }
+ if (type.indexOf("pdf") !== -1) {
+ ctor = Docs.PdfDocument;
+ options.nativeWidth = 1200;
+ }
+ if (type.indexOf("excel") !== -1) {
+ ctor = Docs.DBDocument;
+ options.dropAction = "copy";
+ }
+ if (type.indexOf("html") !== -1) {
+ if (path.includes(window.location.hostname)) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ DocServer.GetRefField(id).then(field => {
+ if (field instanceof Doc) {
+ let alias = Doc.MakeAlias(field);
+ alias.x = options.x || 0;
+ alias.y = options.y || 0;
+ alias.width = options.width || 300;
+ alias.height = options.height || options.width || 300;
+ addDocument && addDocument(alias, false);
+ }
+ });
+ return undefined;
+ }
+ ctor = Docs.WebDocument;
+ options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ }
+ return ctor ? ctor(path, options) : undefined;
+ }
+
export function CaptionDocument(doc: Doc) {
const captionDoc = Doc.MakeAlias(doc);
captionDoc.overlayLayout = FixedCaption();
diff --git a/src/client/util/Import & Export/ImageImporter.tsx b/src/client/util/Import & Export/ImageImporter.tsx
new file mode 100644
index 000000000..d664f6487
--- /dev/null
+++ b/src/client/util/Import & Export/ImageImporter.tsx
@@ -0,0 +1,67 @@
+import "fs";
+import React = require("react");
+import { Doc } from "../../../new_fields/Doc";
+import { DocServer } from "../../DocServer";
+import { RouteStore } from "../../../server/RouteStore";
+import { action } from "mobx";
+import { Docs } from "../../documents/Documents";
+import { FieldViewProps } from "../../views/nodes/FieldView";
+
+interface ImageImporterProps {
+ addSchema: (imageDocs: Doc[]) => void;
+}
+
+export default class BulkImporter extends React.Component<FieldViewProps> {
+ private selector = React.createRef<HTMLInputElement>();
+
+ handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ let promises: Promise<void>[] = [];
+ let docs: Doc[] = [];
+
+ let files = e.target.files;
+ if (!files) return;
+
+ for (let i = 0; i < files.length; i++) {
+ let target = files.item(i);
+
+ if (target === null) {
+ continue;
+ }
+
+ let type = target.type;
+ let formData = new FormData();
+ formData.append('file', target);
+ let dropFileName = target ? target.name : "-empty-";
+
+ let prom = fetch(DocServer.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ }).then(async (res: Response) => {
+ (await res.json()).map(action((file: any) => {
+ let path = window.location.origin + file;
+ let docPromise = Docs.getDocumentFromType(type, path, { nativeWidth: 300, width: 300, title: dropFileName });
+ docPromise.then(doc => doc && docs.push(doc));
+ }));
+ });
+ promises.push(prom);
+ }
+
+ await Promise.all(promises);
+
+ let parent = Docs.SchemaDocument(["title", "data"], docs, { width: 300, height: 300, title: "Bulk Import from Directory" });
+ }
+
+ componentDidMount() {
+ this.selector.current!.setAttribute("directory", "true");
+ this.selector.current!.setAttribute("webkitdirectory", "true");
+ }
+
+ render() {
+ return (
+ <div>
+ <input ref={this.selector} name={"selector"} onChange={this.handleSelection} type="file" style={{ position: "absolute" }} />
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/Import & Export/ImportBox.tsx b/src/client/util/Import & Export/ImportBox.tsx
new file mode 100644
index 000000000..630911710
--- /dev/null
+++ b/src/client/util/Import & Export/ImportBox.tsx
@@ -0,0 +1,134 @@
+import "fs";
+import React = require("react");
+import { Doc } from "../../../new_fields/Doc";
+import { DocServer } from "../../DocServer";
+import { RouteStore } from "../../../server/RouteStore";
+import { action, observable } 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 { faArrowUp } from '@fortawesome/free-solid-svg-icons';
+import { Docs, DocumentOptions } from "../../documents/Documents";
+
+interface ImageImporterProps {
+ addSchema: (imageDocs: Doc[]) => void;
+}
+
+export default class ImportBox extends React.Component<FieldViewProps> {
+ @observable private top = 0;
+ @observable private left = 0;
+ private dimensions = 50;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ library.add(faArrowUp);
+ }
+
+ public static LayoutString() { return FieldView.LayoutString(ImportBox); }
+
+ private selector = React.createRef<HTMLInputElement>();
+
+ handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
+ let promises: Promise<void>[] = [];
+ let docs: Doc[] = [];
+
+ let files = e.target.files;
+ if (!files || files.length === 0) return;
+
+ let directory = (files.item(0) as any).webkitRelativePath.split("/", 1);
+
+ for (let i = 0; i < files.length; i++) {
+ let uploaded_file = files.item(i);
+
+ if (!uploaded_file) {
+ continue;
+ }
+
+ let formData = new FormData();
+ formData.append('file', uploaded_file);
+ let dropFileName = uploaded_file ? uploaded_file.name : "-empty-";
+ let type = uploaded_file.type;
+
+ let prom = fetch(DocServer.prepend(RouteStore.upload), {
+ method: 'POST',
+ body: formData
+ }).then(async (res: Response) => {
+ (await res.json()).map(action((file: any) => {
+ let path = DocServer.prepend(file);
+ let docPromise = Docs.getDocumentFromType(type, path, { nativeWidth: 300, width: 300, title: dropFileName });
+ docPromise.then(doc => doc && docs.push(doc));
+ }));
+ });
+ promises.push(prom);
+ }
+
+ await Promise.all(promises);
+
+ let doc = this.props.Document;
+ let options: DocumentOptions = { title: `Import of ${directory}`, width: 500, height: 500, x: Doc.GetT(doc, "x", "number"), y: Doc.GetT(doc, "y", "number") };
+ let parent = this.props.ContainingCollectionView;
+ if (parent) {
+ let importContainer = Docs.StackingDocument(docs, options);
+ Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+ this.props.removeDocument && this.props.removeDocument(doc);
+ }
+ }
+
+ componentDidMount() {
+ this.selector.current!.setAttribute("directory", "true");
+ this.selector.current!.setAttribute("webkitdirectory", "true");
+ }
+
+ @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;
+ }
+
+ render() {
+ let dimensions = 50;
+ return (
+ <Measure offset onResize={this.preserveCentering}>
+ {({ measureRef }) =>
+ <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+ <input
+ id={"selector"}
+ ref={this.selector}
+ name={"selector"}
+ onChange={this.handleSelection}
+ type="file"
+ style={{
+ position: "absolute",
+ display: "none"
+ }} />
+ <label htmlFor={"selector"}>
+ <div style={{
+ width: dimensions,
+ height: dimensions,
+ borderRadius: "50%",
+ background: "black",
+ position: "absolute",
+ left: this.left,
+ top: this.top
+ }} />
+ <div style={{
+ position: "absolute",
+ left: this.left + 12.5,
+ top: this.top + 11
+ }}>
+ <FontAwesomeIcon icon={faArrowUp} color="#FFFFFF" size={"2x"} />
+ </div>
+ </label>
+ </div>
+ }
+ </Measure>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 3c396362e..9efef888d 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -53,7 +53,7 @@ export namespace SelectionManager {
stored.length > 0 && (targetColor = stored);
}
InkingControl.Instance.updateSelectedColor(targetColor);
- });
+ }, { fireImmediately: true });
export function DeselectDoc(docView: DocumentView): void {
manager.DeselectDoc(docView);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ab9fe0118..e38fd3c8b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -38,6 +38,7 @@ import PDFMenu from './pdf/PDFMenu';
import { InkTool } from '../../new_fields/InkField';
import _ from "lodash";
import KeyManager from './GlobalKeyHandler';
+import BulkImporter from '../util/Import & Export/ImageImporter';
@observer
export class MainView extends React.Component {
@@ -351,11 +352,13 @@ export class MainView extends React.Component {
let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
let addTreeNode = action(() => CurrentUserUtils.UserDocument);
let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addImportCollectionNode = action(() => Docs.DirectoryImportDocument({ title: "Directory Import", width: 150, height: 150 }));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
[React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
[React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
+ [React.createRef<HTMLDivElement>(), "arrow-up", "Import Directory", addImportCollectionNode],
];
return < div id="add-nodes-menu" >
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 3b3bbdbd9..e886ad84a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -102,47 +102,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return false;
}
- protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
- let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
- if (type.indexOf("image") !== -1) {
- ctor = Docs.ImageDocument;
- }
- if (type.indexOf("video") !== -1) {
- ctor = Docs.VideoDocument;
- }
- if (type.indexOf("audio") !== -1) {
- ctor = Docs.AudioDocument;
- }
- if (type.indexOf("pdf") !== -1) {
- ctor = Docs.PdfDocument;
- options.nativeWidth = 1200;
- }
- if (type.indexOf("excel") !== -1) {
- ctor = Docs.DBDocument;
- options.dropAction = "copy";
- }
- if (type.indexOf("html") !== -1) {
- if (path.includes(window.location.hostname)) {
- let s = path.split('/');
- let id = s[s.length - 1];
- DocServer.GetRefField(id).then(field => {
- if (field instanceof Doc) {
- let alias = Doc.MakeAlias(field);
- alias.x = options.x || 0;
- alias.y = options.y || 0;
- alias.width = options.width || 300;
- alias.height = options.height || options.width || 300;
- this.props.addDocument(alias, false);
- }
- });
- return undefined;
- }
- ctor = Docs.WebDocument;
- options = { height: options.width, ...options, title: path, nativeWidth: undefined };
- }
- return ctor ? ctor(path, options) : undefined;
- }
-
@undoBatch
@action
protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
@@ -223,7 +182,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
.then(result => {
let type = result["content-type"];
if (type) {
- this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
+ Docs.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }, this.props.addDocument)
.then(doc => doc && this.props.addDocument(doc, false));
}
});
@@ -245,8 +204,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName });
-
+ let docPromise = Docs.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName }, this.props.addDocument);
docPromise.then(doc => doc && this.props.addDocument(doc));
}));
});
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 0da4888a1..e9fec1588 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -24,6 +24,7 @@ import { Without, OmitKeys } from "../../../Utils";
import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { Doc } from "../../../new_fields/Doc";
+import ImportBox from "../../util/Import & Export/ImportBox";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -106,7 +107,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (this.props.renderDepth > 7) return (null);
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
- components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ components={{ FormattedTextBox, ImageBox, IconBox, ImportBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
bindings={this.CreateBindings(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document)}
jsx={this.finalLayout}
showWarnings={true}