From 2c833fe4e7915b8219c6b48abb596e4359744750 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 21 Jul 2022 10:49:52 -0400
Subject: fixed hiding marquee options menu when clicking on background.

---
 src/client/views/pdf/AnchorMenu.tsx | 264 +++++++++++++++++++++---------------
 1 file changed, 151 insertions(+), 113 deletions(-)

(limited to 'src/client/views/pdf/AnchorMenu.tsx')

diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 29d068817..1a1120b6c 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,16 +1,16 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { action, computed, observable, IReactionDisposer, reaction, ObservableMap } from "mobx";
-import { observer } from "mobx-react";
-import { ColorState } from "react-color";
-import { Doc, Opt } from "../../../fields/Doc";
-import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from "../../../Utils";
-import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu";
-import { ButtonDropdown } from "../nodes/formattedText/RichTextMenu";
-import "./AnchorMenu.scss";
-import { SelectionManager } from "../../util/SelectionManager";
-import { LinkPopup } from "../linking/LinkPopup";
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import { ColorState } from 'react-color';
+import { Doc, Opt } from '../../../fields/Doc';
+import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../Utils';
+import { SelectionManager } from '../../util/SelectionManager';
+import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
+import { LinkPopup } from '../linking/LinkPopup';
+import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
+import './AnchorMenu.scss';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -19,30 +19,31 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     private _disposer: IReactionDisposer | undefined;
     private _commentCont = React.createRef<HTMLButtonElement>();
     private _palette = [
-        "rgba(208, 2, 27, 0.8)",
-        "rgba(238, 0, 0, 0.8)",
-        "rgba(245, 166, 35, 0.8)",
-        "rgba(248, 231, 28, 0.8)",
-        "rgba(245, 230, 95, 0.616)",
-        "rgba(139, 87, 42, 0.8)",
-        "rgba(126, 211, 33, 0.8)",
-        "rgba(65, 117, 5, 0.8)",
-        "rgba(144, 19, 254, 0.8)",
-        "rgba(238, 169, 184, 0.8)",
-        "rgba(224, 187, 228, 0.8)",
-        "rgba(225, 223, 211, 0.8)",
-        "rgba(255, 255, 255, 0.8)",
-        "rgba(155, 155, 155, 0.8)",
-        "rgba(0, 0, 0, 0.8)"];
-
-    @observable private _keyValue: string = "";
-    @observable private _valueValue: string = "";
+        'rgba(208, 2, 27, 0.8)',
+        'rgba(238, 0, 0, 0.8)',
+        'rgba(245, 166, 35, 0.8)',
+        'rgba(248, 231, 28, 0.8)',
+        'rgba(245, 230, 95, 0.616)',
+        'rgba(139, 87, 42, 0.8)',
+        'rgba(126, 211, 33, 0.8)',
+        'rgba(65, 117, 5, 0.8)',
+        'rgba(144, 19, 254, 0.8)',
+        'rgba(238, 169, 184, 0.8)',
+        'rgba(224, 187, 228, 0.8)',
+        'rgba(225, 223, 211, 0.8)',
+        'rgba(255, 255, 255, 0.8)',
+        'rgba(155, 155, 155, 0.8)',
+        'rgba(0, 0, 0, 0.8)',
+    ];
+
+    @observable private _keyValue: string = '';
+    @observable private _valueValue: string = '';
     @observable private _added: boolean = false;
-    @observable private highlightColor: string = "rgba(245, 230, 95, 0.616)";
+    @observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
     @observable private _showLinkPopup: boolean = false;
 
     @observable public Highlighting: boolean = false;
-    @observable public Status: "marquee" | "annotation" | "" = "";
+    @observable public Status: 'marquee' | 'annotation' | '' = '';
 
     public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
 
@@ -57,7 +58,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public PinToPres: () => void = unimplementedFunction;
     public MakePushpin: () => void = unimplementedFunction;
     public IsPushpin: () => boolean = returnFalse;
