diff options
Diffstat (limited to 'src/mobile')
-rw-r--r-- | src/mobile/AudioUpload.scss | 61 | ||||
-rw-r--r-- | src/mobile/AudioUpload.tsx | 153 | ||||
-rw-r--r-- | src/mobile/ImageUpload.scss | 130 | ||||
-rw-r--r-- | src/mobile/ImageUpload.tsx | 228 | ||||
-rw-r--r-- | src/mobile/MobileInkOverlay.tsx | 4 | ||||
-rw-r--r-- | src/mobile/MobileInterface.scss | 444 | ||||
-rw-r--r-- | src/mobile/MobileInterface.tsx | 871 | ||||
-rw-r--r-- | src/mobile/MobileMain.tsx | 25 | ||||
-rw-r--r-- | src/mobile/MobileMenu.scss | 0 |
9 files changed, 1536 insertions, 380 deletions
diff --git a/src/mobile/AudioUpload.scss b/src/mobile/AudioUpload.scss new file mode 100644 index 000000000..6e64d9e2e --- /dev/null +++ b/src/mobile/AudioUpload.scss @@ -0,0 +1,61 @@ +@import "../client/views/globalCssVariables.scss"; + +.audioUpload_cont { + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + margin-top: 10px; + height: 400px; + width: 600px; +} + +.upload_label { + position: relative; + font-weight: 700; + color: black; + background-color: rgba(0, 0, 0, 0); + border: solid 3px black; + margin: 10px; + font-size: 30; + height: 70px; + width: 60%; + display: inline-flex; + font-family: sans-serif; + text-transform: uppercase; + justify-content: center; + flex-direction: column; + border-radius: 10px; +} + +.restart_label { + position: relative; + font-weight: 700; + color: black; + background-color: rgba(0, 0, 0, 0); + border: solid 3px black; + margin: 10px; + font-size: 30; + height: 70px; + width: 60%; + display: inline-flex; + font-family: sans-serif; + text-transform: uppercase; + justify-content: center; + flex-direction: column; + border-radius: 10px; +} + +.audio-upload { + top: 100%; + opacity: 0; +} + +.audio-upload.active { + top: 0; + position: absolute; + z-index: 999; + height: 100vh; + width: 100vw; + opacity: 1; +}
\ No newline at end of file diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx new file mode 100644 index 000000000..b75102e43 --- /dev/null +++ b/src/mobile/AudioUpload.tsx @@ -0,0 +1,153 @@ +import { Docs } from '../client/documents/Documents'; +import "./ImageUpload.scss"; +import React = require('react'); +import { observer } from 'mobx-react'; +import { observable, action, computed } from 'mobx'; +import { Utils, emptyPath, returnFalse, emptyFunction, returnOne, returnZero, returnTrue, returnEmptyFilter } from '../Utils'; +import { Doc, Opt } from '../fields/Doc'; +import { Cast, FieldValue } from '../fields/Types'; +import { listSpec } from '../fields/Schema'; +import MainViewModal from '../client/views/MainViewModal'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { nullAudio } from '../fields/URLField'; +import { Transform } from '../client/util/Transform'; +import { DocumentView } from '../client/views/nodes/DocumentView'; +import { MobileInterface } from './MobileInterface'; +import { DictationOverlay } from '../client/views/DictationOverlay'; +import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu'; +import { ContextMenu } from '../client/views/ContextMenu'; + +@observer +export class AudioUpload extends React.Component { + @observable public _audioCol: Doc = FieldValue(Cast(Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: "mobile audio", _width: 500, _height: 100 }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitToBox: true, boxShadow: "0 0" }), Doc)) as Doc; + + /** + * Handles the onclick functionality for the 'Restart' button + * Resets the document to its default view + */ + @action + clearUpload = () => { + for (let i = 1; i < 8; i++) { + this.setOpacity(i, "0.2"); + } + this._audioCol = FieldValue(Cast( + Docs.Create.FreeformDocument( + [Cast(Docs.Create.AudioDocument(nullAudio, { + title: "mobile audio", + _width: 500, + _height: 100 + }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitToBox: true, boxShadow: "0 0" }), Doc)) as Doc; + } + + /** + * Handles the onClick of the 'Close' button + * Reset upload interface and toggle audio + */ + closeUpload = () => { + this.clearUpload(); + MobileInterface.Instance.toggleAudio(); + } + + /** + * Handles the on click of the 'Upload' button. + * Pushing the audio doc onto Dash Web through the right side bar + */ + uploadAudio = () => { + const audioRightSidebar = Cast(Doc.UserDoc().rightSidebarCollection, Doc) as Doc; + const audioDoc = this._audioCol; + const data = Cast(audioRightSidebar.data, listSpec(Doc)); + for (let i = 1; i < 8; i++) { + setTimeout(() => this.setOpacity(i, "1"), i * 200); + } + if (data) { + data.push(audioDoc); + } + // Resets uploader after 3 seconds + setTimeout(this.clearUpload, 3000); + } + + // Returns the upload audio menu + private get uploadInterface() { + return ( + <> + <ContextMenu /> + <DictationOverlay /> + <div style={{ display: "none" }}><RichTextMenu key="rich" /></div> + <div className="closeUpload" onClick={() => this.closeUpload()}> + <FontAwesomeIcon icon="window-close" size={"lg"} /> + </div> + <FontAwesomeIcon icon="microphone" size="lg" style={{ fontSize: "130" }} /> + <div className="audioUpload_cont"> + <DocumentView + Document={this._audioCol} + DataDoc={undefined} + LibraryPath={emptyPath} + addDocument={undefined} + addDocTab={returnFalse} + pinToPres={emptyFunction} + rootSelected={returnTrue} + removeDocument={undefined} + docFilters={returnEmptyFilter} + onClick={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={returnOne} + PanelWidth={() => 600} + PanelHeight={() => 400} + NativeHeight={returnZero} + NativeWidth={returnZero} + renderDepth={0} + focus={emptyFunction} + backgroundColor={() => "rgba(0,0,0,0)"} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + <div className="restart_label" onClick={this.clearUpload}> + Restart + </div> + <div className="upload_label" onClick={this.uploadAudio}> + Upload + </div> + <div className="loadingImage"> + <div className="loadingSlab" id="slab01" /> + <div className="loadingSlab" id="slab02" /> + <div className="loadingSlab" id="slab03" /> + <div className="loadingSlab" id="slab04" /> + <div className="loadingSlab" id="slab05" /> + <div className="loadingSlab" id="slab06" /> + <div className="loadingSlab" id="slab07" /> + </div> + </> + ); + } + + // Handles the setting of the loading bar + setOpacity = (index: number, opacity: string) => { + const slab = document.getElementById("slab0" + index); + if (slab) { + slab.style.opacity = opacity; + } + } + + @observable private dialogueBoxOpacity = 1; + @observable private overlayOpacity = 0.4; + + render() { + return ( + <MainViewModal + contents={this.uploadInterface} + isDisplayed={true} + interactive={true} + dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} + overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.closeUpload} + /> + ); + } + +} + + diff --git a/src/mobile/ImageUpload.scss b/src/mobile/ImageUpload.scss index eea69b81c..890258918 100644 --- a/src/mobile/ImageUpload.scss +++ b/src/mobile/ImageUpload.scss @@ -5,8 +5,32 @@ justify-content: center; flex-direction: column; align-items: center; - width: 100vw; - height: 100vh; + max-width: 400px; + min-width: 400px; + + .upload_label { + font-weight: 700; + color: black; + background-color: rgba(0, 0, 0, 0); + border: solid 3px black; + margin: 10px; + font-size: 30; + height: 70px; + width: 80%; + display: flex; + font-family: sans-serif; + text-transform: uppercase; + justify-content: center; + flex-direction: column; + border-radius: 10px; + } + + .file { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: ltr; + } .button_file { text-align: center; @@ -17,18 +41,98 @@ font-size: 3em; } - .input_file { - display: none; + .inputfile { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; } - .upload_label, - .upload_button { - background: $dark-color; - font-size: 500%; - font-family: $sans-serif; - text-align: center; - padding: 5vh; - margin-bottom: 20px; - color: white; + .inputfile+label { + font-weight: 700; + color: black; + background-color: rgba(0, 0, 0, 0); + border: solid 3px black; + margin: 10px; + font-size: 30; + height: 70px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 30px; + width: 80%; + display: flex; + font-family: sans-serif; + text-transform: uppercase; + justify-content: center; + flex-direction: column; + border-radius: 10px; + } + + .inputfile.active+label { + font-style: italic; + color: black; + background-color: lightgreen; + border: solid 3px darkgreen; + } + + .status { + font-size: 2em; } + +} + +.image-upload { + top: 100%; + opacity: 0; +} + +.image-upload.active { + top: 0; + position: absolute; + z-index: 999; + height: 100vh; + width: 100vw; + opacity: 1; +} + +.uploadContainer { + top: 40; + position: absolute; + z-index: 1000; + height: 20vh; + width: 80vw; + opacity: 1; +} + +.closeUpload { + position: absolute; + border-radius: 10px; + top: 3; + color: black; + font-size: 30; + right: 3; + z-index: 1002; + padding: 0px 3px; + background: rgba(0, 0, 0, 0); + transition: 0.5s ease all; + border: 0px solid; +} + +.loadingImage { + display: inline-flex; + width: max-content; +} + +.loadingSlab { + position: relative; + width: 30px; + height: 30px; + margin: 10; + border-radius: 20px; + opacity: 0.2; + background-color: black; + transition: all 2s, opacity 1.5s; }
\ No newline at end of file diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index b15042f9f..c6420bab5 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -1,128 +1,180 @@ -import * as ReactDOM from 'react-dom'; import * as rp from 'request-promise'; import { Docs } from '../client/documents/Documents'; import "./ImageUpload.scss"; import React = require('react'); import { DocServer } from '../client/DocServer'; -import { Opt, Doc } from '../fields/Doc'; +import { observer } from 'mobx-react'; +import { observable, action } from 'mobx'; +import { Utils } from '../Utils'; +import { Networking } from '../client/Network'; +import { Doc, Opt } from '../fields/Doc'; import { Cast } from '../fields/Types'; import { listSpec } from '../fields/Schema'; import { List } from '../fields/List'; -import { observer } from 'mobx-react'; -import { observable } from 'mobx'; -import { Utils } from '../Utils'; -import MobileInterface from './MobileInterface'; -import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; -import { resolvedPorts } from '../client/views/Main'; +import MainViewModal from '../client/views/MainViewModal'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { MobileInterface } from './MobileInterface'; + +export interface ImageUploadProps { + Document: Doc; // Target document for upload (upload location) +} -// const onPointerDown = (e: React.TouchEvent) => { -// let imgInput = document.getElementById("input_image_file"); -// if (imgInput) { -// imgInput.click(); -// } -// } const inputRef = React.createRef<HTMLInputElement>(); @observer -class Uploader extends React.Component { +export class Uploader extends React.Component<ImageUploadProps> { @observable error: string = ""; - @observable status: string = ""; + @observable nm: string = "Choose files"; // Text of 'Choose Files' button + @observable process: string = ""; // Current status of upload onClick = async () => { - console.log("uploader click"); try { - this.status = "initializing protos"; + const col = this.props.Document; await Docs.Prototypes.initialize(); const imgPrev = document.getElementById("img_preview"); + this.setOpacity(1, "1"); // Slab 1 if (imgPrev) { const files: FileList | null = inputRef.current!.files; + this.setOpacity(2, "1"); // Slab 2 if (files && files.length !== 0) { - console.log(files[0]); - const name = files[0].name; - const formData = new FormData(); - formData.append("file", files[0]); - - const upload = window.location.origin + "/uploadFormData"; - this.status = "uploading image"; - console.log("uploading image", formData); - const res = await fetch(upload, { - method: 'POST', - body: formData - }); - this.status = "upload image, getting json"; - const json = await res.json(); - json.map(async (file: any) => { - const path = window.location.origin + file; - const doc = Docs.Create.ImageDocument(path, { _nativeWidth: 200, _width: 200, title: name }); - - this.status = "getting user document"; - - const res = await rp.get(Utils.prepend("/getUserDocumentId")); - if (!res) { - throw new Error("No user id returned"); - } - const field = await DocServer.GetRefField(res); - let pending: Opt<Doc>; - if (field instanceof Doc) { - pending = await Cast(field.rightSidebarCollection, Doc); - } - if (pending) { - this.status = "has pending docs"; - const data = await Cast(pending.data, listSpec(Doc)); - if (data) { - data.push(doc); + this.process = "Uploading Files"; + for (let index = 0; index < files.length; ++index) { + const file = files[index]; + const res = await Networking.UploadFilesToServer(file); + this.setOpacity(3, "1"); // Slab 3 + // For each item that the user has selected + res.map(async ({ result }) => { + const name = file.name; + if (result instanceof Error) { + return; + } + const path = Utils.prepend(result.accessPaths.agnostic.client); + let doc = null; + // Case 1: File is a video + if (file.type === "video/mp4") { + doc = Docs.Create.VideoDocument(path, { _nativeWidth: 400, _width: 400, title: name }); + // Case 2: File is a PDF document + } else if (file.type === "application/pdf") { + doc = Docs.Create.PdfDocument(path, { _nativeWidth: 400, _width: 400, _fitWidth: true, title: name }); + // Case 3: File is another document type (most likely Image) } else { - pending.data = new List([doc]); + doc = Docs.Create.ImageDocument(path, { _nativeWidth: 400, _width: 400, title: name }); } - this.status = "finished"; - } - }); - - // console.log(window.location.origin + file[0]) - - //imgPrev.setAttribute("src", window.location.origin + files[0].name) + this.setOpacity(4, "1"); // Slab 4 + const res = await rp.get(Utils.prepend("/getUserDocumentId")); + if (!res) { + throw new Error("No user id returned"); + } + const field = await DocServer.GetRefField(res); + let pending: Opt<Doc>; + if (field instanceof Doc) { + pending = col; + } + if (pending) { + const data = await Cast(pending.data, listSpec(Doc)); + if (data) data.push(doc); + else pending.data = new List([doc]); + this.setOpacity(5, "1"); // Slab 5 + this.process = "File " + (index + 1).toString() + " Uploaded"; + this.setOpacity(6, "1"); // Slab 6 + } + if ((index + 1) === files.length) { + this.process = "Uploads Completed"; + this.setOpacity(7, "1"); // Slab 7 + } + }); + } + // Case in which the user pressed upload and no files were selected + } else { + this.process = "No file selected"; } + // Three seconds after upload the menu will reset + setTimeout(this.clearUpload, 3000); } } catch (error) { this.error = JSON.stringify(error); } } - render() { + // Updates label after a files is selected (so user knows a file is uploaded) + inputLabel = async () => { + const files: FileList | null = inputRef.current!.files; + await files; + if (files && files.length === 1) { + this.nm = files[0].name; + } else if (files && files.length > 1) { + this.nm = files.length.toString() + " files selected"; + } + } + + // Loops through load icons, and resets buttons + @action + clearUpload = () => { + for (let i = 1; i < 8; i++) { + this.setOpacity(i, "0.2"); + } + this.nm = "Choose files"; + + if (inputRef.current) { + inputRef.current.value = ""; + } + this.process = ""; + } + + // Clears the upload and closes the upload menu + closeUpload = () => { + this.clearUpload(); + MobileInterface.Instance.toggleUpload(); + } + + // Handles the setting of the loading bar + setOpacity = (index: number, opacity: string) => { + const slab = document.getElementById("slab" + index); + if (slab) slab.style.opacity = opacity; + } + + // Returns the upload interface for mobile + private get uploadInterface() { return ( <div className="imgupload_cont"> - <label htmlFor="input_image_file" className="upload_label">Choose an Image</label> - <input type="file" accept="image/*" className="input_file" id="input_image_file" ref={inputRef}></input> - <button onClick={this.onClick} className="upload_button">Upload</button> + <div className="closeUpload" onClick={() => this.closeUpload()}> + <FontAwesomeIcon icon="window-close" size={"lg"} /> + </div> + <FontAwesomeIcon icon="upload" size="lg" style={{ fontSize: "130" }} /> + <input type="file" accept="application/pdf, video/*,image/*" className={`inputFile ${this.nm !== "Choose files" ? "active" : ""}`} id="input_image_file" ref={inputRef} onChange={this.inputLabel} multiple></input> + <label className="file" id="label" htmlFor="input_image_file">{this.nm}</label> + <div className="upload_label" onClick={this.onClick}> + Upload + </div> <img id="img_preview" src=""></img> - <p>{this.status}</p> - <p>{this.error}</p> + <div className="loadingImage"> + <div className="loadingSlab" id="slab1" /> + <div className="loadingSlab" id="slab2" /> + <div className="loadingSlab" id="slab3" /> + <div className="loadingSlab" id="slab4" /> + <div className="loadingSlab" id="slab5" /> + <div className="loadingSlab" id="slab6" /> + <div className="loadingSlab" id="slab7" /> + </div> + <p className="status">{this.process}</p> </div> ); } -} + @observable private dialogueBoxOpacity = 1; + @observable private overlayOpacity = 0.4; - -// DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "image upload"); -(async () => { - const info = await CurrentUserUtils.loadCurrentUser(); - DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, info.email + "mobile"); - await Docs.Prototypes.initialize(); - if (info.id !== "__guest__") { - // a guest will not have an id registered - await CurrentUserUtils.loadUserDocument(info); + render() { + return ( + <MainViewModal + contents={this.uploadInterface} + isDisplayed={true} + interactive={true} + dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} + overlayDisplayedOpacity={this.overlayOpacity} + closeOnExternalClick={this.closeUpload} + /> + ); } - document.getElementById('root')!.addEventListener('wheel', event => { - if (event.ctrlKey) { - event.preventDefault(); - } - }, true); - ReactDOM.render(( - // <Uploader /> - <MobileInterface /> - ), - document.getElementById('root') - ); + } -)();
\ No newline at end of file diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx index 59c73ed27..d668d134e 100644 --- a/src/mobile/MobileInkOverlay.tsx +++ b/src/mobile/MobileInkOverlay.tsx @@ -4,11 +4,9 @@ import { MobileInkOverlayContent, GestureContent, UpdateMobileInkOverlayPosition import { observable, action } from "mobx"; import { GestureUtils } from "../pen-gestures/GestureUtils"; import "./MobileInkOverlay.scss"; -import { StrCast, Cast } from '../fields/Types'; import { DragManager } from "../client/util/DragManager"; import { DocServer } from '../client/DocServer'; -import { Doc, DocListCastAsync } from '../fields/Doc'; -import { listSpec } from '../fields/Schema'; +import { Doc } from '../fields/Doc'; @observer diff --git a/src/mobile/MobileInterface.scss b/src/mobile/MobileInterface.scss index 4d86e208f..4b32c3da0 100644 --- a/src/mobile/MobileInterface.scss +++ b/src/mobile/MobileInterface.scss @@ -1,19 +1,445 @@ -.mobileInterface-inkInterfaceButtons { - position: absolute; +$navbar-height: 120px; +$pathbar-height: 50px; + +@media only screen and (max-device-width: 480px) { + * { + margin: 0px; + padding: 0px; + box-sizing: border-box; + font-family: sans-serif; + } +} + +body { + overflow: hidden; +} + +.mobileInterface-container { + height: 100%; + position: relative; + touch-action: none; + width: 100%; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +// Topbar of Dash Mobile +.navbar { + position: fixed; top: 0px; + left: 0px; + width: 100vw; + height: $navbar-height; + background-color: whitesmoke; + z-index: 150; + + .cover { + position: absolute; + right: 0px; + top: 0px; + height: 120px; + width: 120px; + background-color: whitesmoke; + z-index: 200; + } + + .toggle-btn { + position: absolute; + right: 20px; + top: 30px; + height: 70px; + width: 70px; + transition: all 400ms ease-in-out 200ms; + z-index: 180; + } + + .background { + position: absolute; + right: 0px; + top: 0px; + height: 120px; + width: 120px; + //border: 1px solid black; + } + + .background.active { + background-color: lightgrey; + } + + .toggle-btn-home { + right: -200px; + } + + .header { + position: absolute; + top: 50%; + top: calc(9px + 50%); + right: 50%; + transform: translate(50%, -50%); + font-size: 40; + font-weight: 700; + text-align: center; + user-select: none; + text-transform: uppercase; + font-family: Arial, Helvetica, sans-serif; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: ltr; + width: 600px; + } + + .toggle-btn span { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 70%; + height: 4px; + background: black; + transition: all 200ms ease; + z-index: 180; + } + + .toggle-btn span:nth-child(1) { + transition: top 200ms ease-in-out; + top: 30%; + } + + .toggle-btn span:nth-child(3) { + transition: top 200ms ease-in-out; + top: 70%; + } + + .toggle-btn.active { + transition: transform 200ms ease-in-out 200ms; + transform: rotate(135deg); + } + + .toggle-btn.active span:nth-child(1) { + top: 50%; + } + + .toggle-btn.active span:nth-child(2) { + transform: translate(-50%, -50%) rotate(90deg); + } + + .toggle-btn.active span:nth-child(3) { + top: 50%; + } +} + +.sidebar { + position: fixed; + top: 120px; + opacity: 0; + right: -100%; + width: 80%; + height: calc(80% - (120px)); + z-index: 101; + background-color: whitesmoke; + transition: all 400ms ease 50ms; + padding: 20px; + box-shadow: 0 0 5px 5px grey; + + .item { + width: 100%; + padding: 13px 12px; + border-bottom: 1px solid rgba(200, 200, 200, 0.7); + font-family: Arial, Helvetica, sans-serif; + font-style: normal; + font-weight: normal; + user-select: none; + display: inline-flex; + font-size: 35px; + text-transform: uppercase; + color: black; + } + + .ink:focus { + outline: 1px solid blue; + } + + .sidebarButtons { + top: 80px; + position: relative; + } +} + + + + + + +.blanket { + position: fixed; + top: 120px; + opacity: 0.5; + right: -100%; + width: 100%; + height: calc(100% - (120px)); + z-index: 101; + background-color: grey; + padding: 20px; +} + +.blanket.active { + position: absolute; + right: 0%; + z-index: 100; +} + +.home { + position: absolute; + top: 30px; + left: 30px; + font-size: 60; + user-select: none; + text-transform: uppercase; + font-family: Arial, Helvetica, sans-serif; + z-index: 200; +} + +.item-type { + display: inline; + text-transform: lowercase; + margin-left: 20px; + font-size: 35px; + font-style: italic; + color: rgb(28, 28, 28); +} + +.item-title { + max-width: 70%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.right { + margin-left: 20px; + z-index: 200; +} + +.open { + right: 20px; + font-size: 35; + position: absolute; +} + +.left { + width: 100%; + height: 100%; +} + + + +.sidebar.active { + position: absolute; + right: 0%; + opacity: 1; + z-index: 101; +} + +.back { + position: absolute; + left: 42px; + top: 0; + background: #1a1a1a; + width: 50px; + height: 100%; display: flex; - justify-content: space-between; + justify-content: center; + text-align: center; + flex-direction: column; + align-items: center; + border-radius: 10px; + font-size: 25px; + user-select: none; + z-index: 100; +} + +.pathbar { + position: fixed; + top: 118px; + left: 0px; + background: #1a1a1a; + z-index: 120; + border-radius: 0px; width: 100%; - z-index: 9999; - height: 50px; + height: 80px; + overflow: hidden; + + .pathname { + position: relative; + font-size: 25; + top: 50%; + width: 86%; + left: 12%; + color: whitesmoke; + transform: translate(0%, -50%); + z-index: 20; + font-family: sans-serif; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + text-align: left; + text-transform: uppercase; + } + + .scrollmenu { + overflow: auto; + width: 100%; + height: 100%; + white-space: nowrap; + display: inline-flex; + } - .mobileInterface-button { + .hidePath { + position: absolute; height: 100%; + width: 200px; + left: 0px; + top: 0px; + background-image: linear-gradient(to right, #1a1a1a, rgba(0, 0, 0, 0)); + text-align: center; + user-select: none; + z-index: 99; + pointer-events: none; + } + + .pathbarItem { + position: relative; + display: flex; + align-items: center; + color: whitesmoke; + text-align: center; + justify-content: center; + user-select: none; + transform: translate(100px, 0px); + font-size: 30px; + padding: 10px; + text-transform: uppercase; + + .pathbarText { + font-family: sans-serif; + text-align: center; + height: 50px; + padding: 10px; + font-size: 30px; + border-radius: 10px; + text-transform: uppercase; + margin-left: 20px; + position: relative; + } + + .pathIcon { + transform: translate(0px, 0px); + position: relative; + } } } -.mobileInterface-container { - height: 100%; + +/** +* docButton appears at the bottom of mobile document +* Buttons include: pin to presentation, download, upload, reload +*/ +.docButton { position: relative; - touch-action: none; + width: 100px; + display: flex; + height: 100px; + font-size: 70px; + text-align: center; + border: 3px solid black; + margin: 20px; + z-index: 100; + border-radius: 100%; + justify-content: center; + flex-direction: column; + align-items: center; +} + +.docButtonContainer { + top: 80%; + position: absolute; + display: flex; + transform: translate(-50%, 0); + left: 50%; + z-index: 100; +} + +.toolbar { + left: 50%; + transform: translate(-50%); + position: absolute; + height: max-content; + top: 0px; + border-radius: 20px; + background-color: lightgrey; + opacity: 0; + transition: all 400ms ease 50ms; +} + +.toolbar.active { + display: inline-block; + width: 300px; + padding: 5px; + opacity: 1; + height: max-content; + top: -450px; +} + +.colorSelector { + position: absolute; + top: 550px; + left: 280px; + transform: translate(-50%, 0); + z-index: 100; + display: inline-flex; + width: max-content; + height: max-content; + pointer-events: all; + font-size: 80px; + user-select: none; +} + +// Menu buttons for toggling between list and icon view +.homeSwitch { + position: fixed; + top: 212; + right: 36px; + display: inline-flex; + width: max-content; + z-index: 99; + height: 70px; + + .list { + width: 70px; + height: 70px; + margin: 5; + padding: 10; + align-items: center; + text-align: center; + font-size: 50; + border-style: solid; + border-width: 3; + border-color: black; + background: whitesmoke; + align-self: center; + border-radius: 10px; + } + + .list.active { + color: darkred; + border-color: darkred; + } }
\ No newline at end of file diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index a50ec103e..3a889b0db 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -1,47 +1,69 @@ -import React = require('react'); +import * as React from "react"; import { library } from '@fortawesome/fontawesome-svg-core'; -import { faEraser, faHighlighter, faLongArrowAltLeft, faMousePointer, faPenNib } from '@fortawesome/free-solid-svg-icons'; +import { + faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, + faTerminal, faToggleOn, faFile as fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, + faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, + faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, + faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, + faThumbtack, faTree, faTv, faBook, faUndoAlt, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faHome, faLongArrowAltLeft, faBars, faTh, faChevronLeft, + faAlignRight, faAlignLeft +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, reaction, trace, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { DocServer } from '../client/DocServer'; -import { Docs } from '../client/documents/Documents'; -import { DocumentManager } from '../client/util/DocumentManager'; -import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu'; +import { Doc, DocListCast } from '../fields/Doc'; +import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; +import { emptyFunction, emptyPath, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils'; +import { Docs, DocumentOptions } from '../client/documents/Documents'; import { Scripting } from '../client/util/Scripting'; -import { Transform } from '../client/util/Transform'; -import { DocumentDecorations } from '../client/views/DocumentDecorations'; -import GestureOverlay from '../client/views/GestureOverlay'; import { DocumentView } from '../client/views/nodes/DocumentView'; -import { RadialMenu } from '../client/views/nodes/RadialMenu'; -import { PreviewCursor } from '../client/views/PreviewCursor'; -import { Doc, DocListCast, FieldResult } from '../fields/Doc'; -import { Id } from '../fields/FieldSymbols'; +import { Transform } from '../client/util/Transform'; +import "./MobileInterface.scss"; +import "./ImageUpload.scss"; +import "./AudioUpload.scss"; +import SettingsManager from '../client/util/SettingsManager'; +import { Uploader } from "./ImageUpload"; +import { DockedFrameRenderer } from '../client/views/collections/CollectionDockingView'; import { InkTool } from '../fields/InkField'; -import { listSpec } from '../fields/Schema'; +import GestureOverlay from "../client/views/GestureOverlay"; +import { ScriptField } from "../fields/ScriptField"; +import { RadialMenu } from "../client/views/nodes/RadialMenu"; +import { UndoManager } from "../client/util/UndoManager"; +import { List } from "../fields/List"; +import { AudioUpload } from "./AudioUpload"; import { Cast, FieldValue } from '../fields/Types'; -import { WebField } from "../fields/URLField"; -import { CurrentUserUtils } from '../client/util/CurrentUserUtils'; -import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero, returnEmptyFilter } from '../Utils'; -import "./MobileInterface.scss"; -import { CollectionView } from '../client/views/collections/CollectionView'; -import { InkingStroke } from '../client/views/InkingStroke'; +import RichTextMenu from "../client/views/nodes/formattedText/RichTextMenu"; +import { AudioBox } from "../client/views/nodes/AudioBox"; +import { CollectionViewType } from "../client/views/collections/CollectionView"; +import { DocumentType } from "../client/documents/DocumentTypes"; +import { CollectionFreeFormViewChrome } from "../client/views/collections/CollectionMenu"; -library.add(faLongArrowAltLeft); +library.add(faTasks, faReply, faQuoteLeft, faHandPointLeft, faFolderOpen, faAngleDoubleLeft, faExternalLinkSquareAlt, faMobile, faThLarge, faWindowClose, faEdit, faTrashAlt, faPalette, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus, + faTerminal, faToggleOn, fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard, + faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt, + faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter, + faLongArrowAltRight, faMicrophone, faMousePointer, faMusic, faObjectGroup, faPause, faPen, faPenNib, faPhone, faPlay, faPortrait, faRedoAlt, faStamp, faStickyNote, + faThumbtack, faTree, faTv, faUndoAlt, faBook, faVideo, faAsterisk, faBrain, faImage, faPaintBrush, faTimes, faEye, faHome, faLongArrowAltLeft, faBars, faTh, faChevronLeft, + faAlignLeft, faAlignRight); @observer -export default class MobileInterface extends React.Component { - @observable static Instance: MobileInterface; - @computed private get userDoc() { return Doc.UserDoc(); } - @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; +export class MobileInterface extends React.Component { + static Instance: MobileInterface; + private _library: Doc = CurrentUserUtils.setupLibrary(Doc.UserDoc()); // to access documents in Dash Web + private _mainDoc: any = CurrentUserUtils.setupActiveMobileMenu(Doc.UserDoc()); + @observable private _sidebarActive: boolean = false; //to toggle sidebar display + @observable private _imageUploadActive: boolean = false; //to toggle image upload + @observable private _audioUploadActive: boolean = false; + @observable private _menuListView: boolean = false; //to switch between menu view (list / icon) + @observable private _ink: boolean = false; //toggle whether ink is being dispalyed + @observable private _homeMenu: boolean = true; // to determine whether currently at home menu + @observable private _child: Doc | null = null; // currently selected document + @observable private _activeDoc: Doc = this._mainDoc; // doc updated as the active mobile page is updated (initially home menu) + @observable private _homeDoc: Doc = this._mainDoc; // home menu as a document + @observable private _parents: Array<Doc> = []; // array of parent docs (for pathbar) - // private inkDoc?: Doc; - public drawingInk: boolean = false; - - // private uploadDoc?: Doc; + @computed private get mainContainer() { return Doc.UserDoc() ? FieldValue(Cast(Doc.UserDoc().activeMobile, Doc)) : CurrentUserUtils.GuestMobile; } constructor(props: Readonly<{}>) { super(props); @@ -50,298 +72,613 @@ export default class MobileInterface extends React.Component { @action componentDidMount = () => { - library.add(...[faPenNib, faHighlighter, faEraser, faMousePointer]); + // if the home menu is in list view -> adjust the menu toggle appropriately + this._menuListView = this._homeDoc._viewType === "stacking" ? true : false; + Doc.SetSelectedTool(InkTool.None); // ink should intially be set to none + Doc.UserDoc().activeMobile = this._homeDoc; // active mobile set to home + AudioBox.Enabled = true; - if (this.userDoc && !this.mainContainer) { - this.userDoc.activeMobile = this.mainDoc; - } + // remove double click to avoid mobile zoom in + document.removeEventListener("dblclick", this.onReactDoubleClick); + document.addEventListener("dblclick", this.onReactDoubleClick); } @action - switchCurrentView = (doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) => { - if (!this.userDoc) return; + componentWillUnmount = () => { + document.removeEventListener('dblclick', this.onReactDoubleClick); + } + + // Prevent zooming in when double tapping the screen + onReactDoubleClick = (e: MouseEvent) => { + e.stopPropagation(); + } - this.userDoc.activeMobile = doc(this.userDoc); - onSwitch && onSwitch(); + // Switch the mobile view to the given doc + @action + switchCurrentView = (doc: Doc, renderView?: () => JSX.Element, onSwitch?: () => void) => { + if (!Doc.UserDoc()) return; + if (this._activeDoc === this._homeDoc) { + this._parents.push(this._activeDoc); + this._homeMenu = false; + } + this._activeDoc = doc; + Doc.UserDoc().activeMobile = doc; + onSwitch?.(); - this.renderView = renderView; + // Ensures that switching to home is not registed + UndoManager.undoStack.length = 0; + UndoManager.redoStack.length = 0; } - onSwitchInking = () => { - Doc.SetSelectedTool(InkTool.Pen); - MobileInterface.Instance.drawingInk = true; + // For toggling the hamburger menu + @action + toggleSidebar = () => { + this._sidebarActive = !this._sidebarActive; - DocServer.Mobile.dispatchOverlayTrigger({ - enableOverlay: true, - width: window.innerWidth, - height: window.innerHeight - }); + if (this._ink) { + this.onSwitchInking(); + } + } + /** + * Method called when 'Library' button is pressed on the home screen + */ + switchToLibrary = async () => { + this.switchCurrentView(this._library); + runInAction(() => this._homeMenu = false); + this.toggleSidebar(); } - 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; - } - } + /** + * Back method for navigating through items + */ + @action + back = () => { + const header = document.getElementById("header") as HTMLElement; + const doc = Cast(this._parents.pop(), Doc) as Doc; // Parent document + // Case 1: Parent document is 'workspaces' + if (doc === Cast(this._library, Doc) as Doc) { + this._child = null; + this.switchCurrentView(this._library); + // Case 2: Parent document is the 'home' menu (root node) + } else if (doc === Cast(this._homeDoc, Doc) as Doc) { + this._homeMenu = true; + this._parents = []; + this._child = null; + this.switchCurrentView(this._homeDoc); + // Case 3: Parent document is any document + } else if (doc) { + this._child = doc; + this.switchCurrentView(doc); + this._homeMenu = false; + header.textContent = String(doc.title); } - DocServer.Mobile.dispatchOverlayTrigger({ - enableOverlay: true, - width: width, - height: height, - text: "Documents uploaded from mobile will show here", - }); + this._ink = false; // turns ink off } - renderDefaultContent = () => { - if (this.mainContainer) { - return <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} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} />; + /** + * Return 'Home", which implies returning to 'Home' menu buttons + */ + @action + returnHome = () => { + if (!this._homeMenu || this._sidebarActive) { + this._homeMenu = true; + this._parents = []; + this._child = null; + this.switchCurrentView(this._homeDoc); + } + if (this._sidebarActive) { + this.toggleSidebar(); } - return "hello"; } - onBack = (e: React.MouseEvent) => { - this.switchCurrentView((userDoc: Doc) => this.mainDoc); - Doc.SetSelectedTool(InkTool.None); // TODO: switch to previous tool + /** + * Return to primary Workspace in library (Workspaces Doc) + */ + @action + returnMain = () => { + this._parents = [this._homeDoc]; + this.switchCurrentView(this._library); + this._homeMenu = false; + this._child = null; + } + + /** + * Note: window.innerWidth and window.screen.width compute different values. + * window.screen.width is the display size, however window.innerWidth is the + * display resolution which computes differently. + */ + returnWidth = () => window.innerWidth; //The windows width + returnHeight = () => (window.innerHeight - 300); //Calculating the windows height (-300 to account for topbar) + whitebackground = () => "white"; + /** + * DocumentView for graphic display of all documents + */ + @computed get displayWorkspaces() { + return !this.mainContainer ? (null) : + <div style={{ position: "relative", top: '198px', height: `calc(100% - 350px)`, width: "100%", left: "0%" }}> + <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} + PanelWidth={this.returnWidth} + PanelHeight={this.returnHeight} + NativeHeight={returnZero} + NativeWidth={returnZero} + renderDepth={0} + focus={emptyFunction} + backgroundColor={this.whitebackground} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={returnEmptyFilter} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div>; + } - DocServer.Mobile.dispatchOverlayTrigger({ - enableOverlay: false, - width: window.innerWidth, - height: window.innerHeight + /** + * Handles the click functionality in the library panel. + * Navigates to the given doc and updates the sidebar. + * @param doc: doc for which the method is called + */ + handleClick = async (doc: Doc) => { + runInAction(() => { + if (doc.type !== "collection" && this._sidebarActive) { + this._parents.push(this._activeDoc); + this.switchCurrentView(doc); + this._homeMenu = false; + this.toggleSidebar(); + } + else { + this._parents.push(this._activeDoc); + this.switchCurrentView(doc); + this._homeMenu = false; + this._child = doc; + } }); + } - // this.inkDoc = undefined; - this.drawingInk = false; + /** + * Called when an item in the library is clicked and should + * be opened (open icon on RHS of all menu items) + * @param doc doc to be opened + */ + @action + openFromSidebar = (doc: Doc) => { + this._parents.push(this._activeDoc); + this.switchCurrentView(doc); + this._homeMenu = false; + this._child = doc; + this.toggleSidebar(); } - shiftLeft = (e: React.MouseEvent) => { - DocServer.Mobile.dispatchOverlayPositionUpdate({ - dx: -10 - }); - e.preventDefault(); - e.stopPropagation(); + // Renders the graphical pathbar + renderPathbar = () => { + const docPath = [...this._parents, this._activeDoc]; + const items = docPath.map((doc: Doc, index: any) => + <div className="pathbarItem" key={index}> + {index === 0 ? (null) : <FontAwesomeIcon key="icon" className="pathIcon" icon="angle-right" size="lg" />} + <div className="pathbarText" + style={{ backgroundColor: this._homeMenu || doc === this._activeDoc ? "rgb(119,17,37)" : undefined }} + onClick={() => this.handlePathClick(doc, index)}>{doc.title} + </div> + </div>); + return (<div className="pathbar"> + <div className="scrollmenu"> + {items} + </div> + {!this._parents.length ? (null) : + <div className="back" > + <FontAwesomeIcon onClick={this.back} icon={"chevron-left"} color="white" size={"2x"} /> + </div>} + <div className="hidePath" /> + </div>); } - shiftRight = (e: React.MouseEvent) => { - DocServer.Mobile.dispatchOverlayPositionUpdate({ - dx: 10 - }); - e.preventDefault(); - e.stopPropagation(); + // Handles when user clicks on a document in the pathbar + @action + handlePathClick = (doc: Doc, index: number) => { + if (doc === this._library) { + this._child = null; + this.switchCurrentView(doc); + this._parents.length = index; + } else if (doc === this._homeDoc) { + this.returnHome(); + } else { + this._child = doc; + this.switchCurrentView(doc); + this._parents.length = index; + } } - 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) { + // Renders the contents of the menu and sidebar + @computed get renderDefaultContent() { + if (this._homeMenu) { 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 className="navbar"> + <FontAwesomeIcon className="home" icon="home" onClick={this.returnHome} /> + <div className="header" id="header">{this._homeDoc.title}</div> + <div className="cover" id="cover" onClick={e => e.stopPropagation()}></div> + <div className="toggle-btn" id="menuButton" onClick={this.toggleSidebar}> + <span></span> + <span></span> + <span></span> </div> </div> - <CollectionView - Document={this.mainContainer} - DataDoc={undefined} - LibraryPath={emptyPath} - filterAddDocument={returnTrue} - 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} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - rootSelected={returnTrue}> - </CollectionView> + {this.renderPathbar()} </div> ); } + // stores workspace documents as 'workspaces' variable + let workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc) as Doc; + if (this._child) { + workspaces = this._child; + } + // returns a list of navbar buttons as 'buttons' + const buttons = DocListCast(workspaces.data).map((doc: Doc, index: any) => { + if (doc.type !== "ink") { + return ( + <div + className="item" + key={index}> + <div className="item-title" onClick={() => this.handleClick(doc)}> {doc.title} </div> + <div className="item-type" onClick={() => this.handleClick(doc)}>{doc.type}</div> + <FontAwesomeIcon onClick={() => this.handleClick(doc)} className="right" icon="angle-right" size="lg" style={{ display: `${doc.type === "collection" ? "block" : "none"}` }} /> + <FontAwesomeIcon className="open" onClick={() => this.openFromSidebar(doc)} icon="external-link-alt" size="lg" /> + </div> + ); + } + }); + + return ( + <div> + <div className="navbar"> + <FontAwesomeIcon className="home" icon="home" onClick={this.returnHome} /> + <div className="header" id="header">{this._sidebarActive ? "library" : this._activeDoc.title}</div> + <div className={`toggle-btn ${this._sidebarActive ? "active" : ""}`} onClick={this.toggleSidebar}> + <span></span> + <span></span> + <span></span> + </div> + <div className={`background ${this._sidebarActive ? "active" : ""}`} onClick={this.toggleSidebar}></div> + </div> + {this.renderPathbar()} + <div className={`sidebar ${this._sidebarActive ? "active" : ""}`}> + <div className="sidebarButtons"> + {this._child ? + <> + {buttons} + <div + className="item" key="home" + onClick={this.returnMain} + style={{ opacity: 0.7 }}> + <FontAwesomeIcon className="right" icon="angle-double-left" size="lg" /> + <div className="item-type">Return to workspaces</div> + </div> + </> : + <> + {buttons} + <div + className="item" + style={{ opacity: 0.7 }} + onClick={() => this.createNewWorkspace()}> + <FontAwesomeIcon className="right" icon="plus" size="lg" /> + <div className="item-type">Create New Workspace</div> + </div> + </> + } + </div> + </div> + <div className={`blanket ${this._sidebarActive ? "active" : ""}`} onClick={this.toggleSidebar}> + </div> + </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(); + /** + * Handles the 'Create New Workspace' button in the menu (taken from MainView.tsx) + */ + @action + createNewWorkspace = async (id?: string) => { + const workspaces = Cast(Doc.UserDoc().myWorkspaces, Doc) as Doc; + const workspaceCount = DocListCast(workspaces.data).length + 1; + const freeformOptions: DocumentOptions = { + x: 0, + y: 400, + title: "Collection " + workspaceCount, + }; + const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); + const workspaceDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600, path: [Doc.UserDoc().myCatalog as Doc] }], { title: `Workspace ${workspaceCount}` }, id, "row"); + + const toggleTheme = ScriptField.MakeScript(`self.darkScheme = !self.darkScheme`); + const toggleComic = ScriptField.MakeScript(`toggleComicMode()`); + const cloneWorkspace = ScriptField.MakeScript(`cloneWorkspace()`); + workspaceDoc.contextMenuScripts = new List<ScriptField>([toggleTheme!, toggleComic!, cloneWorkspace!]); + workspaceDoc.contextMenuLabels = new List<string>(["Toggle Theme Colors", "Toggle Comic Mode", "New Workspace Layout"]); + + Doc.AddDocToList(workspaces, "data", workspaceDoc); } - 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"; + // Button for switching between pen and ink mode + @action + onSwitchInking = () => { + const button = document.getElementById("inkButton") as HTMLElement; + button.style.backgroundColor = this._ink ? "white" : "black"; + button.style.color = this._ink ? "black" : "white"; - } - } - 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>(); - } + if (!this._ink) { + Doc.SetSelectedTool(InkTool.Pen); + this._ink = true; + } else { + Doc.SetSelectedTool(InkTool.None); + this._ink = false; } } - renderUploadContent() { - if (this.mainContainer) { + // The static ink menu that appears at the top + @computed get inkMenu() { + return this._activeDoc._viewType !== CollectionViewType.Docking || !this._ink ? (null) : + <div className="colorSelector"> + {/* <CollectionFreeFormViewChrome /> */} + </div>; + } + + // DocButton that uses UndoManager and handles the opacity change if CanUndo is true + @computed get undo() { + if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && + this._activeDoc !== Doc.UserDoc().rightSidebarCollection && this._activeDoc.title !== "WORKSPACES") { 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} - docFilters={returnEmptyFilter} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} /> + <div className="docButton" + style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanUndo() ? "1" : "0.4", }} + id="undoButton" + title="undo" + onClick={(e: React.MouseEvent) => { + UndoManager.Undo(); + e.stopPropagation(); + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="undo-alt" /> + </div>); + } else return (null); + } + + // DocButton that uses UndoManager and handles the opacity change if CanRedo is true + @computed get redo() { + if (this.mainContainer && this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && + this._activeDoc !== Doc.UserDoc().rightSidebarCollection && this._activeDoc.title !== "WORKSPACES") { + return ( + <div className="docButton" + style={{ backgroundColor: "black", color: "white", fontSize: "60", opacity: UndoManager.CanRedo() ? "1" : "0.4", }} + id="undoButton" + title="redo" + onClick={(e: React.MouseEvent) => { + UndoManager.Redo(); + e.stopPropagation(); + }}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="redo-alt" /> + </div>); + } else return (null); + } + + // DocButton for switching into ink mode + @computed get drawInk() { + return !this.mainContainer || this._activeDoc._viewType !== CollectionViewType.Docking ? (null) : + <div className="docButton" + id="inkButton" + title={Doc.isDocPinned(this._activeDoc) ? "Pen on" : "Pen off"} + onClick={this.onSwitchInking}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="pen-nib" /> + </div>; + } + + // DocButton: Button that appears on the bottom of the screen to initiate image upload + @computed get uploadImageButton() { + if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking && this._activeDoc.title !== "WORKSPACES") { + return <div className="docButton" + id="imageButton" + title={Doc.isDocPinned(this._activeDoc) ? "Pen on" : "Pen off"} + onClick={this.toggleUpload}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="upload" /> + </div>; + } else return (null); + } + + // DocButton to download images on the mobile + @computed get downloadDocument() { + if (this._activeDoc.type === "image" || this._activeDoc.type === "pdf" || this._activeDoc.type === "video") { + return <div className="docButton" + title={"Download Image"} + style={{ backgroundColor: "white", color: "black" }} + onClick={e => window.open(this._activeDoc["data-path"]?.toString())}> {/* daa-path holds the url */} + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="download" /> + </div>; + } else return (null); + } + + // DocButton for pinning images to presentation + @computed get pinToPresentation() { + // Only making button available if it is an image + if (!(this._activeDoc.type === "collection" || this._activeDoc.type === "presentation")) { + const isPinned = this._activeDoc && Doc.isDocPinned(this._activeDoc); + return <div className="docButton" + title={Doc.isDocPinned(this._activeDoc) ? "Unpin from presentation" : "Pin to presentation"} + style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }} + onClick={e => DockedFrameRenderer.PinDoc(this._activeDoc, isPinned)}> + <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" /> + </div>; + } else return (null); + } + + // Buttons for switching the menu between large and small icons + @computed get switchMenuView() { + return this._activeDoc.title !== this._homeDoc.title ? (null) : + <div className="homeSwitch"> + <div className={`list ${!this._menuListView ? "active" : ""}`} onClick={this.changeToIconView}> + <FontAwesomeIcon size="sm" icon="th-large" /> </div> - ); + <div className={`list ${this._menuListView ? "active" : ""}`} onClick={this.changeToListView}> + <FontAwesomeIcon size="sm" icon="bars" /> + </div> + </div>; + } + + // Logic for switching the menu into the icons + @action + changeToIconView = () => { + if (this._homeDoc._viewType = "stacking") { + this._menuListView = false; + this._homeDoc._viewType = "masonry"; + this._homeDoc.columnWidth = 300; + this._homeDoc._columnWidth = 300; + const menuButtons = DocListCast(this._homeDoc.data); + menuButtons.map(doc => { + const buttonData = DocListCast(doc.data); + buttonData[1]._nativeWidth = 0.1; + buttonData[1]._width = 0.1; + buttonData[1]._dimMagnitude = 0; + buttonData[1]._opacity = 0; + doc._nativeWidth = 400; + }); } } + // Logic for switching the menu into the stacking view + @action + changeToListView = () => { + if (this._homeDoc._viewType = "masonry") { + this._homeDoc._viewType = "stacking"; + this._menuListView = true; + const menuButtons = DocListCast(this._homeDoc.data); + menuButtons.map(doc => { + const buttonData = DocListCast(doc.data); + buttonData[1]._nativeWidth = 450; + buttonData[1]._dimMagnitude = 2; + buttonData[1]._opacity = 1; + doc._nativeWidth = 900; + }); + } + } + + // For setting up the presentation document for the home menu + @action + setupDefaultPresentation = () => { + const presentation = Cast(Doc.UserDoc().activePresentation, Doc) as Doc; + + if (presentation) { + this.switchCurrentView(presentation); + this._homeMenu = false; + } + } + + // For toggling image upload pop up + @action + toggleUpload = () => this._imageUploadActive = !this._imageUploadActive + + // For toggling audio record and dictate pop up + @action + toggleAudio = () => this._audioUploadActive = !this._audioUploadActive + + // Button for toggling the upload pop up in a collection + @action + toggleUploadInCollection = () => { + const button = document.getElementById("imageButton") as HTMLElement; + button.style.backgroundColor = this._imageUploadActive ? "white" : "black"; + button.style.color = this._imageUploadActive ? "black" : "white"; + + this._imageUploadActive = !this._imageUploadActive; + } + + // For closing the image upload pop up + @action + closeUpload = () => { + this._imageUploadActive = false; + } + + // Returns the image upload pop up + @computed get uploadImage() { + const doc = !this._homeMenu ? this._activeDoc : Cast(Doc.UserDoc().rightSidebarCollection, Doc) as Doc; + return <Uploader Document={doc} />; + } + + // Radial menu can only be used if it is a colleciton and it is not a homeDoc + // (and cannot be used on Workspace to avoid pin to presentation opening on right) + @computed get displayRadialMenu() { + return this._activeDoc.type === "collection" && this._activeDoc !== this._homeDoc && + this._activeDoc._viewType !== CollectionViewType.Docking ? <RadialMenu /> : (null); + } + onDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); } + /** + * MENU BUTTON + * Switch view from mobile menu to access the mobile uploads + * Global function name: openMobileUploads() + */ + @action + switchToMobileUploads = () => { + const mobileUpload = Cast(Doc.UserDoc().rightSidebarCollection, Doc) as Doc; + this.switchCurrentView(mobileUpload); + this._homeMenu = false; + } + render() { - // const content = this.currentView === "main" ? this.mainContent : - // this.currentView === "ink" ? this.inkContent : - // this.currentView === "upload" ? this.uploadContent : <></>; return ( <div className="mobileInterface-container" onDragOver={this.onDragOver}> - {/* <DocumentDecorations /> - <GestureOverlay> - {this.renderView ? this.renderView() : this.renderDefaultContent()} - </GestureOverlay> */} - - {/* <DictationOverlay /> - <SharingManager /> - <GoogleAuthenticationManager /> */} - <DocumentDecorations /> + <SettingsManager /> + <div className={`image-upload ${this._imageUploadActive ? "active" : ""}`}> + {this.uploadImage} + </div> + <div className={`audio-upload ${this._audioUploadActive ? "active" : ""}`}> + <AudioUpload /> + </div> + {this.switchMenuView} + {this.inkMenu} <GestureOverlay> - {this.renderView ? this.renderView() : this.renderDefaultContent()} + <div style={{ display: "none" }}><RichTextMenu key="rich" /></div> + <div className="docButtonContainer"> + {this.pinToPresentation} + {this.downloadDocument} + {this.undo} + {this.redo} + {this.drawInk} + {this.uploadImageButton} + </div> + {this.displayWorkspaces} + {this.renderDefaultContent} </GestureOverlay> - <PreviewCursor /> - {/* <ContextMenu /> */} - <RadialMenu /> - <RichTextMenu /> - {/* <PDFMenu /> - <MarqueeOptionsMenu /> - <OverlayView /> */} + {this.displayRadialMenu} </div> ); } } -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(); }); +//Global functions for mobile menu +Scripting.addGlobal(function switchToMobileLibrary() { return MobileInterface.Instance.switchToLibrary(); }, + "opens the library to navigate through workspaces on Dash Mobile"); +Scripting.addGlobal(function openMobileUploads() { return MobileInterface.Instance.toggleUpload(); }, + "opens the upload files menu for Dash Mobile"); +Scripting.addGlobal(function switchToMobileUploadCollection() { return MobileInterface.Instance.switchToMobileUploads(); }, + "opens the mobile uploads collection on Dash Mobile"); +Scripting.addGlobal(function openMobileAudio() { return MobileInterface.Instance.toggleAudio(); }, + "opens the record and dictate menu on Dash Mobile"); +Scripting.addGlobal(function switchToMobilePresentation() { return MobileInterface.Instance.setupDefaultPresentation(); }, + "opens the presentation on Dash Mobile"); +Scripting.addGlobal(function openMobileSettings() { return SettingsManager.Instance.open(); }, + "opens settings on Dash Mobile"); + +// Other global functions for mobile +Scripting.addGlobal(function switchMobileView(doc: Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); }, + "changes the active document displayed on the Dash Mobile", "(doc: any)"); diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx new file mode 100644 index 000000000..3d4680d58 --- /dev/null +++ b/src/mobile/MobileMain.tsx @@ -0,0 +1,25 @@ +import { MobileInterface } from "./MobileInterface"; +import { Docs } from "../client/documents/Documents"; +import { CurrentUserUtils } from "../client/util/CurrentUserUtils"; +import * as ReactDOM from 'react-dom'; +import * as React from 'react'; +import { DocServer } from "../client/DocServer"; +import { AssignAllExtensions } from "../extensions/General/Extensions"; + +AssignAllExtensions(); + +(async () => { + const info = await CurrentUserUtils.loadCurrentUser(); + DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + " (mobile)"); + await Docs.Prototypes.initialize(); + if (info.id !== "__guest__") { + // a guest will not have an id registered + await CurrentUserUtils.loadUserDocument(info); + } + document.getElementById('root')!.addEventListener('wheel', event => { + if (event.ctrlKey) { + event.preventDefault(); + } + }, true); + ReactDOM.render(<MobileInterface />, document.getElementById('root')); +})();
\ No newline at end of file diff --git a/src/mobile/MobileMenu.scss b/src/mobile/MobileMenu.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mobile/MobileMenu.scss |