diff options
Diffstat (limited to 'src/mobile')
| -rw-r--r-- | src/mobile/ImageUpload.tsx | 9 | ||||
| -rw-r--r-- | src/mobile/MobileInkOverlay.scss | 39 | ||||
| -rw-r--r-- | src/mobile/MobileInkOverlay.tsx | 191 | ||||
| -rw-r--r-- | src/mobile/MobileInterface.scss | 19 | ||||
| -rw-r--r-- | src/mobile/MobileInterface.tsx | 322 |
5 files changed, 559 insertions, 21 deletions
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 1583e3d5d..5903a2ce9 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -13,6 +13,7 @@ import { observable } from 'mobx'; import { Utils } from '../Utils'; import MobileInterface from './MobileInterface'; import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; +import { Scripting } from '../client/util/Scripting'; @@ -27,12 +28,11 @@ const inputRef = React.createRef<HTMLInputElement>(); @observer class Uploader extends React.Component { - @observable - error: string = ""; - @observable - status: string = ""; + @observable error: string = ""; + @observable status: string = ""; onClick = async () => { + console.log("uploader click"); try { this.status = "initializing protos"; await Docs.Prototypes.initialize(); @@ -47,6 +47,7 @@ class Uploader extends React.Component { const upload = window.location.origin + "/uploadFormData"; this.status = "uploading image"; + console.log("uploading image", formData); const res = await fetch(upload, { method: 'POST', body: formData diff --git a/src/mobile/MobileInkOverlay.scss b/src/mobile/MobileInkOverlay.scss new file mode 100644 index 000000000..b9c1fb146 --- /dev/null +++ b/src/mobile/MobileInkOverlay.scss @@ -0,0 +1,39 @@ +.mobileInkOverlay { + border: 10px dashed red; + background-color: rgba(0, 0, 0, .05); +} + +.mobileInkOverlay-border { + // background-color: rgba(0, 255, 0, .4); + position: absolute; + pointer-events: auto; + cursor: pointer; + + &.top { + width: calc(100% + 20px); + height: 10px; + top: -10px; + left: -10px; + } + + &.left { + width: 10px; + height: calc(100% + 20px); + top: -10px; + left: -10px; + } + + &.right { + width: 10px; + height: calc(100% + 20px); + top: -10px; + right: -10px; + } + + &.bottom { + width: calc(100% + 20px); + height: 10px; + bottom: -10px; + left: -10px; + } +}
\ No newline at end of file diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx new file mode 100644 index 000000000..1537ae034 --- /dev/null +++ b/src/mobile/MobileInkOverlay.tsx @@ -0,0 +1,191 @@ +import React = require('react'); +import { observer } from "mobx-react"; +import { MobileInkOverlayContent, GestureContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "../server/Message"; +import { observable, action } from "mobx"; +import { GestureUtils } from "../pen-gestures/GestureUtils"; +import "./MobileInkOverlay.scss"; +import { StrCast, Cast } from '../new_fields/Types'; +import { DragManager } from "../client/util/DragManager"; +import { DocServer } from '../client/DocServer'; +import { Doc, DocListCastAsync } from '../new_fields/Doc'; +import { listSpec } from '../new_fields/Schema'; + + +@observer +export default class MobileInkOverlay extends React.Component { + public static Instance: MobileInkOverlay; + + @observable private _scale: number = 1; + @observable private _width: number = 0; + @observable private _height: number = 0; + @observable private _x: number = -300; + @observable private _y: number = -300; + @observable private _text: string = ""; + + @observable private _offsetX: number = 0; + @observable private _offsetY: number = 0; + @observable private _isDragging: boolean = false; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + + constructor(props: Readonly<{}>) { + super(props); + MobileInkOverlay.Instance = this; + } + + initialSize(mobileWidth: number, mobileHeight: number) { + const maxWidth = window.innerWidth - 30; + const maxHeight = window.innerHeight - 30; // -30 for padding + if (mobileWidth > maxWidth || mobileHeight > maxHeight) { + const scale = Math.min(maxWidth / mobileWidth, maxHeight / mobileHeight); + return { width: mobileWidth * scale, height: mobileHeight * scale, scale: scale }; + } + return { width: mobileWidth, height: mobileHeight, scale: 1 }; + } + + @action + initMobileInkOverlay(content: MobileInkOverlayContent) { + const { width, height, text } = content; + const scaledSize = this.initialSize(width ? width : 0, height ? height : 0); + this._width = scaledSize.width; + this._height = scaledSize.height; + this._scale = scaledSize.scale; + this._x = 300; // TODO: center on screen + this._y = 25; // TODO: center on screen + this._text = text ? text : ""; + } + + @action + updatePosition(content: UpdateMobileInkOverlayPositionContent) { + const { dx, dy, dsize } = content; + if (dx) this._x += dx; + if (dy) this._y += dy; + // TODO: scale dsize + } + + drawStroke = (content: GestureContent) => { + // TODO: figure out why strokes drawn in corner of mobile interface dont get inserted + + const { points, bounds } = content; + console.log("received points", points, bounds); + + const B = { + right: (bounds.right * this._scale) + this._x, + left: (bounds.left * this._scale) + this._x, // TODO: scale + bottom: (bounds.bottom * this._scale) + this._y, + top: (bounds.top * this._scale) + this._y, // TODO: scale + width: bounds.width * this._scale, + height: bounds.height * this._scale, + }; + + const target = document.elementFromPoint(this._x + 10, this._y + 10); + target?.dispatchEvent( + new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture", + { + bubbles: true, + detail: { + points: points, + gesture: GestureUtils.Gestures.Stroke, + bounds: B + } + } + ) + ); + } + + uploadDocument = async (content: MobileDocumentUploadContent) => { + const { docId } = content; + const doc = await DocServer.GetRefField(docId); + + if (doc && doc instanceof Doc) { + const target = document.elementFromPoint(this._x + 10, this._y + 10); + const dragData = new DragManager.DocumentDragData([doc]); + const complete = new DragManager.DragCompleteEvent(false, dragData); + + if (target) { + console.log("dispatching upload doc!!!!", target, doc); + target.dispatchEvent( + new CustomEvent<DragManager.DropEvent>("dashOnDrop", + { + bubbles: true, + detail: { + x: this._x, + y: this._y, + complete: complete, + altKey: false, + metaKey: false, + ctrlKey: false, + shiftKey: false + } + } + ) + ); + } else { + alert("TARGET IS UNDEFINED"); + } + } + } + + @action + dragStart = (e: React.PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + document.addEventListener("pointermove", this.dragging); + document.addEventListener("pointerup", this.dragEnd); + + this._isDragging = true; + this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left; + this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top; + + e.preventDefault(); + e.stopPropagation(); + } + + @action + dragging = (e: PointerEvent) => { + const x = e.pageX - this._offsetX; + const y = e.pageY - this._offsetY; + + // TODO: don't allow drag over library? + this._x = Math.min(Math.max(x, 0), window.innerWidth - this._width); + this._y = Math.min(Math.max(y, 0), window.innerHeight - this._height); + + e.preventDefault(); + e.stopPropagation(); + } + + @action + dragEnd = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.dragging); + document.removeEventListener("pointerup", this.dragEnd); + + this._isDragging = false; + + e.preventDefault(); + e.stopPropagation(); + } + + render() { + + return ( + <div className="mobileInkOverlay" + style={{ + width: this._width, + height: this._height, + position: "absolute", + transform: `translate(${this._x}px, ${this._y}px)`, + zIndex: 30000, + pointerEvents: "none", + borderStyle: this._isDragging ? "solid" : "dashed", + } + } + ref={this._mainCont} + > + <p>{this._text}</p> + <div className="mobileInkOverlay-border top" onPointerDown={this.dragStart}></div> + <div className="mobileInkOverlay-border bottom" onPointerDown={this.dragStart}></div> + <div className="mobileInkOverlay-border left" onPointerDown={this.dragStart}></div> + <div className="mobileInkOverlay-border right" onPointerDown={this.dragStart}></div> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/mobile/MobileInterface.scss b/src/mobile/MobileInterface.scss new file mode 100644 index 000000000..4d86e208f --- /dev/null +++ b/src/mobile/MobileInterface.scss @@ -0,0 +1,19 @@ +.mobileInterface-inkInterfaceButtons { + position: absolute; + top: 0px; + display: flex; + justify-content: space-between; + width: 100%; + z-index: 9999; + height: 50px; + + .mobileInterface-button { + height: 100%; + } +} + +.mobileInterface-container { + height: 100%; + position: relative; + touch-action: none; +}
\ No newline at end of file diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index b1eaeaa0a..c87ac719c 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -1,46 +1,136 @@ import React = require('react'); import { observer } from 'mobx-react'; -import { computed, action } from 'mobx'; +import { computed, action, observable } from 'mobx'; import { CurrentUserUtils } from '../server/authentication/models/current_user_utils'; -import { FieldValue, Cast } from '../new_fields/Types'; -import { Doc } from '../new_fields/Doc'; +import { FieldValue, Cast, StrCast } from '../new_fields/Types'; +import { Doc, DocListCast } from '../new_fields/Doc'; import { Docs } from '../client/documents/Documents'; import { CollectionView } from '../client/views/collections/CollectionView'; import { DocumentView } from '../client/views/nodes/DocumentView'; -import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue } from '../Utils'; +import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, returnZero } from '../Utils'; import { Transform } from '../client/util/Transform'; import { library } from '@fortawesome/fontawesome-svg-core'; -import { faPenNib, faHighlighter, faEraser, faMousePointer } from '@fortawesome/free-solid-svg-icons'; +import { faPenNib, faHighlighter, faEraser, faMousePointer, faBreadSlice, faTrash, faCheck, faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons'; +import { Scripting } from '../client/util/Scripting'; +import { CollectionFreeFormView } from '../client/views/collections/collectionFreeForm/CollectionFreeFormView'; +import GestureOverlay from '../client/views/GestureOverlay'; +import { InkingControl } from '../client/views/InkingControl'; +import { InkTool } from '../new_fields/InkField'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import "./MobileInterface.scss"; +import { SelectionManager } from '../client/util/SelectionManager'; +import { DateField } from '../new_fields/DateField'; +import { GestureUtils } from '../pen-gestures/GestureUtils'; +import { DocServer } from '../client/DocServer'; +import { DocumentDecorations } from '../client/views/DocumentDecorations'; +import { OverlayView } from '../client/views/OverlayView'; +import { DictationOverlay } from '../client/views/DictationOverlay'; +import SharingManager from '../client/util/SharingManager'; +import { PreviewCursor } from '../client/views/PreviewCursor'; +import { ContextMenu } from '../client/views/ContextMenu'; +import { RadialMenu } from '../client/views/nodes/RadialMenu'; +import PDFMenu from '../client/views/pdf/PDFMenu'; +import MarqueeOptionsMenu from '../client/views/collections/collectionFreeForm/MarqueeOptionsMenu'; +import GoogleAuthenticationManager from '../client/apis/GoogleAuthenticationManager'; +import { listSpec } from '../new_fields/Schema'; +import { Id } from '../new_fields/FieldSymbols'; +import { DocumentManager } from '../client/util/DocumentManager'; +import RichTextMenu from '../client/util/RichTextMenu'; +import { WebField } from "../new_fields/URLField"; +import { FieldResult } from "../new_fields/Doc"; +import { List } from '../new_fields/List'; + +library.add(faLongArrowAltLeft); @observer export default class MobileInterface extends React.Component { + @observable static Instance: MobileInterface; @computed private get userDoc() { return CurrentUserUtils.UserDocument; } @computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeMobile, Doc)) : CurrentUserUtils.GuestMobile; } + // @observable private currentView: "main" | "ink" | "upload" = "main"; + private mainDoc: any = CurrentUserUtils.setupMobileDoc(this.userDoc); + @observable private renderView?: () => JSX.Element; + + // private inkDoc?: Doc; + public drawingInk: boolean = false; + + // private uploadDoc?: Doc; + + constructor(props: Readonly<{}>) { + super(props); + MobileInterface.Instance = this; + } @action componentDidMount = () => { library.add(...[faPenNib, faHighlighter, faEraser, faMousePointer]); if (this.userDoc && !this.mainContainer) { - const doc = CurrentUserUtils.setupMobileDoc(this.userDoc); - this.userDoc.activeMobile = doc; + this.userDoc.activeMobile = this.mainDoc; + } + } + + @action + switchCurrentView = (doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) => { + if (!this.userDoc) return; + + this.userDoc.activeMobile = doc(this.userDoc); + onSwitch && onSwitch(); + + this.renderView = renderView; + } + + onSwitchInking = () => { + InkingControl.Instance.switchTool(InkTool.Pen); + MobileInterface.Instance.drawingInk = true; + + DocServer.Mobile.dispatchOverlayTrigger({ + enableOverlay: true, + width: window.innerWidth, + height: window.innerHeight + }); + } + + onSwitchUpload = async () => { + let width = 300; + let height = 300; + + // get width and height of the collection doc + if (this.mainContainer) { + const data = Cast(this.mainContainer.data, listSpec(Doc)); + if (data) { + const collectionDoc = await data[1]; // this should be the collection doc since the positions should be locked + const docView = DocumentManager.Instance.getDocumentView(collectionDoc); + if (docView) { + width = docView.nativeWidth ? docView.nativeWidth : 300; + height = docView.nativeHeight ? docView.nativeHeight : 300; + } + } } + DocServer.Mobile.dispatchOverlayTrigger({ + enableOverlay: true, + width: width, + height: height, + text: "Documents uploaded from mobile will show here", + }); } - @computed - get mainContent() { + renderDefaultContent = () => { if (this.mainContainer) { return <DocumentView Document={this.mainContainer} DataDoc={undefined} LibraryPath={emptyPath} - addDocument={undefined} + addDocument={returnFalse} addDocTab={returnFalse} pinToPres={emptyFunction} + rootSelected={returnFalse} removeDocument={undefined} onClick={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} PanelWidth={() => window.screen.width} PanelHeight={() => window.screen.height} renderDepth={0} @@ -50,19 +140,217 @@ export default class MobileInterface extends React.Component { whenActiveChanged={emptyFunction} bringToFront={emptyFunction} ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - zoomToScale={emptyFunction} - getScale={returnOne}> - </DocumentView>; + ContainingCollectionDoc={undefined} />; } return "hello"; } + onBack = (e: React.MouseEvent) => { + this.switchCurrentView((userDoc: Doc) => this.mainDoc); + InkingControl.Instance.switchTool(InkTool.None); // TODO: switch to previous tool + + DocServer.Mobile.dispatchOverlayTrigger({ + enableOverlay: false, + width: window.innerWidth, + height: window.innerHeight + }); + + // this.inkDoc = undefined; + this.drawingInk = false; + } + + shiftLeft = (e: React.MouseEvent) => { + DocServer.Mobile.dispatchOverlayPositionUpdate({ + dx: -10 + }); + e.preventDefault(); + e.stopPropagation(); + } + + shiftRight = (e: React.MouseEvent) => { + DocServer.Mobile.dispatchOverlayPositionUpdate({ + dx: 10 + }); + e.preventDefault(); + e.stopPropagation(); + } + + panelHeight = () => window.innerHeight; + panelWidth = () => window.innerWidth; + renderInkingContent = () => { + console.log("rendering inking content"); + // TODO: support panning and zooming + // TODO: handle moving of ink strokes + if (this.mainContainer) { + return ( + <div className="mobileInterface"> + <div className="mobileInterface-inkInterfaceButtons"> + <div className="navButtons"> + <button className="mobileInterface-button cancel" onClick={this.onBack} title="Cancel drawing">BACK</button> + </div> + <div className="inkSettingButtons"> + <button className="mobileInterface-button cancel" onClick={this.onBack} title="Cancel drawing"><FontAwesomeIcon icon="long-arrow-alt-left" /></button> + </div> + <div className="navButtons"> + <button className="mobileInterface-button" onClick={this.shiftLeft} title="Shift left">left</button> + <button className="mobileInterface-button" onClick={this.shiftRight} title="Shift right">right</button> + </div> + </div> + <CollectionView + Document={this.mainContainer} + DataDoc={undefined} + LibraryPath={emptyPath} + fieldKey={""} + dropAction={"alias"} + bringToFront={emptyFunction} + addDocTab={returnFalse} + pinToPres={emptyFunction} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} + focus={emptyFunction} + isSelected={returnFalse} + select={emptyFunction} + active={returnFalse} + ContentScaling={returnOne} + whenActiveChanged={returnFalse} + ScreenToLocalTransform={Transform.Identity} + renderDepth={0} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + rootSelected={returnTrue}> + </CollectionView> + </div> + ); + } + } + + upload = async (e: React.MouseEvent) => { + if (this.mainContainer) { + const data = Cast(this.mainContainer.data, listSpec(Doc)); + if (data) { + const collectionDoc = await data[1]; // this should be the collection doc since the positions should be locked + const children = DocListCast(collectionDoc.data); + const uploadDoc = children.length === 1 ? children[0] : Docs.Create.StackingDocument(children, { + title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true, _width: 300, _height: 300 + }); + if (uploadDoc) { + DocServer.Mobile.dispatchMobileDocumentUpload({ + docId: uploadDoc[Id], + }); + } + } + } + e.stopPropagation(); + e.preventDefault(); + } + + addWebToCollection = async () => { + let url = "https://en.wikipedia.org/wiki/Hedgehog"; + if (this.mainContainer) { + const data = Cast(this.mainContainer.data, listSpec(Doc)); + if (data) { + const webDoc = await data[0]; + const urlField: FieldResult<WebField> = Cast(webDoc.data, WebField); + url = urlField ? urlField.url.toString() : "https://en.wikipedia.org/wiki/Hedgehog"; + + } + } + Docs.Create.WebDocument(url, { _width: 300, _height: 300, title: "Mobile Upload Web Doc" }); + } + + clearUpload = async () => { + if (this.mainContainer) { + const data = Cast(this.mainContainer.data, listSpec(Doc)); + if (data) { + const collectionDoc = await data[1]; + const children = DocListCast(collectionDoc.data); + children.forEach(doc => { + }); + // collectionDoc[data] = new List<Doc>(); + } + } + } + + renderUploadContent() { + if (this.mainContainer) { + return ( + <div className="mobileInterface" onDragOver={this.onDragOver}> + <div className="mobileInterface-inkInterfaceButtons"> + <button className="mobileInterface-button cancel" onClick={this.onBack} title="Back">BACK</button> + {/* <button className="mobileInterface-button" onClick={this.clearUpload} title="Clear Upload">CLEAR</button> */} + {/* <button className="mobileInterface-button" onClick={this.addWeb} title="Add Web Doc to Upload Collection"></button> */} + <button className="mobileInterface-button" onClick={this.upload} title="Upload">UPLOAD</button> + </div> + <DocumentView + Document={this.mainContainer} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={returnFalse} + addDocTab={returnFalse} + pinToPres={emptyFunction} + rootSelected={returnFalse} + removeDocument={undefined} + onClick={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={returnOne} + NativeHeight={returnZero} + NativeWidth={returnZero} + PanelWidth={() => window.screen.width} + PanelHeight={() => window.screen.height} + renderDepth={0} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} /> + </div> + ); + } + } + + onDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + } + render() { + // const content = this.currentView === "main" ? this.mainContent : + // this.currentView === "ink" ? this.inkContent : + // this.currentView === "upload" ? this.uploadContent : <></>; return ( - <div className="mobile-container"> - {this.mainContent} + <div className="mobileInterface-container" onDragOver={this.onDragOver}> + {/* <DocumentDecorations /> + <GestureOverlay> + {this.renderView ? this.renderView() : this.renderDefaultContent()} + </GestureOverlay> */} + + {/* <DictationOverlay /> + <SharingManager /> + <GoogleAuthenticationManager /> */} + <DocumentDecorations /> + <GestureOverlay> + {this.renderView ? this.renderView() : this.renderDefaultContent()} + </GestureOverlay> + <PreviewCursor /> + {/* <ContextMenu /> */} + <RadialMenu /> + <RichTextMenu /> + {/* <PDFMenu /> + <MarqueeOptionsMenu /> + <OverlayView /> */} </div> ); } -}
\ No newline at end of file +} + +Scripting.addGlobal(function switchMobileView(doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); }); +Scripting.addGlobal(function onSwitchMobileInking() { return MobileInterface.Instance.onSwitchInking(); }); +Scripting.addGlobal(function renderMobileInking() { return MobileInterface.Instance.renderInkingContent(); }); +Scripting.addGlobal(function onSwitchMobileUpload() { return MobileInterface.Instance.onSwitchUpload(); }); +Scripting.addGlobal(function renderMobileUpload() { return MobileInterface.Instance.renderUploadContent(); }); +Scripting.addGlobal(function addWebToMobileUpload() { return MobileInterface.Instance.addWebToCollection(); }); + |