-    public get Active() { return this._left > 0; }
+    public get Active() {
+        return this._left > 0;
+    }
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -67,26 +70,40 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     componentDidMount() {
-        this._disposer = reaction(() => SelectionManager.Views(),
+        this._disposer = reaction(
+            () => SelectionManager.Views(),
             selected => {
                 this._showLinkPopup = false;
                 AnchorMenu.Instance.fadeOut(true);
-            });
+            }
+        );
     }
 
     pointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, (e: PointerEvent) => {
-            this.StartDrag(e, this._commentCont.current!);
-            return true;
-        }, returnFalse, e => this.OnClick?.(e));
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            (e: PointerEvent) => {
+                this.StartDrag(e, this._commentCont.current!);
+                return true;
+            },
+            returnFalse,
+            e => this.OnClick?.(e)
+        );
+    };
 
     cropDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, (e: PointerEvent) => {
-            this.StartCropDrag(e, this._commentCont.current!);
-            return true;
-        }, returnFalse, e => this.OnCrop?.(e));
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            (e: PointerEvent) => {
+                this.StartCropDrag(e, this._commentCont.current!);
+                return true;
+            },
+            returnFalse,
+            e => this.OnCrop?.(e)
+        );
+    };
 
     @action
     highlightClicked = (e: React.MouseEvent) => {
@@ -94,41 +111,45 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             this.Highlighting = !this.Highlighting;
         }
         AnchorMenu.Instance.fadeOut(true);
-    }
+    };
 
     @action
     toggleLinkPopup = (e: React.MouseEvent) => {
         //ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button
         //change popup visibility field to visible
         this._showLinkPopup = !this._showLinkPopup;
-    }
+    };
 
     @computed get highlighter() {
-        const button =
+        const button = (
             <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}>
-                <div className="anchor-color-preview" >
-                    <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
+                <div className="anchor-color-preview">
+                    <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: this.Highlighting ? '' : 'rotate(-45deg)' }} />
                     <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>
                 </div>
-            </button>;
+            </button>
+        );
 
-        const dropdownContent =
+        const dropdownContent = (
             <div className="dropdown">
                 <p>Change highlighter color:</p>
                 <div className="color-wrapper">
                     {this._palette.map(color => {
                         if (color) {
-                            return this.highlightColor === color ?
-                                <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button> :
-                                <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button>;
+                            return this.highlightColor === color ? (
+                                <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button>
+                            ) : (
+                                <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button>
+                            );
                         }
                     })}
                 </div>
-            </div>;
+            </div>
+        );
         return (
-            <Tooltip key="highlighter" title={<div className="dash-tooltip">{"Click to Highlight"}</div>}>
+            <Tooltip key="highlighter" title={<div className="dash-tooltip">{'Click to Highlight'}</div>}>
                 <div className="anchorMenu-highlighter">
-                    <ButtonDropdown key={"highlighter"} button={button} dropdownContent={dropdownContent} pdf={true} />
+                    <ButtonDropdown key={'highlighter'} button={button} dropdownContent={dropdownContent} pdf={true} />
                 </div>
             </Tooltip>
         );
