diff options
Diffstat (limited to 'src/client/views/collections/CollectionCarouselView.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 316 |
1 files changed, 86 insertions, 230 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 559dcfe2a..64ddaac79 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,52 +1,35 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { StyleProp } from '../StyleProp'; -import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { FlashcardPracticeUI } from './FlashcardPracticeUI'; -enum cardMode { - STAR = 'star', - ALL = 'all', -} -enum practiceMode { - PRACTICE = 'practice', - QUIZ = 'quiz', -} -enum practiceVal { - MISSED = 'missed', - CORRECT = 'correct', -} @observer export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore - get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore - get starField() { return "#star"; } // prettier-ignore - - _sideBtnWidth = 35; _fadeTimer: NodeJS.Timeout | undefined; + _sideBtnWidth = 35; + @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined; + @observable _last_index = this.carouselIndex; + @observable _last_opacity = 1; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } - @observable _last_index = this.carouselIndex; - @observable _last_opacity = 1; - componentWillUnmount() { this._dropDisposer?.(); } @@ -58,39 +41,32 @@ export class CollectionCarouselView extends CollectionSubView() { } }; - @computed get practiceMode() { - return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : ''; - } - @computed get practiceMessage() { - const cardCount = this.carouselItems.length; - if (this.practiceMode) { - if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) { - return 'Finished! Click here to view all flashcards.'; - } - } - return ''; - } - - @computed get filterMessage() { - const cardCount = this.carouselItems.length; - if (!this.practiceMessage) { - if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) { - return 'No tagged items. Click here to view all flash cards.'; - } - if (this.practiceMode) { - if (!cardCount) return 'No flashcards to show! Click here to leave practice mode'; - } - } - return ''; - } - @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore + @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore @computed get carouselItems() { return this.childDocs .filter(doc => doc.type !== DocumentType.LINK) - .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct + .filter(doc => !this._filterFunc?.(doc)) } // prettier-ignore /** + * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore + + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetScale() { + const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1)); + return Math.max(maxWidgetSize / this._sideBtnWidth, 1); + } + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaleTransform() { return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore + screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale; + + /** * Move forward or backward the specified number of Docs * @param dir signed number indicating Docs to move forward or backward */ @@ -102,8 +78,8 @@ export class CollectionCarouselView extends CollectionSubView() { /** * Goes to the next Doc in the stack subject to the currently selected filter option. */ - advance = (e: React.MouseEvent) => { - e.stopPropagation(); + advance = (e?: React.MouseEvent) => { + e?.stopPropagation(); this.move(1); }; @@ -115,55 +91,23 @@ export class CollectionCarouselView extends CollectionSubView() { this.move(-1); }; - /* - * Toggles whether the 'star' metadata field is set on the current Doc - */ - toggleStar = (e: React.MouseEvent) => { - e.stopPropagation(); - const curDoc = this.carouselItems[this.carouselIndex]; - if (curDoc) { - if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField); - else TagItem.addTagToDoc(curDoc, this.starField); - } - }; - - /* - * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. - */ - setPracticeVal = (e: React.MouseEvent, val: string) => { - e.stopPropagation(); - const curDoc = this.carouselItems[this.carouselIndex]; - curDoc && (curDoc[this.practiceField] = val); - this.advance(e); - }; - - /** - * Sets the practice mode answer style for flashcards - * @param mode practiceMode or undefined for no practice - */ - setPracticeMode = (mode: practiceMode | undefined) => { - this.layoutDoc.practiceMode = mode; - this.carouselItems?.map(doc => (doc[this.practiceField] = undefined)); - if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined)); - }; + curDoc = () => this.carouselItems[this.carouselIndex]; captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => { // first look for properties on the document in the carousel, then fallback to properties on the container const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property); }; + contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); - captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; + captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX; contentScreenToLocalXf = () => this._props .ScreenToLocalTransform() .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)) .scale(this._props.NativeDimScaling?.() || 1); - - contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); - isChildContentActive = () => this._props.isContentActive?.() === false ? false @@ -172,10 +116,7 @@ export class CollectionCarouselView extends CollectionSubView() { : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; - renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { - const screenScale = this.ScreenToLocalBoxXf().Scale; - const fitWidthScale = (NumCast(this.Document.width, 1) / NumCast(this.carouselItems[this.carouselIndex]?._width)) * (this._props.NativeDimScaling?.() || 1); return ( <DocumentView {...this._props} @@ -203,7 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() { ScreenToLocalTransform={this.contentScreenToLocalXf} PanelWidth={this.contentPanelWidth} PanelHeight={this.contentPanelHeight} - xPadding={(this._sideBtnWidth * Math.min(this.maxWidgetScale, screenScale * screenScale)) / fitWidthScale} // padding shrinks based on screenScale to maintain its size, and then again by screenSize to get smaller + screenXPadding={this.screenXPadding} /> ); }; @@ -214,7 +155,7 @@ export class CollectionCarouselView extends CollectionSubView() { const fadeTime = 500; const lastDoc = this.carouselItems?.[this._last_index]; return !lastDoc || this.carouselIndex === this._last_index ? null : ( - <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}> + <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}> {this.renderDoc( lastDoc, false, // hide captions if the carousel is configured to show the captions @@ -235,15 +176,18 @@ export class CollectionCarouselView extends CollectionSubView() { </div> ); } + @computed get renderedDoc() { + const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); + return this.renderDoc(this.curDoc(), !!carouselShowsCaptions); + } + @computed get content() { - const index = this.carouselIndex; - const curDoc = this.carouselItems?.[index]; const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined }; const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); - return !curDoc ? null : ( + return !this.curDoc() ? null : ( <> <div className="collectionCarouselView-image" key="image"> - {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.renderedDoc} {this.overlay} </div> {!carouselShowsCaptions ? null : ( @@ -253,158 +197,70 @@ export class CollectionCarouselView extends CollectionSubView() { onWheel={StopEvent} style={{ borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string, - marginRight: this.marginX, - marginLeft: this.marginX, - width: `calc(100% - ${this.marginX * 2}px)`, + marginRight: this.captionMarginX, + marginLeft: this.captionMarginX, + width: `calc(100% - ${this.captionMarginX * 2}px)`, }}> - <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} /> + <FormattedTextBox xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={this.curDoc()} TemplateDataDocument={undefined} /> </div> )} </> ); } - togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode); - toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true); - setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray'); - - @computed get filterDoc() { - return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); - } - filterHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this.ScreenToLocalBoxXf().Scale); - filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); - - /** - * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings - */ - @computed get contentScaling() { - return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); - } - - /** - * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. - */ - @computed get maxWidgetScale() { - const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.Document.width, 1)); - return Math.max(maxWidgetSize / this._sideBtnWidth, 1); - } - /** - * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content - */ - @computed get uiBtnScaleTransform() { - return `scale(${this.maxWidgetScale * Math.min(1, this.contentScaling)})`; - } - @computed get menu() { - const curDoc = this.carouselItems?.[this.carouselIndex]; - return ( - <div className="carouselView-menu" style={{ height: this.filterHeight(), width: this.filterHeight(), transform: this.uiBtnScaleTransform }}> - {!this.filterDoc ? null : ( - <DocumentView - {...this._props} - Document={this.filterDoc} - TemplateDataDocument={undefined} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - renderDepth={this._props.renderDepth + 1} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} - showTags={false} - hideFilterStatus={true} - containerViewPath={this.childContainerViewPath} - setContentViewBox={undefined} - onDoubleClickScript={this.onContentDoubleClick} - onClickScript={this.onContentClick} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} - isContentActive={this.isChildContentActive} - hideCaptions={true} - childFilters={this.childDocFilters} - hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} - addDocument={this._props.addDocument} - ScreenToLocalTransform={this.contentScreenToLocalXf} - PanelWidth={this.filterWidth} - PanelHeight={this.filterHeight} - /> - )} - <div - className="carouselView-practiceModes" - style={{ - transformOrigin: `0px ${-this.filterHeight()}px`, - transform: `scale(${Math.max(1, 1 / this.ScreenToLocalBoxXf().Scale / this.maxWidgetScale)})`, - display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none', - }}> - <Tooltip title="Practice flashcards using GPT"> - <div key="back" className="carouselView-quiz" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}> - <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" /> - </div> - </Tooltip> - <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> - <div key="back" className="carouselView-practice" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}> - <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" /> - </div> - </Tooltip> - </div> - </div> - ); - } - @computed get buttons() { - return ( + @computed get navButtons() { + return this.Document._chromeHidden || !this.curDoc() ? null : ( <> - <div key="back" className="carouselView-back" style={{ transform: this.uiBtnScaleTransform }} onClick={this.goback}> + <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> - <div key="fwd" className="carouselView-fwd" style={{ transform: this.uiBtnScaleTransform }} onClick={this.advance}> + <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - {this.practiceMode == practiceMode.PRACTICE ? ( - <div style={{ transform: this.uiBtnScaleTransform, bottom: `${this._sideBtnWidth}px`, height: `${this._sideBtnWidth}px`, position: 'absolute', width: `100%` }}> - <Tooltip title="Incorrect. View again later."> - <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}> - <FontAwesomeIcon icon="xmark" color="red" size="1x" /> - </div> - </Tooltip> - <Tooltip title="Correct"> - <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}> - <FontAwesomeIcon icon="check" color="green" size="1x" /> - </div> - </Tooltip> - </div> - ) : null} </> ); } + docViewProps = () => ({ + ...this._props, // + isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, + isContentActive: this.isChildContentActive, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + carouselItemsFunc = () => this.carouselItems; + answered = () => this.advance(); + @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore + render() { return ( - <div> - <div - className="collectionCarouselView-outer" - ref={this.createDashEventsTarget} - style={{ - background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, - width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`, - height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`, - left: NumCast(this.layoutDoc._xMargin), - top: NumCast(this.layoutDoc._yMargin), - }}> - {!this.practiceMessage && !this.filterMessage ? ( - this.content - ) : ( - <p - className="message" - onClick={() => { - if (this.filterMessage || this.practiceMessage) { - this.setPracticeMode(undefined); - Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove'); - } - }}> - {this.filterMessage || this.practiceMessage} - </p> - )} - </div> - {!this.Document._chromeHidden ? this.menu : null} - {!this.Document._chromeHidden && this.carouselItems?.[this.carouselIndex] ? this.buttons : null} + <div + className="collectionCarouselView-outer" + ref={this.createDashEventsTarget} + style={{ + background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, + color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, + width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`, + height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`, + left: NumCast(this.layoutDoc._xMargin), + top: NumCast(this.layoutDoc._yMargin), + }}> + {this.content} + <FlashcardPracticeUI + setFilterFunc={this.setFilterFunc} + fieldKey={this.fieldKey} + sideBtnWidth={this._sideBtnWidth} + carouselItems={this.carouselItemsFunc} + childDocs={this.childDocs} + advance={this.answered} + curDoc={this.curDoc} + layoutDoc={this.layoutDoc} + maxWidgetScale={this.maxWidgetScale} + uiBtnScaleTransform={this.uiBtnScaleTransform} + ScreenToLocalBoxXf={this.ScreenToLocalBoxXf} + renderDepth={this._props.renderDepth} + docViewProps={this.docViewProps} + /> + {this.navButtons} </div> ); } |
