diff options
Diffstat (limited to 'src/client/views')
-rw-r--r-- | src/client/views/DocumentDecorations.scss | 31 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 7 | ||||
-rw-r--r-- | src/client/views/InkingControl.tsx | 3 | ||||
-rw-r--r-- | src/client/views/Main.tsx | 20 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 21 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 13 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 13 | ||||
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 94 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.scss | 108 | ||||
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 20 |
11 files changed, 223 insertions, 109 deletions
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index ac8497bd0..470365627 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -266,6 +266,31 @@ $linkGap : 3px; } } -@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } } -@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } } -@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file +@-moz-keyframes spin { + 100% { + -moz-transform: rotate(360deg); + } +} + +@-webkit-keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes shadow-pulse { + 0% { + box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2); + } + + 100% { + box-shadow: 0 0 0 35px rgba(0, 0, 0, 0); + } +}
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 814d718be..97876204e 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -83,6 +83,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable public pullIcon: IconProp = "arrow-alt-circle-down"; @observable public pullColor: string = "white"; @observable public isAnimatingFetch = false; + @observable public isAnimatingPulse = false; @observable public openHover = false; public pullColorAnimating = false; @@ -103,6 +104,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> public startPushOutcome = action((success: boolean) => { if (!this.pushAnimating) { this.pushAnimating = true; + this.isAnimatingPulse = false; this.pushIcon = success ? "check-circle" : "stop-circle"; setTimeout(() => runInAction(() => { this.pushIcon = "arrow-alt-circle-up"; @@ -725,9 +727,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> return ( <div className={"linkButtonWrapper"}> <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => { + if (!published) { + runInAction(() => this.isAnimatingPulse = true); + } DocumentDecorations.hasPushedHack = false; this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1; - }}> + }} style={{ animation: this.isAnimatingPulse ? "shadow-pulse 1s infinite" : "none" }}> <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} /> </div> </div> diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index aa573f16b..147418400 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -67,8 +67,9 @@ export class InkingControl extends React.Component { cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")"; view.props.ContainingCollectionView.props.Document.colorPalette = new List(cp); targetDoc.backgroundColor = cp[closest]; - } else + } else { targetDoc.backgroundColor = this._selectedColor; + } if (view.props.Document.heading) { let cv = view.props.ContainingCollectionView; let ruleProvider = cv && (Cast(cv.props.Document.ruleProvider, Doc) as Doc); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 11ec6f0c9..e35ba18e4 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -8,6 +8,26 @@ import { Doc, DocListCastAsync } from "../../new_fields/Doc"; import { List } from "../../new_fields/List"; import { DocServer } from "../DocServer"; +String.prototype.removeTrailingNewlines = function () { + let sliced = this; + while (sliced.endsWith("\n")) { + sliced = sliced.substring(0, this.length - 1); + } + return sliced as string; +}; + +String.prototype.hasNewline = function () { + return this.endsWith("\n"); +}; + +(Array.prototype as any).lastElement = function (this: any[]) { + if (!this.length) { + return undefined; + } + return this[this.length - 1]; +}; + + let swapDocs = async () => { let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc); // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b64986084..28edf181b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema'; import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types'; import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils'; +import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils'; import { DocServer } from '../DocServer'; import { Docs } from '../documents/Documents'; import { ClientUtils } from '../util/ClientUtils'; @@ -40,6 +40,8 @@ import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; +import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../new_fields/URLField'; import { LinkFollowBox } from './linking/LinkFollowBox'; import { DocumentManager } from '../util/DocumentManager'; @@ -128,6 +130,8 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); + this.executeGooglePhotosRoutine(); + reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; let recent = CurrentUserUtils.UserDocument.recentlyClosed; @@ -145,6 +149,16 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } + executeGooglePhotosRoutine = async () => { + // let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + // let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }); + // doc.caption = "Well isn't this a nice cat image!"; + // let photos = await GooglePhotos.endpoint(); + // let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id; + // console.log(await GooglePhotos.UploadImages([doc], { id: albumId })); + GooglePhotos.Query.Search({ included: [GooglePhotos.ContentCategories.ANIMALS] }).then(console.log); + } + componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); //close presentation @@ -457,12 +471,15 @@ export class MainView extends React.Component { // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw"; // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" })); - let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ + // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] }); + + let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode], [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode], [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument], [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode], + // [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch], [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher], ]; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 99e5ab7b3..5fc4f36a7 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -253,7 +253,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName }; - let path = Utils.prepend(file); + let path = Utils.prepend(file.path); Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); })); }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2591bdd8d..3377775db 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -99,7 +99,7 @@ export namespace PivotView { groups.forEach((val, key) => minSize = Math.min(minSize, val.length)); const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize)); - const fontSize = NumCast(collection.pivotFontSize); + const fontSize = NumCast(collection.pivotFontSize, 30); const docMap = new Map<Doc, ViewDefBounds>(); const groupNames: PivotData[] = []; @@ -114,7 +114,8 @@ export namespace PivotView { x, y: width + 50, width: width * 1.25 * numCols, - height: 100, fontSize: fontSize + height: 100, + fontSize }); for (const doc of val) { docMap.set(doc, { @@ -761,7 +762,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result; } - viewDefsToJSX = (views: any[]) => { + viewDefsToJSX = (views: PivotView.PivotData[]) => { let elements: ViewDefResult[] = []; if (Array.isArray(views)) { elements = views.reduce<typeof elements>((prev, ele) => { @@ -773,12 +774,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return elements; } - private viewDefToJSX(viewDef: any): Opt<ViewDefResult> { + private viewDefToJSX(viewDef: PivotView.PivotData): Opt<ViewDefResult> { if (viewDef.type === "text") { const text = Cast(viewDef.text, "string"); const x = Cast(viewDef.x, "number"); const y = Cast(viewDef.y, "number"); - const z = Cast(viewDef.z, "number"); + // const z = Cast(viewDef.z, "number"); const width = Cast(viewDef.width, "number"); const height = Cast(viewDef.height, "number"); const fontSize = Cast(viewDef.fontSize, "number"); @@ -790,7 +791,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { ele: <div className="collectionFreeform-customText" style={{ transform: `translate(${x}px, ${y}px)`, width, height, fontSize - }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! } + }}>{text}</div>, bounds: { x: x!, y: y!, width: width!, height: height! } }; } } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c2143bb82..2a8030c0c 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -41,6 +41,8 @@ import "./DocumentView.scss"; import { FormattedTextBox } from './FormattedTextBox'; import React = require("react"); import { DocumentType } from '../../documents/DocumentTypes'; +import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; +import { ImageField } from '../../../new_fields/URLField'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? library.add(fa.faTrash); @@ -629,12 +631,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" }); subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" }); cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" }); - + if (Cast(this.props.Document.data, ImageField)) { + cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" }); + } + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } let existingMake = ContextMenu.Instance.findByDescription("Make..."); let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : []; makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" }); makes.push({ description: "Custom Document View", event: this.makeCustomViewClicked, icon: "concierge-bell" }); - makes.push({ description: "Metadata Field View", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }) + makes.push({ description: "Metadata Field View", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" }); makes.push({ description: "Into Portal", event: this.makeIntoPortal, icon: "window-restore" }); makes.push({ description: this.layoutDoc.ignoreClick ? "Selectable" : "Unselectable", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" }); !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" }); diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 0ea36cdc2..8a623e648 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -12,7 +12,7 @@ import { DateField } from '../../../new_fields/DateField'; import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc"; import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { List } from '../../../new_fields/List'; -import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField"; +import { RichTextField } from "../../../new_fields/RichTextField"; import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Utils, numberRange, timenow } from '../../../Utils'; @@ -37,6 +37,8 @@ import { DocumentDecorations } from '../DocumentDecorations'; import { DictationManager } from '../../util/DictationManager'; import { ReplaceStep } from 'prosemirror-transform'; import { DocumentType } from '../../documents/DocumentTypes'; +import { RichTextUtils } from '../../../new_fields/RichTextUtils'; +import * as _ from "lodash"; import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment'; import { inputRules } from 'prosemirror-inputrules'; import { select } from 'async'; @@ -44,8 +46,6 @@ import { select } from 'async'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); -export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; - export interface FormattedTextBoxProps { isOverlay?: boolean; hideOnLeave?: boolean; @@ -64,13 +64,15 @@ export const GoogleRef = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); -type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void; +type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(FormattedTextBox, fieldStr); } + public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); + public static Instance: FormattedTextBox; private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _proseRef?: HTMLDivElement; @@ -119,7 +121,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe @undoBatch public setFontColor(color: string) { - this._editorView!.state.storedMarks + this._editorView!.state.storedMarks; if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false; if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) { this.props.Document.color = color; @@ -135,6 +137,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this.props.isOverlay) { DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined); } + FormattedTextBox.Instance = this; } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -174,7 +177,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let range = tx.selection.$from.blockRange(tx.selection.$to); let text = range ? tx.doc.textBetween(range.start, range.end) : ""; let textEndSelection = tx.selection.to; - for (; textEndSelection < range!.end && text[textEndSelection - range!.start] != " "; textEndSelection++) { } + for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { } text = text.substr(0, textEndSelection - range!.start); text = text.split(" ")[text.split(" ").length - 1]; let split = text.split("::"); @@ -188,7 +191,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; } else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id); - }) + }); }); const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); const mval = this._editorView!.state.schema.marks.metadataVal.create(); @@ -392,7 +395,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._reactionDisposer = reaction( () => { const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; - return field ? field.Data : Blank; + return field ? field.Data : RichTextUtils.Initialize(); }, incomingValue => { if (this._editorView && !this._applyingChange) { @@ -462,18 +465,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } pushToGoogleDoc = async () => { - this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.WriteMode; + this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { + let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string"); + let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; - reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) }; + reference = { title: StrCast(this.dataDoc.title) }; } let redo = async () => { - let data = Cast(this.dataDoc.data, RichTextField); - if (this._editorView && reference && data) { - let content = data[ToPlainText](); + if (this._editorView && reference) { + let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); response && (this.dataDoc[GoogleRef] = response.documentId); let pushSuccess = response !== undefined && !("errors" in response); @@ -482,7 +484,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } }; let undo = () => { - let content = exportState.body; + if (!exportState) { + return; + } + let content: GoogleApiClientUtils.Docs.Content = { + text: exportState.text, + requests: [] + }; if (reference && content) { GoogleApiClientUtils.Docs.write({ reference, content, mode }); } @@ -495,49 +503,41 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pullFromGoogleDoc = async (handler: PullHandler) => { let dataDoc = this.dataDoc; let documentId = StrCast(dataDoc[GoogleRef]); - let exportState: GoogleApiClientUtils.ReadResult = {}; + let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; if (documentId) { - exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId }); + exportState = await RichTextUtils.GoogleDocs.Import(documentId); } UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls); } - updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { + updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { let pullSuccess = false; - if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { - const data = Cast(dataDoc.data, RichTextField); - if (data instanceof RichTextField) { - pullSuccess = true; - dataDoc.data = new RichTextField(data[FromPlainText](exportState.body)); - setTimeout(() => { - if (this._editorView) { - let state = this._editorView.state; - let end = state.doc.content.size - 1; - this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); - } - }, 0); - dataDoc.title = exportState.title; - this.Document.customTitle = true; - dataDoc.unchanged = true; - } + if (exportState !== undefined) { + pullSuccess = true; + dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); + setTimeout(() => { + if (this._editorView) { + let state = this._editorView.state; + let end = state.doc.content.size - 1; + this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); + } + }, 0); + dataDoc.title = exportState.title; + this.Document.customTitle = true; + dataDoc.unchanged = true; } else { delete dataDoc[GoogleRef]; } DocumentDecorations.Instance.startPullOutcome(pullSuccess); } - checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => { - if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) { - let data = Cast(dataDoc.data, RichTextField); - if (data) { - let storedPlainText = data[ToPlainText]() + "\n"; - let receivedPlainText = exportState.body; - let storedTitle = dataDoc.title; - let receivedTitle = exportState.title; - let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle; - dataDoc.unchanged = unchanged; - DocumentDecorations.Instance.setPullState(unchanged); - } + checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { + if (exportState && this._editorView) { + let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc); + let equalTitles = dataDoc.title === exportState.title; + let unchanged = equalContent && equalTitles; + dataDoc.unchanged = unchanged; + DocumentDecorations.Instance.setPullState(unchanged); } } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 00c069e1f..98cf7f92f 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,43 +1,59 @@ .imageBox-cont { - padding: 0vw; - position: relative; - text-align: center; - width: 100%; - height: auto; - max-width: 100%; - max-height: 100%; - pointer-events: none; + padding: 0vw; + position: relative; + text-align: center; + width: 100%; + height: auto; + max-width: 100%; + max-height: 100%; + pointer-events: none; } + .imageBox-cont-interactive { - pointer-events: all; - width:100%; - height:auto; + pointer-events: all; + width: 100%; + height: auto; } .imageBox-dot { - position:absolute; + position: absolute; bottom: 10; left: 0; border-radius: 10px; - width:20px; - height:20px; - background:gray; + width: 20px; + height: 20px; + background: gray; } .imageBox-cont img { height: auto; - width:100%; + width: 100%; } + .imageBox-cont-interactive img { height: auto; - width:100%; + width: 100%; +} + +#google-photos { + transition: all 0.5s ease 0s; + width: 30px; + height: 30px; + position: absolute; + top: 15px; + right: 15px; + border: 2px solid black; + border-radius: 50%; + padding: 3px; + background: white; + cursor: pointer; } .imageBox-button { - padding: 0vw; - border: none; - width: 100%; - height: 100%; + padding: 0vw; + border: none; + width: 100%; + height: 100%; } .imageBox-audioBackground { @@ -49,6 +65,7 @@ border-radius: 25px; background: white; opacity: 0.3; + svg { width: 90% !important; height: 70%; @@ -59,44 +76,47 @@ } #cf { - position:relative; - width:100%; - margin:0 auto; - display:flex; + position: relative; + width: 100%; + margin: 0 auto; + display: flex; align-items: center; - height:100%; - overflow:hidden; + height: 100%; + overflow: hidden; + .imageBox-fadeBlocker { - width:100%; - height:100%; + width: 100%; + height: 100%; background: black; - display:flex; + display: flex; flex-direction: row; align-items: center; z-index: 1; + .imageBox-fadeaway { object-fit: contain; - width:100%; - height:100%; + width: 100%; + height: 100%; } } - } - - #cf img { - position:absolute; - left:0; - } - - .imageBox-fadeBlocker { +} + +#cf img { + position: absolute; + left: 0; +} + +.imageBox-fadeBlocker { -webkit-transition: opacity 1s ease-in-out; -moz-transition: opacity 1s ease-in-out; -o-transition: opacity 1s ease-in-out; transition: opacity 1s ease-in-out; - } - .imageBox-fadeBlocker:hover { +} + +.imageBox-fadeBlocker:hover { -webkit-transition: opacity 1s ease-in-out; -moz-transition: opacity 1s ease-in-out; -o-transition: opacity 1s ease-in-out; transition: opacity 1s ease-in-out; - opacity:0; - }
\ No newline at end of file + opacity: 0; +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6fc94a140..b7aadcd3d 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -63,11 +63,10 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD private _lastTap: number = 0; @observable private _isOpen: boolean = false; private dropDisposer?: DragManager.DragDropDisposer; - + @observable private hoverActive = false; @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); } - protected createDropTarget = (ele: HTMLDivElement) => { if (this.dropDisposer) { this.dropDisposer(); @@ -372,6 +371,20 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD this.recordAudioAnnotation(); } + considerGooglePhotosLink = () => { + const remoteUrl = StrCast(this.props.Document.googlePhotosUrl); + if (remoteUrl) { + return ( + <img + id={"google-photos"} + src={"/assets/google_photos.png"} + style={{ opacity: this.hoverActive ? 1 : 0 }} + onClick={() => window.open(remoteUrl)} + /> + ); + } + return (null); + } render() { // let transform = this.props.ScreenToLocalTransform().inverse(); @@ -408,6 +421,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD return ( <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} onPointerDown={this.onPointerDown} + onPointerEnter={action(() => this.hoverActive = true)} + onPointerLeave={action(() => this.hoverActive = false)} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <div id="cf"> <img id={id} @@ -434,6 +449,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD <FontAwesomeIcon className="imageBox-audioFont" style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> </div> + {this.considerGooglePhotosLink()} {/* {this.lightbox(paths)} */} <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); |