@@ -136,70 +157,87 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     @action changeHighlightColor = (color: string, e: React.PointerEvent) => {
         const col: ColorState = {
-            hex: color, hsl: { a: 0, h: 0, s: 0, l: 0, source: "" }, hsv: { a: 0, h: 0, s: 0, v: 0, source: "" },
-            rgb: { a: 0, r: 0, b: 0, g: 0, source: "" }, oldHue: 0, source: "",
+            hex: color,
+            hsl: { a: 0, h: 0, s: 0, l: 0, source: '' },
+            hsv: { a: 0, h: 0, s: 0, v: 0, source: '' },
+            rgb: { a: 0, r: 0, b: 0, g: 0, source: '' },
+            oldHue: 0,
+            source: '',
         };
         e.preventDefault();
         e.stopPropagation();
         this.highlightColor = Utils.colorString(col);
-    }
-
-    @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this._keyValue = e.currentTarget.value; };
-    @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { this._valueValue = e.currentTarget.value; };
+    };
+
+    @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._keyValue = e.currentTarget.value;
+    };
+    @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._valueValue = e.currentTarget.value;
+    };
     @action addTag = (e: React.PointerEvent) => {
         if (this._keyValue.length > 0 && this._valueValue.length > 0) {
             this._added = this.AddTag(this._keyValue, this._valueValue);
-            setTimeout(action(() => this._added = false), 1000);
+            setTimeout(
+                action(() => (this._added = false)),
+                1000
+            );
         }
-    }
+    };
 
     render() {
-        const buttons = this.Status === "marquee" ?
-            [
-                this.highlighter,
-
-                <Tooltip key="annotate" title={<div className="dash-tooltip">{"Drag to Place Annotation"}</div>}>
-                    <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: "grab" }}>
-                        <FontAwesomeIcon icon="comment-alt" size="lg" />
-                    </button>
-                </Tooltip>,
-                //NOTE: link popup is currently in progress
-                <Tooltip key="link" title={<div className="dash-tooltip">{"Find document to link to selected text"}</div>}>
-                    <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
-                        <FontAwesomeIcon style={{ position: "absolute", transform: "scale(1.5)" }} icon={"search"} size="lg" />
-                        <FontAwesomeIcon style={{ position: "absolute", transform: "scale(0.5)", transformOrigin: "top left", top: 12, left: 12 }} icon={"link"} size="lg" />
-                    </button>
-                </Tooltip>,
-                <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />,
-                AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? <></> : <Tooltip key="crop" title={<div className="dash-tooltip">{"Click/Drag to create cropped image"}</div>}>
-                    <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: "grab" }}>
-                        <FontAwesomeIcon icon="image" size="lg" />
-                    </button>
-                </Tooltip>,
-            ] : [
-                <Tooltip key="trash" title={<div className="dash-tooltip">{"Remove Link Anchor"}</div>}>
-                    <button className="antimodeMenu-button" onPointerDown={this.Delete}>
-                        <FontAwesomeIcon icon="trash-alt" size="lg" />
-                    </button>
-                </Tooltip>,
-                <Tooltip key="Pin" title={<div className="dash-tooltip">{"Pin to Presentation"}</div>}>
-                    <button className="antimodeMenu-button" onPointerDown={this.PinToPres}>
-                        <FontAwesomeIcon icon="map-pin" size="lg" />
-                    </button>
-                </Tooltip>,
-                <Tooltip key="pushpin" title={<div className="dash-tooltip">{"toggle pushpin behavior"}</div>}>
-                    <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? "black" : "white", backgroundColor: this.IsPushpin() ? "white" : "black" }} onPointerDown={this.MakePushpin}>
-                        <FontAwesomeIcon icon="thumbtack" size="lg" />
-                    </button>
-                </Tooltip>,
-                // <div key="7" className="anchorMenu-addTag" >
-                //     <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
-                //     <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
-                // </div>,
-                // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
-                //     <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
-            ];
+        const buttons =
+            this.Status === 'marquee'
+                ? [
+                      this.highlighter,
+
+                      <Tooltip key="annotate" title={<div className="dash-tooltip">{'Drag to Place Annotation'}</div>}>
+                          <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}>
+                              <FontAwesomeIcon icon="comment-alt" size="lg" />
+                          </button>
+                      </Tooltip>,
+                      //NOTE: link popup is currently in progress
+                      <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
+                          <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
+                              <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
+                              <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" />
+                          </button>
+                      </Tooltip>,
+                      <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />,
+                      AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? (
+                          <></>
+                      ) : (
+                          <Tooltip key="crop" title={<div className="dash-tooltip">{'Click/Drag to create cropped image'}</div>}>
+                              <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}>
+                                  <FontAwesomeIcon icon="image" size="lg" />
+                              </button>
+                          </Tooltip>
+                      ),
+                  ]
+                : [
+                      <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}>
+                          <button className="antimodeMenu-button" onPointerDown={this.Delete}>
+                              <FontAwesomeIcon icon="trash-alt" size="lg" />
+                          </button>
+                      </Tooltip>,
+                      <Tooltip key="Pin" title={<div className="dash-tooltip">{'Pin to Presentation'}</div>}>
+                          <button className="antimodeMenu-button" onPointerDown={this.PinToPres}>
+                              <FontAwesomeIcon icon="map-pin" size="lg" />
+                          </button>
+                      </Tooltip>,
+                      <Tooltip key="pushpin" title={<div className="dash-tooltip">{'toggle pushpin behavior'}</div>}>
+                          <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? 'black' : 'white', backgroundColor: this.IsPushpin() ? 'white' : 'black' }} onPointerDown={this.MakePushpin}>
+                              <FontAwesomeIcon icon="thumbtack" size="lg" />
+                          </button>
+                      </Tooltip>,
+                      // <div key="7" className="anchorMenu-addTag" >
+                      //     <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+                      //     <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+                      // </div>,
+                      // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
+                      //     <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
+                  ];
 
         return this.getElement(buttons);
     }
