From 78d9ea53fcf13a42445f2b2cd9b8a76fef0fdfea Mon Sep 17 00:00:00 2001 From: yipstanley Date: Sat, 28 Sep 2019 15:26:27 -0400 Subject: sidebar now flies out --- src/client/views/MainView.tsx | 65 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 10 deletions(-) (limited to 'src/client/views/MainView.tsx') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 244b217ed..afc3612f6 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons'; +import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -195,6 +195,7 @@ export class MainView extends React.Component { library.add(faArrowUp); library.add(faCloudUploadAlt); library.add(faBolt); + library.add(faChevronRight); this.initEventListeners(); this.initAuthenticationRouters(); } @@ -311,12 +312,14 @@ export class MainView extends React.Component { } @observable flyoutWidth: number = 250; + @observable flyoutTranslate: boolean = true; @computed get dockingContent() { let flyoutWidth = this.flyoutWidth; + let countWidth = this.flyoutTranslate; let mainCont = this.mainContainer; return {({ measureRef }) => -
+
{!mainCont ? (null) : { + if (this.flyoutWidth === 0) { + this.flyoutWidth = 250; + this.flyoutTranslate = false; + } + } + + @action + pointerLeaveDragger = () => { + if (!this.flyoutTranslate) { + this.flyoutWidth = 0; + this.flyoutTranslate = true; + } + } + @action onPointerMove = (e: PointerEvent) => { this.flyoutWidth = Math.max(e.clientX, 0); @@ -411,18 +431,34 @@ export class MainView extends React.Component { getScale={returnOne}> ; } + @computed get mainContent() { let sidebar = CurrentUserUtils.UserDocument.sidebar; if (!(sidebar instanceof Doc)) return (null); return
-
- -
-
- {this.flyout} +
+
+ +
+
+ {this.flyout} + {this.expandButton} +
{this.dockingContent}
; @@ -478,7 +514,7 @@ export class MainView extends React.Component { ]; if (!ClientUtils.RELEASE) btns.unshift([React.createRef(), "cat", "Add Cat Image", addImageNode]); - return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} > + return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} > @@ -569,6 +605,15 @@ export class MainView extends React.Component { return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : ; } + @computed get expandButton() { + return !this.flyoutTranslate ? (
{ + runInAction(() => { + this.flyoutWidth = 250; + this.flyoutTranslate = true; + }); + }}>
) : (null); + } + render() { return (
-- cgit v1.2.3-70-g09d2 From 2d3deb7291b7d98acf71566a67c4d648a90ef5af Mon Sep 17 00:00:00 2001 From: yipstanley Date: Sat, 28 Sep 2019 16:00:44 -0400 Subject: search bar filtering updatesA --- src/client/views/MainView.tsx | 3 ++- src/client/views/search/SearchBox.scss | 9 +++++++++ src/client/views/search/SearchBox.tsx | 11 ++++++++--- 3 files changed, 19 insertions(+), 4 deletions(-) (limited to 'src/client/views/MainView.tsx') diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index afc3612f6..ffbba3a20 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -196,6 +196,7 @@ export class MainView extends React.Component { library.add(faCloudUploadAlt); library.add(faBolt); library.add(faChevronRight); + library.add(faEllipsisV); this.initEventListeners(); this.initAuthenticationRouters(); } diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 5ed33a596..0dd4d3dc5 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -34,6 +34,9 @@ &.searchBox-filter { align-self: stretch; + } + + &.searchBox-submit { margin-left: 2px; margin-right: 2px } @@ -45,6 +48,12 @@ } } +.searchBox-quickFilter { + width: 500px; + margin-left: 25px; + margin-top: 10px; +} + .searchBox-results { margin-right: 136px; top: 300px; diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 0d50124dd..1b8177842 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -18,6 +18,7 @@ import { FilterBox } from './FilterBox'; import "./FilterBox.scss"; import "./SearchBox.scss"; import { SearchItem } from './SearchItem'; +import { IconBar } from './IconBar'; library.add(faTimes); @@ -231,7 +232,7 @@ export class SearchBox extends React.Component { } @action.bound - openSearch(e: React.PointerEvent) { + openSearch(e: React.SyntheticEvent) { e.stopPropagation(); this._openNoResults = false; FilterBox.Instance.closeFilter(); @@ -337,12 +338,16 @@ export class SearchBox extends React.Component { + -
+ {(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) : + (
+
+
)}
Date: Fri, 4 Oct 2019 01:50:57 -0400 Subject: got rid of MainOverlayTextBox! fixed some things with embedding docs in text --- src/client/util/RichTextSchema.tsx | 4 +- src/client/views/InkingControl.tsx | 3 +- src/client/views/MainOverlayTextBox.scss | 29 ---- src/client/views/MainOverlayTextBox.tsx | 156 --------------------- src/client/views/MainView.tsx | 5 +- .../collectionFreeForm/CollectionFreeFormView.scss | 6 - .../collectionFreeForm/MarqueeView.scss | 7 + .../collections/collectionFreeForm/MarqueeView.tsx | 4 +- src/client/views/nodes/FormattedTextBox.scss | 7 +- src/client/views/nodes/FormattedTextBox.tsx | 52 ++----- 10 files changed, 31 insertions(+), 242 deletions(-) delete mode 100644 src/client/views/MainOverlayTextBox.scss delete mode 100644 src/client/views/MainOverlayTextBox.tsx (limited to 'src/client/views/MainView.tsx') diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 7be773475..12cf40b32 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -720,8 +720,8 @@ export class DashDocView { this._outer.style.overflow = "hidden"; (this._outer.style as any).float = node.attrs.float; - this._dashSpan.style.width = "100%"; - this._dashSpan.style.height = "100%"; + this._dashSpan.style.width = node.attrs.width; + this._dashSpan.style.height = node.attrs.height; this._dashSpan.style.position = "absolute"; this._dashSpan.style.display = "inline-block" this._handle.style.position = "absolute"; diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index a10df0e75..ee8b77050 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -10,7 +10,6 @@ import { InkTool } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; import { undoBatch, UndoManager } from "../util/UndoManager"; import { StrCast, NumCast, Cast } from "../../new_fields/Types"; -import { MainOverlayTextBox } from "./MainOverlayTextBox"; import { listSpec } from "../../new_fields/Schema"; import { List } from "../../new_fields/List"; import { Utils } from "../../Utils"; @@ -46,7 +45,7 @@ export class InkingControl extends React.Component { switchColor = action((color: ColorResult): void => { this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff"); if (InkingControl.Instance.selectedTool === InkTool.None) { - if (MainOverlayTextBox.Instance.SetColor(color.hex)) return; + // if (MainOverlayTextBox.Instance.SetColor(color.hex)) return; let selected = SelectionManager.SelectedDocuments(); let oldColors = selected.map(view => { let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document); diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss deleted file mode 100644 index c9d44e194..000000000 --- a/src/client/views/MainOverlayTextBox.scss +++ /dev/null @@ -1,29 +0,0 @@ -@import "globalCssVariables"; - -.mainOverlayTextBox-textInput { - background-color: rgba(248, 6, 6, 0.001); - width: 400px; - height: 200px; - position: absolute; - overflow: visible; - top: 0; - left: 0; - pointer-events: none; - z-index: $mainTextInput-zindex; - - .formattedTextBox-cont { - background-color: rgba(248, 6, 6, 0.001); - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - } -} - -.mainOverlayTextBox-unscaled_div { - // width: 0px; - z-index: 10000; - position: absolute; - pointer-events: none; -} \ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx deleted file mode 100644 index 73eef9478..000000000 --- a/src/client/views/MainOverlayTextBox.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { action, observable, reaction, trace } from 'mobx'; -import { observer } from 'mobx-react'; -import "normalize.css"; -import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; -import { BoolCast } from '../../new_fields/Types'; -import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils'; -import { DragManager } from '../util/DragManager'; -import { Transform } from '../util/Transform'; -import { CollectionDockingView } from './collections/CollectionDockingView'; -import "./MainOverlayTextBox.scss"; -import { FormattedTextBox } from './nodes/FormattedTextBox'; - -interface MainOverlayTextBoxProps { - firstinstance?: boolean; -} - -@observer -export class MainOverlayTextBox extends React.Component { - public static Instance: MainOverlayTextBox; - @observable _textXf: () => Transform = () => Transform.Identity(); - public TextFieldKey: string = "data"; - private _textColor: string | null = null; - private _textHideOnLeave?: boolean; - private _textTargetDiv: HTMLDivElement | undefined; - private _textProxyDiv: React.RefObject; - private _textBottom: boolean | undefined; - private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); }; - private _outerdiv: HTMLElement | null = null; - private _textBox: FormattedTextBox | undefined; - private _tooltip?: HTMLElement; - ChromeHeight?: () => number; - @observable public TextDoc?: Doc; - @observable public TextDataDoc?: Doc; - - updateTooltip = () => { - this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip); - } - - public SetColor(color: string) { - return this._textBox && this._textBox.setFontColor(color); - } - - constructor(props: MainOverlayTextBoxProps) { - super(props); - this._textProxyDiv = React.createRef(); - MainOverlayTextBox.Instance = this; - reaction(() => FormattedTextBox.InputBoxOverlay, - (box?: FormattedTextBox) => { - this._textBox = box; - if (box) { - this.ChromeHeight = box.props.ChromeHeight; - this.TextDoc = box.props.Document; - this.TextDataDoc = box.props.DataDoc; - let xf = () => { - box.props.ScreenToLocalTransform(); - let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined); - return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); - }; - this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight) || box.props.height === "min-content"); - } - else { - this.TextDoc = undefined; - this.TextDataDoc = undefined; - this.setTextDoc(); - } - }); - } - - @action - private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) { - if (this._textTargetDiv) { - this._textTargetDiv.style.color = this._textColor; - } - this.TextFieldKey = textFieldKey!; - let txf = tx ? tx : () => Transform.Identity(); - this._textXf = txf; - this._textTargetDiv = div; - this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave; - if (div) { - let parTop = div.parentElement && getComputedStyle(div.parentElement).top; - this._textBottom = parTop && parTop !== "0px" && parTop != "auto" ? true : false; - this._textColor = (getComputedStyle(div) as any).color; - div.style.color = "transparent"; - } - } - - @action - textScroll = (e: React.UIEvent) => { - if (this._textProxyDiv.current && this._textTargetDiv) { - this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop; - } - } - - textBoxDown = (e: React.PointerEvent) => { - if (e.button !== 0 || e.metaKey || e.altKey) { - document.addEventListener("pointermove", this.textBoxMove); - document.addEventListener('pointerup', this.textBoxUp); - } - } - @action - textBoxMove = (e: PointerEvent) => { - if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) { - document.removeEventListener("pointermove", this.textBoxMove); - document.removeEventListener('pointerup', this.textBoxUp); - let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document]); - const [left, top] = this._textXf().inverse().transformPoint(0, 0); - dragData.offset = [e.clientX - left, e.clientY - top]; - DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, { - handlers: { - dragComplete: action(emptyFunction), - }, - hideSource: false - }); - } - } - textBoxUp = (e: PointerEvent) => { - document.removeEventListener("pointermove", this.textBoxMove); - document.removeEventListener('pointerup', this.textBoxUp); - } - - addDocTab = (doc: Doc, dataDoc: Opt, location: string) => { - return this._textBox && this._textBox.props.addDocTab(doc, dataDoc, location) ? true : false; - } - render() { - this.TextDoc; this.TextDataDoc; - if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) { - let wid = FormattedTextBox.InputBoxOverlay.props.Document.width; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations) - let hgtx = FormattedTextBox.InputBoxOverlay.props.Document.height; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations) - let textRect = this._textTargetDiv.getBoundingClientRect(); - let s = this._textXf().Scale; - let location = this._textBottom ? textRect.bottom : textRect.top; - let hgt = (this._textBox && this._textBox.props.Document.autoHeight) || this._textBottom ? "auto" : this._textTargetDiv.clientHeight; - return
-
-
-
- { this._tooltip = tooltip; this.updateTooltip(); }} /> -
-
-
- ; - } - else return (null); - } -} \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 660b42290..545f99a41 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -34,14 +34,12 @@ import { DocumentDecorations } from './DocumentDecorations'; import KeyManager from './GlobalKeyHandler'; import { InkingControl } from './InkingControl'; import "./Main.scss"; -import { MainOverlayTextBox } from './MainOverlayTextBox'; import MainViewModal from './MainViewModal'; import { DocumentView } from './nodes/DocumentView'; -import { PresBox } from './nodes/PresBox'; -import { OverlayView } from './OverlayView'; import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; +import { OverlayView } from './OverlayView'; @observer export class MainView extends React.Component { @@ -687,7 +685,6 @@ export class MainView extends React.Component { {this.nodesMenu()} {this.miscButtons} -
); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index c4311fa52..bb1a12f88 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -46,12 +46,6 @@ border-radius: inherit; box-sizing: border-box; position: absolute; - overflow: hidden; - - .marqueeView { - overflow: hidden; - } - top: 0; left: 0; width: 100%; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 9fc2e44fb..7dc54ea79 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -6,6 +6,13 @@ width:100%; height:100%; } +.marqueeView { + overflow: hidden; +} + +.marqueeView:focus-within { + overflow: visible; +} .marquee { border-style: dashed; box-sizing: border-box; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 82193aefa..bf154d37d 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -442,8 +442,8 @@ export class MarqueeView extends React.Component render() { let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0]; - return
-
+ return
e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> +
e.currentTarget.scrollLeft = 0} > {this._visible ? this.marqueeDiv : null}
{this.props.children} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 45e516015..541c29faa 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -28,7 +28,7 @@ } .formattedTextBox-cont-hidden { - pointer-events: none; + // pointer-events: none; } .formattedTextBox-inner-rounded { @@ -40,9 +40,10 @@ left: 10%; } -.formattedTextBox-inner-rounded div, -.formattedTextBox-inner div { +.formattedTextBox-inner-rounded , +.formattedTextBox-inner { padding: 10px 10px; + height: 100%; } .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 819accf20..13eb78f48 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -46,7 +46,6 @@ library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); export interface FormattedTextBoxProps { - isOverlay?: boolean; hideOnLeave?: boolean; height?: string; color?: string; @@ -95,9 +94,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @observable private _fontFamily = "Arial"; @observable private _fontAlign = ""; @observable private _entered = false; - @observable public static InputBoxOverlay?: FormattedTextBox = undefined; public static SelectOnLoad = ""; - public static InputBoxOverlayScroll: number = 0; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; } @@ -137,9 +134,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe constructor(props: FieldViewProps) { super(props); - if (this.props.isOverlay) { - DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); - } FormattedTextBox.Instance = this; } @@ -300,8 +294,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe node = model.create({ src: url, docid: link[Id] }) } else { node = schema.nodes.dashDoc.create({ - width: this.props.Document[WidthSym](), height: this.props.Document[HeightSym](), - title: "dashDoc", docid: target[Id] + width: target[WidthSym](), height: target[HeightSym](), + title: "dashDoc", docid: target[Id], + float: "none" }); } let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y }); @@ -390,7 +385,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe (schema as any).Document = this.props.Document; return { schema, - plugins: this.props.isOverlay ? [ + plugins: [ inputRules(inpRules), this.tooltipTextMenuPlugin(), history(), @@ -403,26 +398,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }), formattedTextBoxCommentPlugin - ] : [ - history(), - keymap(this._keymap), - keymap(baseKeymap), - ] + ] }; } componentDidMount() { - - if (!this.props.isOverlay) { - this._proxyReactionDisposer = reaction(() => this.props.isSelected(), - () => { - if (this.props.isSelected()) { - FormattedTextBox.InputBoxOverlay = this; - FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop; - } - }, { fireImmediately: true }); - } - this.pullFromGoogleDoc(this.checkState); this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); @@ -551,7 +531,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let editor = this._editorView; let ret = findLinkFrag(editor.state.doc.content, editor); - if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) { + if (ret.frag.size > 2) { let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected @@ -754,7 +734,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - (this._editorView as any).isOverlay = this.props.isOverlay; if (startup) { Doc.GetProto(doc).documentText = undefined; this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); @@ -766,7 +745,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe FormattedTextBox.SelectOnLoad = ""; this.props.select(false); } - else if (this.props.isOverlay) this._editorView!.focus(); + this._editorView!.focus(); // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })]; } @@ -824,13 +803,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe document.removeEventListener("keypress", this.recordKeyHandler); document.addEventListener("keypress", this.recordKeyHandler); this.tryUpdateHeight(); - if (!this.props.isOverlay) { - FormattedTextBox.InputBoxOverlay = this; - } else { - if (this._ref.current) { - this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll; - } - } } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time @@ -901,7 +873,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } } - this._proseRef!.focus(); + let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + if (pos && pos.pos > 0) { + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new TextSelection(this._editorView!.state.doc.resolve(pos.pos)))); + } + this._editorView!.focus(); if (this._linkClicked) { this._linkClicked = ""; e.preventDefault(); @@ -959,7 +935,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe tryUpdateHeight() { const ChromeHeight = this.props.ChromeHeight; let sh = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) { + if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) { let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0); let dh = NumCast(this.props.Document.height, 0); this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0)); @@ -968,7 +944,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } render() { - let style = this.props.isOverlay ? "scroll" : "hidden"; + let style = "hidden"; let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : ""; let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "none" : "all"; -- cgit v1.2.3-70-g09d2 From d611773fc805082a935cae49723d516ce66e1a14 Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Fri, 4 Oct 2019 23:45:01 -0400 Subject: more pdf cleanup. fix to mix-multiply-mode for better highlighters/opacity. small text box fixes. --- src/client/util/DocumentManager.ts | 2 +- src/client/util/RichTextSchema.tsx | 26 ++- src/client/util/SelectionManager.ts | 2 - src/client/views/DocumentButtonBar.tsx | 1 - src/client/views/DocumentDecorations.tsx | 1 - src/client/views/MainView.tsx | 3 +- .../collectionFreeForm/MarqueeView.scss | 2 +- src/client/views/nodes/Annotation.tsx | 117 ------------ src/client/views/nodes/FormattedTextBox.tsx | 7 +- src/client/views/pdf/Annotation.scss | 3 +- src/client/views/pdf/Annotation.tsx | 4 +- src/client/views/pdf/PDFMenu.tsx | 10 +- src/client/views/pdf/PDFViewer.scss | 6 +- src/client/views/pdf/PDFViewer.tsx | 198 +++++++++------------ src/new_fields/Doc.ts | 2 +- .../authentication/models/current_user_utils.ts | 2 +- 16 files changed, 117 insertions(+), 269 deletions(-) delete mode 100644 src/client/views/nodes/Annotation.tsx (limited to 'src/client/views/MainView.tsx') diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index ffd311665..6fe97edbb 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -139,7 +139,7 @@ export class DocumentManager { const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc); finalDocView && (finalDocView.Document.scrollToLinkID = linkId); finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document); - } + }; const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc); const annotatedDoc = await Cast(targetDoc.annotationOn, Doc); if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight? diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 53eaf9ce2..528c0000b 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -654,17 +654,17 @@ export class ImageResizeView { this._img.onclick = function (e: any) { e.stopPropagation(); e.preventDefault(); - if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) - view.dispatch( - view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2)))); - } + if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) { + view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2)))); + } + }; this._img.onpointerdown = function (e: any) { if (e.ctrlKey) { e.preventDefault(); e.stopPropagation(); DocServer.GetRefField(node.attrs.docid).then(async linkDoc => (linkDoc instanceof Doc) && - DocumentManager.Instance.FollowLink(linkDoc, (view.state.schema as any).Document, + DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document, document => addDocTab(document, undefined, node.attrs.location ? node.attrs.location : "inTab"), false)); } }; @@ -730,7 +730,7 @@ export class DashDocView { this._dashSpan.style.width = node.attrs.width; this._dashSpan.style.height = node.attrs.height; this._dashSpan.style.position = "absolute"; - this._dashSpan.style.display = "inline-block" + this._dashSpan.style.display = "inline-block"; this._handle.style.position = "absolute"; this._handle.style.width = "20px"; this._handle.style.height = "20px"; @@ -771,16 +771,10 @@ export class DashDocView { this._dashSpan.onclick = function (e: any) { FormattedTextBox.firstTarget && FormattedTextBox.firstTarget(); e.stopPropagation(); - } - this._dashSpan.onkeydown = function (e: any) { - e.stopPropagation(); - } - this._dashSpan.onkeypress = function (e: any) { - e.stopPropagation(); - } - this._dashSpan.onkeyup = function (e: any) { - e.stopPropagation(); - } + }; + this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); }; + this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); }; + this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); }; this._handle.onpointerdown = function (e: any) { e.preventDefault(); e.stopPropagation(); diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index a02a270ee..df1b46b33 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -27,7 +27,6 @@ export namespace SelectionManager { } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) { manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false)); manager.SelectedDocuments = [docView]; - FormattedTextBox.InputBoxOverlay = undefined; } } @action @@ -42,7 +41,6 @@ export namespace SelectionManager { DeselectAll(): void { manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false)); manager.SelectedDocuments = []; - FormattedTextBox.InputBoxOverlay = undefined; } } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index e57745b86..9e2d41621 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -140,7 +140,6 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[], let selDoc = this.props.views[0]; let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined; let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); - FormattedTextBox.InputBoxOverlay = undefined; this._linkDrag = UndoManager.StartBatch("Drag Link"); DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { handlers: { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 26ffaf3a6..9acb77ce2 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -466,7 +466,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> break; } - if (!this._resizing) runInAction(() => FormattedTextBox.InputBoxOverlay = undefined); SelectionManager.SelectedDocuments().forEach(element => { if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { let doc = PositionDocument(element.props.Document); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 545f99a41..3b0457dff 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,6 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons'; -import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 7dc54ea79..04f6ec2ad 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -11,7 +11,7 @@ } .marqueeView:focus-within { - overflow: visible; + overflow: hidden; } .marquee { border-style: dashed; diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx deleted file mode 100644 index 3e4ed6bf1..000000000 --- a/src/client/views/nodes/Annotation.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import "./ImageBox.scss"; -import React = require("react"); -import { observer } from "mobx-react"; -import { observable, action } from 'mobx'; -import 'react-pdf/dist/Page/AnnotationLayer.css'; - -interface IProps { - Span: HTMLSpanElement; - X: number; - Y: number; - Highlights: any[]; - Annotations: any[]; - CurrAnno: any[]; - -} - -/** - * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color - * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). - * Also need to support multiline highlighting - * - * Written by: Andrew Kim - */ -@observer -export class Annotation extends React.Component { - - /** - * changes color of the span (highlighted section) - */ - onColorChange = (e: React.PointerEvent) => { - if (e.currentTarget.innerHTML === "r") { - this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)"; - } else if (e.currentTarget.innerHTML === "b") { - this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)"; - } else if (e.currentTarget.innerHTML === "y") { - this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)"; - } else if (e.currentTarget.innerHTML === "g") { - this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)"; - } - - } - - /** - * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this - */ - @action - onRemove = (e: any) => { - let index: number = -1; - //finding the highlight in the highlight array - this.props.Highlights.forEach((e) => { - for (const span of e.spans) { - if (span === this.props.Span) { - index = this.props.Highlights.indexOf(e); - this.props.Highlights.splice(index, 1); - } - } - }); - - //removing from CurrAnno and Annotation array - this.props.Annotations.splice(index, 1); - this.props.CurrAnno.pop(); - - //removing span from div - if (this.props.Span.parentElement) { - let nodesArray = this.props.Span.parentElement.childNodes; - nodesArray.forEach((e) => { - if (e === this.props.Span) { - if (this.props.Span.parentElement) { - this.props.Highlights.forEach((item) => { - if (item === e) { - item.remove(); - } - }); - e.remove(); - } - } - }); - } - - - } - - render() { - return ( -
-
- -
- - - - -
- -
-
- -
-
- - ); - } -} \ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 05904e1e7..2b6a86aed 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -291,7 +291,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (de.data.urlField && link) { let url: string = de.data.urlField.url.href; let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image; - node = model.create({ src: url, docid: link[Id] }) + node = model.create({ src: url, docid: link[Id] }); } else { node = schema.nodes.dashDoc.create({ width: target[WidthSym](), height: target[HeightSym](), @@ -798,7 +798,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._editorView!.focus(); } } - } + }; } onPointerUp = (e: React.PointerEvent): void => { @@ -807,9 +807,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.stopPropagation(); } } - + static InputBoxOverlay: any = null; @action onFocused = (e: React.FocusEvent): void => { + FormattedTextBox.InputBoxOverlay = this; document.removeEventListener("keypress", this.recordKeyHandler); document.addEventListener("keypress", this.recordKeyHandler); this.tryUpdateHeight(); diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss index 0c6df74f0..cc326eb93 100644 --- a/src/client/views/pdf/Annotation.scss +++ b/src/client/views/pdf/Annotation.scss @@ -2,6 +2,5 @@ pointer-events: all; user-select: none; position: absolute; - background-color: red; - opacity: 0.1; + background-color: rgba(146, 245, 95, 0.467); } \ No newline at end of file diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx index 134e757d1..4bb166ffe 100644 --- a/src/client/views/pdf/Annotation.tsx +++ b/src/client/views/pdf/Annotation.tsx @@ -122,9 +122,9 @@ class RegionAnnotation extends React.Component { left: this.props.x, width: this.props.width, height: this.props.height, - transition: "opacity 0.5s", opacity: this._brushed ? 0.5 : undefined, - backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.color) + backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor), + transition: "opacity 0.5s", }} />); } } \ No newline at end of file diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx index e62542014..1e3320069 100644 --- a/src/client/views/pdf/PDFMenu.tsx +++ b/src/client/views/pdf/PDFMenu.tsx @@ -4,7 +4,7 @@ import { observable, action, } from "mobx"; import { observer } from "mobx-react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { emptyFunction, returnFalse } from "../../../Utils"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, Opt } from "../../../new_fields/Doc"; @observer export default class PDFMenu extends React.Component { @@ -31,7 +31,7 @@ export default class PDFMenu extends React.Component { @observable public Pinned: boolean = false; public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction; - public Highlight: (color: string) => void = emptyFunction; + public Highlight: (color: string) => Opt = (color: string) => undefined; public Delete: () => void = emptyFunction; public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction; public AddTag: (key: string, value: string) => boolean = returnFalse; @@ -155,12 +155,8 @@ export default class PDFMenu extends React.Component { @action highlightClicked = (e: React.MouseEvent) => { - if (!this.Pinned) { - this.Highlight("#f4f442"); - } - else { + if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { this.Highlighting = !this.Highlighting; - this.Highlight("#f4f442"); } } diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index a71e4f81e..c77cee792 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -16,6 +16,7 @@ mix-blend-mode: multiply; opacity: 0.9; } + .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation() .textLayer .highlight { background-color: yellow; } @@ -51,10 +52,9 @@ pointer-events: none; mix-blend-mode: multiply; - .pdfPage-annotationBox { + .pdfViewer-annotationBox { position: absolute; - background-color: red; - opacity: 0.1; + background-color: rgba(245, 230, 95, 0.616); } } .pdfViewer-waiting { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 1b76ddbdc..4516e9904 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -75,8 +75,9 @@ export class PDFViewer extends React.Component { @observable private _showWaiting = true; @observable private _showCover = false; @observable private _zoomed = 1; + @observable private _scrollTop = 0; - public pdfViewer: any; + private _pdfViewer: any; private _retries = 0; // number of times tried to create the PDF viewer private _isChildActive = false; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void); @@ -149,15 +150,16 @@ export class PDFViewer extends React.Component { copy = (e: ClipboardEvent) => { if (this.props.active() && e.clipboardData) { - e.clipboardData.setData("text/plain", this._selectionText); - e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); - e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument("#0390fc")[Id]); + let annoDoc = this.makeAnnotationDocument("#0390fc"); + if (annoDoc) { + e.clipboardData.setData("text/plain", this._selectionText); + e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]); + e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]); + } e.preventDefault(); } } - setSelectionText = (text: string) => this._selectionText = text; - @action initialLoad = async () => { if (this._pageSizes.length === 0) { @@ -215,7 +217,7 @@ export class PDFViewer extends React.Component { document.removeEventListener("copy", this.copy); document.addEventListener("copy", this.copy); document.addEventListener("pagesinit", action(() => { - this.pdfViewer.currentScaleValue = this._zoomed = 1; + this._pdfViewer.currentScaleValue = this._zoomed = 1; this.gotoPage(NumCast(this.props.Document.curPage, 1)); })); document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false)); @@ -223,36 +225,35 @@ export class PDFViewer extends React.Component { let pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService, }); - this.pdfViewer = new PDFJSViewer.PDFViewer({ + this._pdfViewer = new PDFJSViewer.PDFViewer({ container: this._mainCont.current, viewer: this._viewer.current, linkService: pdfLinkService, findController: pdfFindController, renderer: "canvas", }); - pdfLinkService.setViewer(this.pdfViewer); + pdfLinkService.setViewer(this._pdfViewer); pdfLinkService.setDocument(this.props.pdf, null); - this.pdfViewer.setDocument(this.props.pdf); + this._pdfViewer.setDocument(this.props.pdf); } @action - makeAnnotationDocument = (color: string): Doc => { + makeAnnotationDocument = (color: string): Opt => { + if (this._savedAnnotations.size() === 0) return undefined; let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); let annoDocs: Doc[] = []; let minY = Number.MAX_VALUE; - if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1) { + if ((this._savedAnnotations.values()[0][0] as any).marqueeing) { let anno = this._savedAnnotations.values()[0][0]; - let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) }); + let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + StrCast(this.props.Document.title) }); if (anno.style.left) annoDoc.x = parseInt(anno.style.left); if (anno.style.top) annoDoc.y = parseInt(anno.style.top); if (anno.style.height) annoDoc.height = parseInt(anno.style.height); if (anno.style.width) annoDoc.width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; - annoDoc.color = color; - annoDoc.type = AnnotationTypes.Region; - annoDocs.push(annoDoc); annoDoc.isButton = true; + annoDocs.push(annoDoc); anno.remove(); mainAnnoDoc = annoDoc; mainAnnoDocProto = Doc.GetProto(mainAnnoDoc); @@ -265,8 +266,7 @@ export class PDFViewer extends React.Component { if (anno.style.height) annoDoc.height = parseInt(anno.style.height); if (anno.style.width) annoDoc.width = parseInt(anno.style.width); annoDoc.group = mainAnnoDoc; - annoDoc.color = color; - annoDoc.type = AnnotationTypes.Region; + annoDoc.backgroundColor = color; annoDocs.push(annoDoc); anno.remove(); (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY)); @@ -307,7 +307,7 @@ export class PDFViewer extends React.Component { @action gotoPage = (p: number) => { - this.pdfViewer && this.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); + this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) }); } @action @@ -317,25 +317,11 @@ export class PDFViewer extends React.Component { Doc.linkFollowHighlight(scrollToAnnotation); } - sendAnnotations = (page: number) => { - return this._savedAnnotations.getValue(page); - } - - receiveAnnotations = (annotations: HTMLDivElement[], page: number) => { - if (page === -1) { - this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); - this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations)); - } - else { - this._savedAnnotations.setValue(page, annotations); - } - } - @observable scrollTop = 0; @action onScroll = (e: React.UIEvent) => { - this.scrollTop = this._mainCont.current!.scrollTop; - this.pdfViewer && (this.props.Document.curPage = this.pdfViewer.currentPageNumber); + this._scrollTop = this._mainCont.current!.scrollTop; + this._pdfViewer && (this.props.Document.curPage = this._pdfViewer.currentPageNumber); } // get the page index that the vertical offset passed in is on @@ -355,6 +341,8 @@ export class PDFViewer extends React.Component { div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString(); } this._annotationLayer.current.append(div); + div.style.backgroundColor = "yellow"; + div.style.opacity = "0.5"; let savedPage = this._savedAnnotations.getValue(page); if (savedPage) { savedPage.push(div); @@ -371,8 +359,8 @@ export class PDFViewer extends React.Component { if (!searchString) { fwd ? this.nextAnnotation() : this.prevAnnotation(); } - else if (this.pdfViewer._pageViewsReady) { - this.pdfViewer.findController.executeCommand('findagain', { + else if (this._pdfViewer._pageViewsReady) { + this._pdfViewer.findController.executeCommand('findagain', { caseSensitive: false, findPrevious: !fwd, highlightAll: true, @@ -382,7 +370,7 @@ export class PDFViewer extends React.Component { } else if (this._mainCont.current) { let executeFind = () => { - this.pdfViewer.findController.executeCommand('find', { + this._pdfViewer.findController.executeCommand('find', { caseSensitive: false, findPrevious: !fwd, highlightAll: true, @@ -406,30 +394,24 @@ export class PDFViewer extends React.Component { } this._marqueeing = false; if (!e.altKey && e.button === 0 && this.active()) { + // clear out old marquees and initialize menu for new selection PDFMenu.Instance.StartDrag = this.startDrag; PDFMenu.Instance.Highlight = this.highlight; PDFMenu.Instance.Snippet = this.createSnippet; PDFMenu.Instance.Status = "pdf"; PDFMenu.Instance.fadeOut(true); + this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); + this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, [])); if (e.target && (e.target as any).parentElement.className === "textLayer") { - if (!e.ctrlKey) { - this.receiveAnnotations([], -1); - } + // start selecting text if mouse down on textLayer spans } - else { + else if (this._mainCont.current) { // set marquee x and y positions to the spatially transformed position - if (this._mainCont.current) { - let boundingRect = this._mainCont.current.getBoundingClientRect(); - this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); - this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; - } + let boundingRect = this._mainCont.current.getBoundingClientRect(); + this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width); + this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop; + this._marqueeHeight = this._marqueeWidth = 0; this._marqueeing = true; - let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox"); - if (marquees && marquees.length) { // make a copy of the marquee - let marquee = marquees[0] as HTMLDivElement; - marquee.style.opacity = "0.2"; - } - this.receiveAnnotations([], -1); } document.removeEventListener("pointermove", this.onSelectMove); document.addEventListener("pointermove", this.onSelectMove); @@ -464,13 +446,13 @@ export class PDFViewer extends React.Component { let clientRects = selRange.getClientRects(); for (let i = 0; i < clientRects.length; i++) { let rect = clientRects.item(i); - if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) { + if (rect) { let scaleY = this._mainCont.current.offsetHeight / boundingRect.height; let scaleX = this._mainCont.current.offsetWidth / boundingRect.width; if (rect.width !== this._mainCont.current.clientWidth && - (i == 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) { + (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) { let annoBox = document.createElement("div"); - annoBox.className = "pdfPage-annotationBox"; + annoBox.className = "pdfViewer-annotationBox"; // transforms the positions from screen onto the pdf div annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString(); annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString(); @@ -481,8 +463,7 @@ export class PDFViewer extends React.Component { } } } - let text = selRange.cloneContents().textContent; - text && this.setSelectionText(text); + this._selectionText = selRange.cloneContents().textContent || ""; // clear selection if (sel.empty) { // Chrome @@ -494,22 +475,22 @@ export class PDFViewer extends React.Component { @action onSelectEnd = (e: PointerEvent): void => { + this._savedAnnotations.clear(); if (this._marqueeing) { if (this._marqueeWidth > 10 || this._marqueeHeight > 10) { let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox"); - if (marquees && marquees.length) { // make a copy of the marquee + if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation. + let style = (marquees[0] as HTMLDivElement).style; let copy = document.createElement("div"); - let marquee = marquees[0] as HTMLDivElement; - let style = marquee.style; copy.style.left = style.left; copy.style.top = style.top; copy.style.width = style.width; copy.style.height = style.height; copy.style.border = style.border; copy.style.opacity = style.opacity; - copy.className = "pdfPage-annotationBox"; + (copy as any).marqueeing = true; + copy.className = "pdfViewer-annotationBox"; this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY)); - marquee.style.opacity = "0"; } if (!e.ctrlKey) { @@ -518,8 +499,7 @@ export class PDFViewer extends React.Component { } PDFMenu.Instance.jumpTo(e.clientX, e.clientY); } - - this._marqueeHeight = this._marqueeWidth = 0; + this._marqueeing = false; } else { let sel = window.getSelection(); @@ -531,7 +511,7 @@ export class PDFViewer extends React.Component { } if (PDFMenu.Instance.Highlighting) { - this.highlight("goldenrod"); + this.highlight("rgba(245, 230, 95, 0.616)"); // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up } else { PDFMenu.Instance.StartDrag = this.startDrag; @@ -545,7 +525,7 @@ export class PDFViewer extends React.Component { highlight = (color: string) => { // creates annotation documents for current highlights let annotationDoc = this.makeAnnotationDocument(color); - Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc); + annotationDoc && Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc); return annotationDoc; } @@ -558,16 +538,18 @@ export class PDFViewer extends React.Component { e.preventDefault(); e.stopPropagation(); let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title }); - let annotationDoc = this.highlight("red"); - let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); - DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { - handlers: { - dragComplete: () => !(dragData as any).linkedToDoc && - DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF") - - }, - hideSource: false - }); + const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); + if (annotationDoc) { + let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc); + DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, { + handlers: { + dragComplete: () => !(dragData as any).linkedToDoc && + DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF") + + }, + hideSource: false + }); + } } createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => { @@ -609,7 +591,7 @@ export class PDFViewer extends React.Component { return true; } scrollXf = () => { - return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this.scrollTop) : this.props.ScreenToLocalTransform(); + return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform(); } setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => { this._setPreviewCursor = func; @@ -647,9 +629,9 @@ export class PDFViewer extends React.Component { onZoomWheel = (e: React.WheelEvent) => { e.stopPropagation(); if (e.ctrlKey) { - let curScale = Number(this.pdfViewer.currentScaleValue); - this.pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000)); - this._zoomed = Number(this.pdfViewer.currentScaleValue); + let curScale = Number(this._pdfViewer.currentScaleValue); + this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000)); + this._zoomed = Number(this._pdfViewer.currentScaleValue); } } @@ -657,29 +639,7 @@ export class PDFViewer extends React.Component { return
{this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) => )} -
; - } - @computed get pdfViewerDiv() { - return
; - } - @computed get standinViews() { - return <> - {this._showCover ? this.getCoverImage() : (null)} - {this._showWaiting ? : (null)} - ; - } - marqueeWidth = () => this._marqueeWidth; - marqueeHeight = () => this._marqueeHeight; - marqueeX = () => this._marqueeX; - marqueeY = () => this._marqueeY; - marqueeing = () => this._marqueeing; - visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; - render() { - return (
- {this.pdfViewerDiv} - - {this.annotationLayer} -
+
NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))} @@ -702,7 +662,29 @@ export class PDFViewer extends React.Component { chromeCollapsed={true}>
+
; + } + @computed get pdfViewerDiv() { + return
; + } + @computed get standinViews() { + return <> + {this._showCover ? this.getCoverImage() : (null)} + {this._showWaiting ? : (null)} + ; + } + marqueeWidth = () => this._marqueeWidth; + marqueeHeight = () => this._marqueeHeight; + marqueeX = () => this._marqueeX; + marqueeY = () => this._marqueeY; + marqueeing = () => this._marqueeing; + visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96; + render() { + return (
+ {this.pdfViewerDiv} + {this.annotationLayer} {this.standinViews} +
); } } @@ -722,11 +704,9 @@ class PdfViewerMarquee extends React.Component { style={{ left: `${this.props.x()}px`, top: `${this.props.y()}px`, width: `${this.props.width()}px`, height: `${this.props.height()}px`, - border: `${this.props.width() === 0 ? "" : "2px dashed black"}` + border: `${this.props.width() === 0 ? "" : "2px dashed black"}`, + opacity: 0.2 }}>
; } -} - - -export enum AnnotationTypes { Region } +} \ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 6acc6e1ca..7e37eba84 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -685,7 +685,7 @@ export namespace Doc { document.removeEventListener("pointerdown", linkFollowUnhighlight); document.addEventListener("pointerdown", linkFollowUnhighlight); let x = dt = Date.now(); - window.setTimeout(() => dt == x && linkFollowUnhighlight(), 5000); + window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000); } export class HighlightBrush { diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index b2509a4f1..0fbfbf2f3 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -112,7 +112,7 @@ export class CurrentUserUtils { if (sidebar) { sidebar.backgroundColor = "lightgrey"; } - }) + }); if (doc.overlays === undefined) { const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" }); -- cgit v1.2.3-70-g09d2 From 45b97084b3d49d521ff39963f250e9cd9efe3f8e Mon Sep 17 00:00:00 2001 From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> Date: Wed, 9 Oct 2019 05:00:23 -0400 Subject: client side google api authentication UI --- src/client/apis/AuthenticationManager.tsx | 90 ++++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 43 ++++++++--- src/client/views/MainView.tsx | 2 + src/server/DashUploadUtils.ts | 2 +- src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 79 +++++++++---------- src/server/apis/google/GooglePhotosUploadUtils.ts | 4 + src/server/index.ts | 31 ++++++-- 8 files changed, 194 insertions(+), 60 deletions(-) create mode 100644 src/client/apis/AuthenticationManager.tsx (limited to 'src/client/views/MainView.tsx') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx new file mode 100644 index 000000000..75a50d8f9 --- /dev/null +++ b/src/client/apis/AuthenticationManager.tsx @@ -0,0 +1,90 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; + +@observer +export default class AuthenticationManager extends React.Component<{}> { + public static Instance: AuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + + private get isOpen() { + return this.openState; + } + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private get hasBeenClicked() { + return this.clickedState; + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public executeFullRoutine = async (authenticationLink: string) => { + this.authenticationLink = authenticationLink; + this.isOpen = true; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGooglePhotosAccessToken, { authenticationCode }).then(token => { + this.isOpen = false; + this.hasBeenClicked = false; + resolve(token); + disposer(); + }); + } + } + ); + }); + } + + constructor(props: {}) { + super(props); + AuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + this.hasBeenClicked = true; + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }) + + private get renderPrompt() { + return ( +
+ + {this.clickedState ? : (null)} +
+ ) + } + + render() { + return ( + + ); + } + +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 29cc042b6..dd1492f51 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -13,14 +13,20 @@ import { Docs, DocumentOptions } from "../../documents/Documents"; import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; import { Identified } from "../../Network"; +import AuthenticationManager from "../AuthenticationManager"; +import { List } from "../../../new_fields/List"; export namespace GooglePhotos { + const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + const endpoint = async () => { - const accessToken = await Identified.FetchFromServer(RouteStore.googlePhotosAccessToken); - return new Photos(accessToken); + let response = await Identified.FetchFromServer(RouteStore.readGooglePhotosAccessToken); + if (new RegExp(AuthenticationUrl).test(response)) { + response = await AuthenticationManager.Instance.executeFullRoutine(response); + } + return new Photos(response); }; export enum MediaType { @@ -89,9 +95,14 @@ export namespace GooglePhotos { } const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); const { id, productUrl } = await Create.Album(resolved); - const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey); - if (newMediaItemResults) { - const mediaItems = newMediaItemResults.map(item => item.mediaItem); + const response = await Transactions.UploadImages(images, { id }, descriptionKey); + if (response) { + const { results, failed } = response; + let index: Opt; + while ((index = failed.pop()) !== undefined) { + Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]); + } + const mediaItems: MediaItem[] = results.map(item => item.mediaItem); if (mediaItems.length !== images.length) { throw new AssertionError({ actual: mediaItems.length, expected: images.length }); } @@ -99,6 +110,9 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; + if (!mediaItem) { + continue; + } image.googlePhotosId = mediaItem.id; image.googlePhotosAlbumUrl = productUrl; image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; @@ -304,17 +318,22 @@ export namespace GooglePhotos { }; export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { - const newMediaItems = await UploadImages(sources, album, descriptionKey); - if (!newMediaItems) { + const response = await UploadImages(sources, album, descriptionKey); + if (!response) { return undefined; } - const baseUrls: string[] = await Promise.all(newMediaItems.map(item => { + const baseUrls: string[] = await Promise.all(response.results.map(item => { return new Promise(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); })); return baseUrls; }; - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + export interface ImageUploadResults { + results: NewMediaItemResult[]; + failed: number[]; + } + + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { if (album && "title" in album) { album = await Create.Album(album.title); } @@ -331,8 +350,8 @@ export namespace GooglePhotos { media.push({ url, description }); } if (media.length) { - const uploads: NewMediaItemResult[] = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); - return uploads; + const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + return results; } }; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3b0457dff..12578e5b8 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,6 +39,7 @@ import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { OverlayView } from './OverlayView'; +import AuthenticationManager from '../apis/AuthenticationManager'; @observer export class MainView extends React.Component { @@ -677,6 +678,7 @@ export class MainView extends React.Component {
{this.dictationOverlay} + {this.mainContent} diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 4230e9b17..57b46714a 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -22,7 +22,7 @@ export namespace DashUploadUtils { const gifs = [".gif"]; const pngs = [".png"]; const jpgs = [".jpg", ".jpeg"]; - const imageFormats = [...pngs, ...jpgs, ...gifs]; + export const imageFormats = [...pngs, ...jpgs, ...gifs]; const videoFormats = [".mov", ".mp4"]; const size = "content-length"; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index ee9cd8a0e..23fdbc53d 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,7 +32,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotosAccessToken = "/googlePhotosAccessToken", + readGooglePhotosAccessToken = "/readGooglePhotosAccessToken", + writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c899c2ef2..2f29cb95f 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -75,6 +75,42 @@ export namespace GoogleApiServerUtils { }); }; + const RetrieveOAuthClient = async (information: CredentialInformation) => { + return new Promise((resolve, reject) => { + readFile(information.credentialsPath, async (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed; + resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0])); + }); + }); + } + + export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { + const client = await RetrieveOAuthClient(information); + return client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES.map(relative => prefix + relative), + }); + }; + + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + const oAuth2Client = await RetrieveOAuthClient(information); + return new Promise((resolve, reject) => { + oAuth2Client.getToken(authenticationCode, async (err, token) => { + if (err || !token) { + reject(err); + return console.error('Error retrieving access token', err); + } + oAuth2Client.setCredentials(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token); + resolve({ token, client: oAuth2Client }); + }); + }); + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { @@ -107,17 +143,13 @@ export namespace GoogleApiServerUtils { return new Promise((resolve, reject) => { // Attempting to authorize user (${userId}) Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { - if (!token) { - // No token registered, so awaiting input from user - return getNewToken(oAuth2Client, userId).then(resolve, reject); - } - if (token.expiry_date! < new Date().getTime()) { + if (token!.expiry_date! < new Date().getTime()) { // Token has expired, so submitting a request for a refreshed access token - return refreshToken(token, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); + return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); } // Authentication successful! - oAuth2Client.setCredentials(token); - resolve({ token, client: oAuth2Client }); + oAuth2Client.setCredentials(token!); + resolve({ token: token!, client: oAuth2Client }); }); }); } @@ -145,35 +177,4 @@ export namespace GoogleApiServerUtils { }); }; - /** - * Get and store new token after prompting for user authorization, and then - * execute the given callback with the authorized OAuth2 client. - * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for. - * @param {getEventsCallback} callback The callback for the authorized client. - */ - function getNewToken(oAuth2Client: OAuth2Client, userId: string) { - return new Promise((resolve, reject) => { - const authUrl = oAuth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES.map(relative => prefix + relative), - }); - console.log('Authorize this app by visiting this url:', authUrl); - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - rl.question('Enter the code from that page here: ', (code) => { - rl.close(); - oAuth2Client.getToken(code, async (err, token) => { - if (err || !token) { - reject(err); - return console.error('Error retrieving access token', err); - } - oAuth2Client.setCredentials(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token); - resolve({ token, client: oAuth2Client }); - }); - }); - }); - } } \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 16c4f6c3a..4a67e57cc 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { MediaItemCreationResult } from './SharedTypes'; import { NewMediaItem } from "../../index"; import { BatchedArray, TimeUnit } from 'array-batcher'; +import { DashUploadUtils } from '../../DashUploadUtils'; export namespace GooglePhotosUploadUtils { @@ -32,6 +33,9 @@ export namespace GooglePhotosUploadUtils { }; export const DispatchGooglePhotosUpload = async (url: string) => { + if (!DashUploadUtils.imageFormats.includes(path.extname(url))) { + return undefined; + } const body = await request(url, { encoding: null }); const parameters = { method: 'POST', diff --git a/src/server/index.ts b/src/server/index.ts index 690836fff..077002894 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -862,7 +862,22 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.header("userId")! }).then(token => res.send(token))); +app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + const information = { credentialsPath, userId }; + if (!token) { + return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + } + GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); +}); + +app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const information = { credentialsPath, userId }; + const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); + res.send(token.access_token); +}); const tokenError = "Unable to successfully upload bytes for all images!"; const mediaError = "Unable to convert all uploaded bytes to media items!"; @@ -885,16 +900,17 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); - let failed = 0; + let failed: number[] = []; const newMediaItems = await BatchedArray.from(media, { batchSize: 25 }).batchedMapPatientInterval( { magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: GooglePhotosUploadUtils.MediaInput[]) => { const newMediaItems: NewMediaItem[] = []; - for (let element of batch) { + for (let index = 0; index < batch.length; index++) { + const element = batch[index]; const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); if (!uploadToken) { - failed++; + failed.push(index); } else { newMediaItems.push({ description: element.description, @@ -906,12 +922,13 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { } ); - if (failed) { - return _error(res, tokenError); + const failedCount = failed.length; + if (failedCount) { + console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`) } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - result => _success(res, result.newMediaItemResults), + result => _success(res, { results: result.newMediaItemResults, failed }), error => _error(res, mediaError, error) ); }); -- cgit v1.2.3-70-g09d2