aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/CollectionCarouselView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/CollectionCarouselView.tsx')
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx316
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>
);
}