-}
\ No newline at end of file
+}
-- 
cgit v1.2.3-70-g09d2


From bca0026764fe554e1066b2f432e749371450d239 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 9 Aug 2022 11:35:00 -0400
Subject: added an audio annotation button to the anchor menu bar.

---
 src/client/views/DocumentButtonBar.tsx             |  12 +-
 src/client/views/MarqueeAnnotator.tsx              |   1 +
 src/client/views/SidebarAnnos.tsx                  |   1 +
 src/client/views/nodes/DocumentView.tsx            |  35 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  11 ++
 src/client/views/pdf/AnchorMenu.tsx                |  14 ++
 src/fields/documentSchemas.ts                      | 182 ++++++++++-----------
 7 files changed, 145 insertions(+), 111 deletions(-)

(limited to 'src/client/views/pdf/AnchorMenu.tsx')

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 265df3abc..81e417fca 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -21,7 +21,7 @@ import './DocumentButtonBar.scss';
 import { Colors } from './global/globalEnums';
 import { MetadataEntryMenu } from './MetadataEntryMenu';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
 import { TemplateMenu } from './TemplateMenu';
@@ -347,7 +347,15 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                     onClick={undoBatch(
                         action(e => {
                             this._isRecording = true;
-                            this.props.views().map(view => view?.docView?.recordAudioAnnotation(action(() => (this._isRecording = false))));
+                            this.props.views().map(
+                                view =>
+                                    view &&
+                                    DocumentViewInternal.recordAudioAnnotation(
+                                        view.dataDoc,
+                                        view.LayoutFieldKey,
+                                        action(() => (this._isRecording = false))
+                                    )
+                            );
                         })
                     )}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index b01ee5f42..f90ad8bb5 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -65,6 +65,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
 
         AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true);
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true));
+        AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = this.highlight;
         AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
         AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 1c14c7cd5..90d9c3c43 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -74,6 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
         this.addDocument(target);
         this._stackRef.current?.focusDocument(target);
