diff options
Diffstat (limited to 'src/client/views/collections/CollectionCarouselView.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 258 |
1 files changed, 178 insertions, 80 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 74cf580c9..bb757cc43 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,13 +1,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +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, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; -import { ContextMenu } from '../ContextMenu'; import { StyleProp } from '../StyleProp'; import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; @@ -17,8 +18,11 @@ import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; enum cardMode { - PRACTICE = 'practice', STAR = 'star', + ALL = 'all', +} +enum practiceMode { + PRACTICE = 'practice', QUIZ = 'quiz', } enum practiceVal { @@ -29,10 +33,10 @@ enum practiceVal { 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 _fadeTimer: NodeJS.Timeout | undefined; - _resetter: IReactionDisposer | undefined; constructor(props: SubCollectionViewProps) { super(props); @@ -42,23 +46,8 @@ export class CollectionCarouselView extends CollectionSubView() { @observable _last_index = this.carouselIndex; @observable _last_opacity = 1; - componentDidMount() { - this._resetter = reaction( - // automatically reset practice fields when all cards have been marked as correct - () => this.carouselItems.length, - itemsCount => { - if (this.layoutDoc.filterOp === cardMode.PRACTICE && !itemsCount) { - this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode - this.carouselItems.forEach(item => { // reset all the practice values - item[this.practiceField] = undefined; - }); - } - } // prettier-ignore - ); - } componentWillUnmount() { this._dropDisposer?.(); - this._resetter?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { @@ -68,23 +57,45 @@ 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 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 => { - switch (StrCast(this.layoutDoc.filterOp)) { - case cardMode.STAR: return !!doc[this.starField]; // show only cards that are starred - case cardMode.PRACTICE: return doc[this.practiceField] !== practiceVal.CORRECT;// show only cards that aren't marked as correct - default: return true; - } // 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 + } // prettier-ignore + /** + * Move forward or backward the specified number of Docs + * @param dir signed number indicating Docs to move forward or backward + */ move = action((dir: number) => { this._last_index = this.carouselIndex; - this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length; + this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0; }); /** @@ -104,9 +115,9 @@ export class CollectionCarouselView extends CollectionSubView() { }; /* - * Stars the document when the star button is pressed. + * Toggles whether the 'star' metadata field is set on the current Doc */ - star = (e: React.MouseEvent) => { + toggleStar = (e: React.MouseEvent) => { e.stopPropagation(); const curDoc = this.carouselItems[this.carouselIndex]; if (curDoc) { @@ -125,25 +136,28 @@ export class CollectionCarouselView extends CollectionSubView() { 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)); + }; + 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); }; - panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0); + 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; - specificMenu = (): void => { - const cm = ContextMenu.Instance; - const revealOptions = cm.findByDescription('Filter Flashcards'); - const revealItems = revealOptions?.subitems ?? []; - revealItems.push({description: 'All', event: () => {this.layoutDoc.filterOp = undefined;}, icon: 'layer-group',}); // prettier-ignore - revealItems.push({description: 'Star', event: () => {this.layoutDoc.filterOp = cardMode.STAR;}, icon: 'star',}); // prettier-ignore - revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = cardMode.PRACTICE;}, icon: 'check',}); // prettier-ignore - revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore - !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); - }; + contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)); + + contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); isChildContentActive = () => this._props.isContentActive?.() === false @@ -166,9 +180,9 @@ export class CollectionCarouselView extends CollectionSubView() { NativeHeight={returnZero} fitWidth={this._props.childLayoutFitWidth} showTags={true} + hideFilterStatus={true} containerViewPath={this.childContainerViewPath} setContentViewBox={undefined} - ScreenToLocalTransform={this.childScreenToLocalXf} onDoubleClickScript={this.onContentDoubleClick} onClickScript={this.onContentClick} isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} @@ -178,7 +192,12 @@ export class CollectionCarouselView extends CollectionSubView() { LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} - PanelHeight={this.panelHeight} + childFilters={this.childDocFilters} + hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} + addDocument={this._props.addDocument} + ScreenToLocalTransform={this.contentScreentToLocalXf} + PanelWidth={this.contentPanelWidth} + PanelHeight={this.contentPanelHeight} /> ); }; @@ -238,58 +257,137 @@ export class CollectionCarouselView extends CollectionSubView() { </> ); } + + addFlashcard() { + const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 }); + this.addDocument?.(newDoc); + } + @computed get buttons() { if (!this.carouselItems?.[this.carouselIndex]) return null; return ( <> + <div> + {/* <Tooltip title="add new flashcard to pile"> + <div key="add" className="carouselView-add" onClick={this.addFlashcard}> + <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> + </div> + </Tooltip> */} + </div> <div key="back" className="carouselView-back" onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> <div key="fwd" className="carouselView-fwd" onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> - <FontAwesomeIcon icon="xmark" color="red" size="1x" /> - </div> - <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> - <FontAwesomeIcon icon="check" color="green" size="1x" /> - </div> + {this.practiceMode == practiceMode.PRACTICE ? ( + <div> + <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} </> ); } - /** - * Prompts user to add more flashcaards if they are in practice mode but there are no flashcards - */ - renderAddFlashcards = () => <p - className="collectionCarouselView-addFlashcards" - style={{display: !this.carouselItems?.[this.carouselIndex] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none'}}> - Add flashcards! - </p> // prettier-ignore + 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'); - /** - * Displays message that a flashcard was recently missed if it had previously been marked as wrong. - * */ - renderRecentlyMissed = () => <p - className="collectionCarouselView-recentlyMissed" - style={{display: this.carouselItems?.[this.carouselIndex]?.[this.practiceField] === practiceVal.MISSED ? 'block' : 'none'}}> - Recently missed! - </p> // prettier-ignore + @computed get filterDoc() { + return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); + } + filterHeight = () => NumCast(this.filterDoc?.height); + filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); + @computed get menu() { + const curDoc = this.carouselItems?.[this.carouselIndex]; + return ( + <div className="carouselView-menu"> + {!this.filterDoc ? null : ( + <div style={{ height: this.filterHeight(), width: this.filterHeight() }}> + <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.contentScreentToLocalXf} + PanelWidth={this.filterWidth} + PanelHeight={this.filterHeight} + /> + </div> + )} + <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}> + <Tooltip title="Practice flashcards using GPT"> + <div key="back" className="carouselView-quiz" onClick={e => 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" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}> + <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" /> + </div> + </Tooltip> + </div> + </div> + ); + } render() { return ( - <div - className="collectionCarouselView-outer" - ref={this.createDashEventsTarget} - onContextMenu={this.specificMenu} - 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, - }}> - {this.content} - {this.renderAddFlashcards()} - {this.renderRecentlyMissed()} - {this.Document._chromeHidden ? null : this.buttons} + <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.buttons : null} </div> ); } |