+        return target;
     };
     makeDocUnfiltered = (doc: Doc) => {
         if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4b2bd07ef..f87581875 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -206,7 +206,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
     _animateScaleTime = 300; // milliseconds;
     @observable _animateScalingTo = 0;
-    @observable _mediaState = 0;
     @observable _pendingDoubleClick = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _downX: number = 0;
@@ -879,7 +878,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
             if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
-                !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? 'Show' : 'Hide'} Audio Button`, event: action(() => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' });
                 const existingOnClick = cm.findByDescription('OnClick...');
                 const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
 
@@ -1010,10 +1008,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
         const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
         const audioView =
-            (!this.props.isSelected() && !this._isHovering) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this._mediaState) ? null : (
+            (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : (
                 <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
                     <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
-                        <FontAwesomeIcon className="documentView-audioFont" style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" />
+                        <FontAwesomeIcon
+                            className="documentView-audioFont"
+                            style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }}
+                            icon={!audioAnnosCount ? 'microphone' : 'file-audio'}
+                            size="sm"
+                        />
                     </div>
                 </Tooltip>
             );
@@ -1154,7 +1157,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const self = this;
         const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
         const anno = audioAnnos.lastElement();
-        if (anno instanceof AudioField && this._mediaState === 0) {
+        if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) {
             new Howl({
                 src: [anno.url.href],
                 format: ['mp3'],
@@ -1163,27 +1166,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 volume: 0.5,
                 onend: function () {
                     runInAction(() => {
-                        console.log('PLAYED');
-                        self._mediaState = 0;
+                        self.dataDoc.audioAnnoState = 0;
                     });
                 },
             });
-            this._mediaState = 1;
+            this.dataDoc.audioAnnoState = 1;
         }
     };
 
-    recordAudioAnnotation = (onEnd?: () => void) => {
+    static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) {
         let gumStream: any;
         let recorder: any;
-        const self = this;
         navigator.mediaDevices
             .getUserMedia({
                 audio: true,
             })
             .then(function (stream) {
-                let audioTextAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
+                let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null);
                 if (audioTextAnnos) audioTextAnnos.push('');
-                else audioTextAnnos = self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'] = new List<string>(['']);
+                else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List<string>(['']);
                 DictationManager.Controls.listen({
                     interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
                     continuous: { indefinite: false },
@@ -1200,24 +1201,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     const [{ result }] = await Networking.UploadFilesToServer(e.data);
                     if (!(result instanceof Error)) {
                         const audioField = new AudioField(result.accessPaths.agnostic.client);
-                        const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+                        const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null);
                         if (audioAnnos === undefined) {
-                            self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioField]);
+                            dataDoc[field + '-audioAnnotations'] = new List([audioField]);
                         } else {
                             audioAnnos.push(audioField);
                         }
                     }
                 };
-                runInAction(() => (self._mediaState = 2));
+                runInAction(() => (dataDoc.audioAnnoState = 2));
                 recorder.start();
                 setTimeout(() => {
                     recorder.stop();
                     DictationManager.Controls.stop(false);
-                    runInAction(() => (self._mediaState = 0));
+                    runInAction(() => (dataDoc.audioAnnoState = 0));
                     gumStream.getAudioTracks()[0].stop();
                 }, 5000);
             });
-    };
+    }
 
     captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
     @computed get innards() {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 929cca1ea..223441b3b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -63,6 +63,7 @@ import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
 import { text } from 'body-parser';
 import { CollectionTreeView } from '../../collections/CollectionTreeView';
+import { DocumentViewInternal } from '../DocumentView';
 const translateGoogleApi = require('translate-google-api');
 
 export interface FormattedTextBoxProps {
@@ -249,6 +250,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.layoutDoc.showSidebar && this.toggleSidebar();
             this._sidebarRef.current?.anchorMenuClick(this.getAnchor());
         };
+        AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
+            !this.layoutDoc.showSidebar && this.toggleSidebar();
+            const anchor = this.getAnchor();
+            const target = this._sidebarRef.current?.anchorMenuClick(anchor);
+            if (target) {
+                anchor.followLinkAudio = true;
+                DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
+                target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
+            }
+        };
         AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
             this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
             return undefined;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 1a1120b6c..ee2ae10a7 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -49,6 +49,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
     public OnClick: (e: PointerEvent) => void = unimplementedFunction;
+    public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
@@ -92,6 +93,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         );
     };
 
+    audioDown = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e));
+    };
+
     cropDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(
             this,
@@ -196,6 +201,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                               <FontAwesomeIcon icon="comment-alt" size="lg" />
                           </button>
                       </Tooltip>,
+                      AnchorMenu.Instance.OnAudio === unimplementedFunction ? (
+                          <></>
+                      ) : (
+                          <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">{'Click to Record Annotation'}</div>}>
+                              <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
+                                  <FontAwesomeIcon icon="microphone" size="lg" />
+                              </button>
+                          </Tooltip>
+                      ),
                       //NOTE: link popup is currently in progress
                       <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
                           <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index be39e0709..24b5a359d 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -1,116 +1,114 @@
-import { makeInterface, createSchema, listSpec } from "./Schema";
-import { ScriptField } from "./ScriptField";
-import { Doc } from "./Doc";
-import { DateField } from "./DateField";
-import { SchemaHeaderField } from "./SchemaHeaderField";
+import { makeInterface, createSchema, listSpec } from './Schema';
+import { ScriptField } from './ScriptField';
+import { Doc } from './Doc';
+import { DateField } from './DateField';
+import { SchemaHeaderField } from './SchemaHeaderField';
 
 export const documentSchema = createSchema({
     // content properties
-    type: "string",             // enumerated type of document -- should be template-specific (ie, start with an '_')
-    title: "string",            // document title (can be on either data document or layout)
-    isTemplateForField: "string",// if specified, it indicates the document is a template that renders the specified field
-    creationDate: DateField,    // when the document was created
-    links: listSpec(Doc),       // computed (readonly) list of links associated with this document
+    type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_')
+    title: 'string', // document title (can be on either data document or layout)
+    isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field
+    creationDate: DateField, // when the document was created
+    links: listSpec(Doc), // computed (readonly) list of links associated with this document
 
     // "Location" properties in a very general sense
-    _curPage: "number",         // current page of a page based document
-    _currentFrame: "number",    // current frame of a frame based collection (e.g., a progressive slide)
-    lastFrame: "number",        // last frame of a frame based collection (e.g., a progressive slide)
-    activeFrame: "number",      // the active frame of a frame based animated document 
-    _currentTimecode: "number", // current play back time of a temporal document (video / audio)
-    _timecodeToShow: "number",  // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
-    _timecodeToHIde: "number",  // the time that a document should be hidden
-    markers: listSpec(Doc),     // list of markers for audio / video
-    x: "number",                // x coordinate when in a freeform view 
-    y: "number",                // y coordinate when in a freeform view 
-    z: "number",                // z "coordinate" - non-zero specifies the overlay layer of a freeformview
-    zIndex: "number",           // zIndex of a document in a freeform view
-    _scrollTop: "number",       // scroll position of a scrollable document (pdf, text, web)
-    lat: "number",
-    lng: "number",
+    _curPage: 'number', // current page of a page based document
+    _currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide)
+    lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide)
+    activeFrame: 'number', // the active frame of a frame based animated document
+    _currentTimecode: 'number', // current play back time of a temporal document (video / audio)
+    _timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+    _timecodeToHIde: 'number', // the time that a document should be hidden
+    markers: listSpec(Doc), // list of markers for audio / video
+    x: 'number', // x coordinate when in a freeform view
+    y: 'number', // y coordinate when in a freeform view
+    z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview
+    zIndex: 'number', // zIndex of a document in a freeform view
+    _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web)
+    lat: 'number',
+    lng: 'number',
 
     // appearance properties on the layout
-    "_backgroundGrid-spacing": "number", // the size of the grid for collection views
-    _autoHeight: "boolean",     // whether the height of the document should be computed automatically based on its contents
-    _nativeWidth: "number",     // native width of document which determines how much document contents are scaled when the document's width is set
-    _nativeHeight: "number",    // "
-    _width: "number",           // width of document in its container's coordinate system
-    _height: "number",          // "
-    _xPadding: "number",        // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
-    _yPadding: "number",        // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
-    _xMargin: "number",         // margin added on left/right of most documents to add separation from their container
-    _yMargin: "number",         // margin added on top/bottom of most documents to add separation from their container
-    _overflow: "string",        // sets overflow behvavior for CollectionFreeForm views
-    _showCaption: "string",     // whether editable caption text is overlayed at the bottom of the document 
-    _showTitle: "string",       // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";".  Use :hover suffix to indicate title should be shown on hover
-    _showAudio: "boolean",      // whether to show the audio record icon on documents
-    _pivotField: "string",      // specifies which field key should be used as the timeline/pivot axis
-    _columnsFill: "boolean",    // whether documents in a stacking view column should be sized to fill the column
-    _columnsSort: "string",     // how a document should be sorted "ascending", "descending", undefined (none)   
-    _columnsHideIfEmpty: "boolean",   // whether empty stacking view column headings should be hidden
+    '_backgroundGrid-spacing': 'number', // the size of the grid for collection views
+    _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents
+    _nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set
+    _nativeHeight: 'number', // "
+    _width: 'number', // width of document in its container's coordinate system
+    _height: 'number', // "
+    _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
+    _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
+    _xMargin: 'number', // margin added on left/right of most documents to add separation from their container
+    _yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container
+    _overflow: 'string', // sets overflow behvavior for CollectionFreeForm views
+    _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document
+    _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";".  Use :hover suffix to indicate title should be shown on hover
+    _pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis
+    _columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column
+    _columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none)
+    _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
     _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
     _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
-    _fontSize: "string",
-    _fontFamily: "string",
-    _sidebarWidthPercent: "string", // percent of text window width taken up by sidebar
+    _fontSize: 'string',
+    _fontFamily: 'string',
+    _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
 
     // appearance properties on the data document
-    backgroundColor: "string",  // background color of document
-    borderRounding: "string",   // border radius rounding of document
-    boxShadow: "string",        // the amount of shadow around the perimeter of a document
-    color: "string",            // foreground color of document
-    fitContentsToBox: "boolean",// whether freeform view contents should be zoomed/panned to fill the area of the document view box
-    fontSize: "string",
-    hidden: "boolean",          // whether a document should not be displayed
-    isInkMask: "boolean",       // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
-    layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below
-    layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
-    letterSpacing: "string",
-    opacity: "number",          // opacity of document
-    strokeWidth: "number",
-    strokeBezier: "number",
-    strokeStartMarker: "string",
-    strokeEndMarker: "string",
-    strokeDash: "string",
-    textTransform: "string",
-    treeViewOpen: "boolean",    //  flag denoting whether the documents sub-tree (contents) is visible or hidden
-    treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
-    treeViewExpandedViewLock: "boolean", // whether the expanded view can be changed
-    treeViewOpenIsTransient: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
-    treeViewType: "string", // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
+    backgroundColor: 'string', // background color of document
+    borderRounding: 'string', // border radius rounding of document
+    boxShadow: 'string', // the amount of shadow around the perimeter of a document
+    color: 'string', // foreground color of document
+    fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box
+    fontSize: 'string',
+    hidden: 'boolean', // whether a document should not be displayed
+    isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
+    layout: 'string', // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below
+    layoutKey: 'string', // holds the field key for the field that actually holds the current lyoat
+    letterSpacing: 'string',
+    opacity: 'number', // opacity of document
+    strokeWidth: 'number',
+    strokeBezier: 'number',
+    strokeStartMarker: 'string',
+    strokeEndMarker: 'string',
+    strokeDash: 'string',
+    textTransform: 'string',
+    treeViewOpen: 'boolean', //  flag denoting whether the documents sub-tree (contents) is visible or hidden
+    treeViewExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
+    treeViewExpandedViewLock: 'boolean', // whether the expanded view can be changed
+    treeViewOpenIsTransient: 'boolean', // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+    treeViewType: 'string', // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
 
     // interaction and linking properties
-    ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
-    onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    ignoreClick: 'boolean', // whether documents ignores input clicks (but does not ignore manipulation and other events)
+    onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
-    onPointerUp: ScriptField,   // script to run when document is clicked (can be overriden by an onClick prop)
-    onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
-    followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, ) 
-    hideLinkButton: "boolean",  // whether the blue link counter button should be hidden
-    hideAllLinks: "boolean",    // whether all individual blue anchor dots should be hidden
-    linkDisplay: "boolean",     // whether a link connection should be shown between link anchor endpoints.
-    isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
-    isLinkButton: "boolean",    // whether document functions as a link follow button to follow the first link on the document when clicked   
-    layers: listSpec("string"), // which layers the document is part of
-    _lockedPosition: "boolean",  // whether the document can be moved (dragged)
-    _lockedTransform: "boolean",// whether a freeformview can pan/zoom
-    displayArrow: "boolean", // toggles directed arrows
+    onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+    onDragStart: ScriptField, // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
+    followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, )
+    hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
+    hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
+    linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
+    isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations
+    isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
+    layers: listSpec('string'), // which layers the document is part of
+    _lockedPosition: 'boolean', // whether the document can be moved (dragged)
+    _lockedTransform: 'boolean', // whether a freeformview can pan/zoom
+    displayArrow: 'boolean', // toggles directed arrows
 
     // drag drop properties
-    _stayInCollection: "boolean",// whether document can be dropped into a different collection
-    dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
-    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
-    targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
-    childDropAction: "string",  // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
-    removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
+    _stayInCollection: 'boolean', // whether document can be dropped into a different collection
+    dragFactory: Doc, // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
+    dropAction: 'string', // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
+    targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
+    childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
+    removeDropProperties: listSpec('string'), // properties that should be removed from the alias/copy/etc of this document when it is dropped
 });
 
-
 export const collectionSchema = createSchema({
     childLayoutTemplate: Doc, // layout template to use to render children of a collecion
-    childLayoutString: "string", //layout string to use to render children of a collection
+    childLayoutString: 'string', //layout string to use to render children of a collection
     childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template)
-    childDontRegisterViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links 
+    childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links
     onChildClick: ScriptField, // script to run for each child when its clicked
     onChildDoubleClick: ScriptField, // script to run for each child when its clicked
     onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view
-- 
cgit v1.2.3-70-g09d2

