diff options
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 59 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCardDeckView.scss | 25 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCardDeckView.tsx | 859 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 105 |
4 files changed, 373 insertions, 675 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 89f1375bc..f2a7f953d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -715,48 +715,35 @@ pie title Minerals in my tap water ] } static cardTools(): Button[] { - - - // if (view != undefined){ - // for (let i=0; i< NumCast(view.layoutDoc.customSortCount); i++){ - // customs.push( - // { title: "Custom " + i, icon:"robot", toolTip:"Custom sort option " + i, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"custom", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}} - // ) - - // } - // } return [ - { title: "Time", icon:"hourglass-half", toolTip:"Sort by most recent document creation", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "Smart Sort", icon:"robot", toolTip:"Have ChatGPT sort your text-based nodes !", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"chat", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "Time", icon:"hourglass-half", toolTip:"Sort by most recent document creation", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + ] + } + static labelTools(): Button[] { + return [ + { title: "AI", icon:"robot", toolTip:"Add AI labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"chat", funcs: {hidden:`showFreeform ("chat", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "AIs", icon:"AI Sort", toolTip:"Filter AI labels", subMenu: this.cardGroupTools("chat"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("chat", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, + { title: "Like", icon:"heart", toolTip:"Add Like labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"like", funcs: {hidden:`showFreeform ("like", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "Likes", icon:"Likes", toolTip:"Filter likes", width: 10, subMenu: this.cardGroupTools("heart"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("like", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, + { title: "Star", icon:"star", toolTip:"Add Star labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"star", funcs: {hidden:`showFreeform ("star", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "Stars", icon:"Stars", toolTip:"Filter stars", width: 80, subMenu: this.cardGroupTools("star"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("star", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, + { title: "Idea", icon:"satellite", toolTip:"Add Idea labels", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"idea", funcs: {hidden:`showFreeform ("idea", true)`},scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "Ideas", icon:"Ideas", toolTip:"Filter ideas", width: 80, subMenu: this.cardGroupTools("satellite"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden:`!showFreeform("idea", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, ] } - - static customCardTools(): Button[] { - return [ - { title: "Create", icon:"heart", toolTip:"Create your first custom grouping!", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"custom1", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "Create", icon:"star", toolTip:"Create your second custom grouping!", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"custom2", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "Create", icon:"satellite", toolTip:"Create your third custom grouping!", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"custom3", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "Custom 1", icon: "Custom", width: 10, toolTip: "Set visibilty!", subMenu: CurrentUserUtils.cardGroupTools("heart"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!showFreeform("custom1", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "Custom 2", icon: "Custom", width: 80, toolTip: "Set visibilty!", subMenu: CurrentUserUtils.cardGroupTools("star"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!showFreeform("custom2", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "Custom 3", icon: "Custom", width: 80, toolTip: "Set visibilty!", subMenu: CurrentUserUtils.cardGroupTools("satellite"), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!showFreeform("custom3", true)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available - - ] - } - static cardGroupTools(icon: string): Button[] { return [ - { title: "", icon:icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"1", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "", icon:icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"2", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "", icon:icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"3", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - { title: "", icon:icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"4", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, - + { title: "1", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"1", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "2", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"2", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "3", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"3", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "4", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"4", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "5", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"5", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "6", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"6", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, + { title: "7", icon, toolTip:"Click to toggle visibility", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"7", funcs: {hidden:`!cardHasLabel(this.toolType)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, ] } - - - static viewTools(): Button[] { return [ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform @@ -858,7 +845,7 @@ pie title Minerals in my tap water { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available { title: "Card", icon: "Sort", toolTip: "Card sort", subMenu: CurrentUserUtils.cardTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "CustomCard", icon: "Create", toolTip: "Create custom groupings!", subMenu: CurrentUserUtils.customCardTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Label", icon: "Label", toolTip: "Assign card labels", subMenu: CurrentUserUtils.labelTools(), expertMode: false, toolType:CollectionViewType.Card, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Always available { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectionManager_selectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss index babc604b5..a089b248d 100644 --- a/src/client/views/collections/CollectionCardDeckView.scss +++ b/src/client/views/collections/CollectionCardDeckView.scss @@ -12,6 +12,7 @@ display: grid; grid-template-columns: repeat(10, 1fr); // width: 100%; + transform-origin: top left; position: absolute; align-items: center; @@ -22,15 +23,15 @@ } .card-button-container { - display: flex; + display: flex; padding: 3px; - // width: 300px; + // width: 300px; background-color: rgb(218, 218, 218); /* Background color of the container */ - border-radius: 50px; /* Rounds the corners of the container */ - transform: translateY(-50px); + border-radius: 50px; /* Rounds the corners of the container */ + transform: translateY(75px); // box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Optional: Adds shadow for depth */ - align-items: center; /* Centers buttons vertically */ - justify-content: start;/* Centers buttons horizontally */ + align-items: center; /* Centers buttons vertically */ + justify-content: start; /* Centers buttons horizontally */ } button { @@ -39,11 +40,11 @@ button { border-radius: 50%; background-color: $dark-gray; // border-color: $medium-blue; - margin: 5px; // transform: translateY(-50px); + margin: 5px; // transform: translateY(-50px); } // button:hover { -// transform: translateY(-50px); +// transform: translateY(-50px); // } // .card-wrapper::after { @@ -62,13 +63,19 @@ button { // align-items: center; // transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); - // } +.card-item-inactive, .card-item-active, .card-item { position: relative; transition: transform 0.5s ease-in-out; + display: flex; + flex-direction: column; +} + +.card-item-inactive { + opacity: 0.5; } .card-item-active { diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index b2a94c3c6..9e5668ffa 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,182 +1,173 @@ -import { ObservableMap, action, computed, makeObservable, observable } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Utils, returnFalse, returnTrue, returnZero } from '../../../Utils'; -import { Doc, DocListCast, Field, StrListCast } from '../../../fields/Doc'; +import { DashColor, Utils, numberRange, returnFalse, returnZero } from '../../../Utils'; +import { Doc, NumListCast } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; -import { NumCast, ScriptCast, StrCast, BoolCast, DocCast, RTFCast, Cast} from '../../../fields/Types'; +import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { URLField } from '../../../fields/URLField'; +import { gptImageLabel } from '../../apis/gpt/GPT'; +import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; +import { undoable } from '../../util/UndoManager'; import { StyleProp } from '../StyleProvider'; import { DocumentView } from '../nodes/DocumentView'; +import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; import './CollectionCardDeckView.scss'; import { CollectionSubView } from './CollectionSubView'; -import { Transform } from '../../util/Transform'; -import { LinkManager } from '../../util/LinkManager'; -// import Card from 'react-bootstrap/Card'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { forEach } from 'lodash'; -import { SnappingManager } from '../../util/SnappingManager'; -import { List } from '../../../fields/List'; -import { gptAPICall, gptImageLabel } from '../../apis/gpt/GPT'; -import { GPTCallType } from '../../apis/gpt/GPT'; -import { ImageField, PdfField, URLField } from '../../../fields/URLField'; -import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; -import { GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; -import { reaction } from 'mobx'; -import { NumListCast } from '../../../fields/Doc'; -import { undoBatch } from '../../util/UndoManager'; -import { CalendarContainer } from 'react-datepicker'; + +enum cardSortings { + Time = 'time', + Type = 'type', + Color = 'color', + Custom = 'custom', + None = '', +} @observer export class CollectionCardView extends CollectionSubView() { + private _dropDisposer?: DragManager.DragDropDisposer; + private _childDocumentWidth = 600; // target width of a Doc... + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _textToDoc = new Map<string, Doc>(); - @observable hoveredNodeIndex = -1; - - //key is the index in the child pair list, value is the id# for the group its in - @observable customGroupDictionary: Map<number, number>[] = [new Map<number, number>(), new Map<number, number>(), new Map<number, number>()]; - - /** - * The child documents to be rendered-- either all of them except the Links or the docs in the currently active - * custom group - */ - @computed get myChildLayoutPairs() { - let activeGroups = NumListCast(this._props.Document.visibleGroupNumbers); - let currCustom = NumCast(this._props.Document.customSortNumber); - - if (activeGroups.length <= 0) { - return this.childLayoutPairs.filter(l => l.layout.type != DocumentType.LINK); - } - - if (StrCast(this._props.Document.cardSort).includes("custom")) { - return this.childLayoutPairs.filter((l, index) => { - if (l.layout.type === DocumentType.LINK) { - return false; - } - // Get the group number for the current index from the customGroupDictionary - const groupNumber = this.getButtonGroup(currCustom, l.layout) - // Check if the group number is in the active groups - return groupNumber !== undefined && activeGroups.includes(groupNumber); - }); - } + @observable _forceChildXf = false; + @observable _isLoading = false; + @observable _hoveredNodeIndex = -1; + @observable _docRefs = new ObservableMap<Doc, DocumentView>(); + @observable _maxRowCount = 10; - // Default return for non-custom cardSort or other cases, filtering out links - return this.childLayoutPairs.filter(l => l.layout.type != DocumentType.LINK); + static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined { + return Cast(doc[groupFieldKey], 'number', null); } + static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); - @action - setHoveredNodeIndex = (index: number) => { - if (!this.isSelected(index)) { - this.hoveredNodeIndex = index; - } - }; - /** - * Translates the hovered node to the center of the screen - * @param index - * @returns - */ - translateHover = (index: number): number => { - if (this.hoveredNodeIndex == index && !this.isSelected(index)) { - return -50; + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + } catch (error) { + console.error('Error:', error); + throw error; } - return 0; }; - @action - setSelectedNodeIndex = (index: number) => { - const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]); - if (SelectionManager.IsSelected(docs[index])) { - this.setSelectedNodeIndex(index); + protected createDashEventsTarget = (ele: HTMLDivElement | null) => { + this._dropDisposer?.(); + if (ele) { + this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } }; - isSelected = (index: number): boolean => { - const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]); - return SelectionManager.IsSelected(docs[index]); - }; - - /** - * Returns all the documents except the one that's currently selected - */ - inactiveDocs = () => { - const docs = this.myChildLayoutPairs; - return docs.filter(d => !SelectionManager.IsSelected(d.layout)); - }; - - middleIndex = Math.floor(this.inactiveDocs().length / 2); - constructor(props: any) { super(props); makeObservable(this); - const pairs = this.childLayoutPairs.filter(d => d.layout.type != DocumentType.LINK); + } - //gets the values from the last time the view was loaded and puts them into their respective places in the hashmaps(s) - for (let i=0; i< pairs.length; i++){ - if (pairs[i].layout.custom1Group != undefined){ - this.customGroupDictionary[0].set(i, NumCast(pairs[i].layout.custom1Group)) - } + componentDidMount(): void { + this._disposers.sort = reaction( + () => ({ cardSort: this.cardSort, field: this.customSortField }), + ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false)) + ); + } - if (pairs[i].layout.custom2Group != undefined){ - this.customGroupDictionary[1].set(i, NumCast(pairs[i].layout.custom2Group)) - } + componentWillUnmount() { + Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); + this._dropDisposer?.(); + } - if (pairs[i].layout.custom3Group != undefined){ - this.customGroupDictionary[2].set(i, NumCast(pairs[i].layout.custom3Group)) - } + @computed get customSortField() { + return StrCast(this.Document.customSortField) as any as 'chat' | 'star' | 'idea' | 'like'; + } - if (pairs[i].layout.chatGroup != undefined){ - this.gptGroups.set(pairs[i].layout, NumCast(pairs[i].layout.chatGroup)) - this.amGPTGroups = NumCast(this._props.Document.chatAmGroups) + @computed get cardSort() { + return StrCast(this.Document.cardSort) as any as cardSortings; + } + /** + * how much to scale down the contents of the view so that everything will fit + */ + @computed get fitContentScale() { + const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount); + return (this._childDocumentWidth * length) / this._props.PanelWidth(); + } - } + @computed get translateWrapperX() { + let translate = 0; + + if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) { + translate += this.panelWidth() / 2; } - reaction( - () => this._props.Document.cardSort, - (cardSort) => { - if (cardSort === 'chat') { - this.openChatPopup(); - } - } - ); + return translate; } - private _dropDisposer?: DragManager.DragDropDisposer; + /** + * The child documents to be rendered-- either all of them except the Links or the docs in the currently active + * custom group + */ + @computed get childDocsWithoutLinks() { + const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK); + const activeGroups = NumListCast(this.Document.visibleGroupNumbers); + + if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) { + return regularDocs.filter(doc => { + // Get the group number for the current index + const groupNumber = CollectionCardView.getButtonGroup(this.customSortField, doc); + // Check if the group number is in the active groups + return groupNumber !== undefined && activeGroups.includes(groupNumber); + }); + } - componentWillUnmount() { - this._dropDisposer?.(); + // Default return for non-custom cardSort or other cases, filtering out links + return regularDocs; } - protected createDashEventsTarget = (ele: HTMLDivElement | null) => { - this._dropDisposer?.(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); + /** + * Determines the order in which the cards will be rendered depending on the current sort type + */ + @computed get sortedDocs() { + return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc)); + } + + @action + setHoveredNodeIndex = (index: number) => { + if (!SelectionManager.IsSelected(this.childDocs[index])) { + this._hoveredNodeIndex = index; } }; + /** + * Translates the hovered node to the center of the screen + * @param index + * @returns + */ + translateHover = (index: number) => (this._hoveredNodeIndex === index && !SelectionManager.IsSelected(this.childDocs[index]) ? -50 : 0); + + isSelected = (index: number) => SelectionManager.IsSelected(this.childDocs[index]); - childDocumentWidth = 600; // target width of a Doc... /** - * how much to scale down the contents of the view so that everything will fit + * Returns all the documents except the one that's currently selected */ - @computed get fitContentScale() { - if (this.myChildLayoutPairs.length < this.maxRowCount) { - length = this.myChildLayoutPairs.length; - } else { - length = this.maxRowCount; - } - return (this.childDocumentWidth * length) / this._props.PanelWidth(); - } + inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !SelectionManager.IsSelected(d)); - panelWidth = () => this.childDocumentWidth; - panelHeight = (layout: Doc) => () => (2 * (this.panelWidth() * NumCast(layout._height))) / NumCast(layout._width); + panelWidth = () => this._childDocumentWidth; + panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width); onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => (this.isContentActive() ? true : false); /** * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row - * @param amCards - * @param index - * @returns + * @param amCards + * @param index + * @returns */ rotate = (amCards: number, index: number) => { const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2)); @@ -184,7 +175,8 @@ export class CollectionCardView extends CollectionSubView() { if (amCards % 2 == 0 && possRotate == 0) { return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2))); - } else if (amCards % 2 == 0 && index > (amCards + 1) / 2) { + } + if (amCards % 2 == 0 && index > (amCards + 1) / 2) { return possRotate + stepMag; } @@ -197,26 +189,25 @@ export class CollectionCardView extends CollectionSubView() { const evenOdd = amCards % 2; const apex = (amCards - evenOdd) / 2; const stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25); - let rowOffset = 0; - if (realIndex > this.maxRowCount - 1) { - rowOffset = 400 * ((realIndex - (realIndex % this.maxRowCount)) / this.maxRowCount); + let rowOffset = 0; + if (realIndex > this._maxRowCount - 1) { + rowOffset = 400 * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount); } if (evenOdd == 1 || index < apex - 1) { return Math.abs(stepMag * (apex - index)) - rowOffset; - } else { - if (index == apex || index == apex - 1) { - return 0 - rowOffset; - } - - return Math.abs(stepMag * (apex - index - 1)) - rowOffset; } + if (index == apex || index == apex - 1) { + return 0 - rowOffset; + } + + return Math.abs(stepMag * (apex - index - 1)) - rowOffset; }; /** * Translates the selected node to the middle fo the screen - * @param index - * @returns + * @param index + * @returns */ translateSelected = (index: number): number => { // if (this.isSelected(index)) { @@ -235,125 +226,45 @@ export class CollectionCardView extends CollectionSubView() { }; /** - * Determines the order in which the cards will be rendered depending on the current sort type - */ - @computed get sortedDocsType() { - const desc = BoolCast(this.layoutDoc.sortDesc); - let sorted = []; - let docs = []; - - for (let i = 0; i < this.myChildLayoutPairs.length; i++) { - //copying everything in childlayout pairs to sorted so that i can use the sort function without altering the original list - sorted[i] = this.myChildLayoutPairs[i]; - } - - switch (this._props.Document.cardSort) { - case 'type': - // Copy and sort documents by type - return this.sort(sorted, 'type', desc); - case 'color': - return this.sort(sorted, 'color', desc); - case 'custom': - return this.sort(sorted, 'custom', desc); - case 'chat': - return this.sort(this.myChildLayoutPairs, 'gpt', BoolCast(this.layoutDoc.sortDesc)); - - default: - docs = this.myChildLayoutPairs; - return { docs }; - } - } - /** - * Converts a hex # to its hsv color value - * @param hex - * @returns - */ - hexToHsv = (hex: string): [number, number, number] => { - if (!hex) return [0, 0, 0]; // Default to black if hex is not defined - const r = parseInt(hex.slice(1, 3), 16) / 255; - const g = parseInt(hex.slice(3, 5), 16) / 255; - const b = parseInt(hex.slice(5, 7), 16) / 255; - const max = Math.max(r, g, b), - min = Math.min(r, g, b); - const d = max - min; - let h: number; - const s = max === 0 ? 0 : d / max; - const v = max; - - switch (max) { - case min: - h = 0; - break; - case r: - h = (g - b) / d + (g < b ? 6 : 0); - break; - case g: - h = (b - r) / d + 2; - break; - case b: - h = (r - g) / d + 4; - break; - default: - h = 0; - break; - } - h /= 6; - return [h, s, v]; - }; - /** * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the * front, latter cards to the back - * @param docs - * @param sortType - * @param isDesc - * @returns + * @param docs + * @param sortType + * @param isDesc + * @returns */ - sort = (docs: { layout: Doc; data: Doc }[], sortType: string, isDesc: boolean) => { + sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => { + if (sortType === cardSortings.None) return docs; docs.sort((docA, docB) => { - let typeA; - let typeB; - - switch (sortType) { - case 'color': - typeA = this.hexToHsv(StrCast(docA.layout.backgroundColor)) ?? ''; // If docA.type is undefined, use an empty string - typeB = this.hexToHsv(StrCast(docB.layout.backgroundColor)) ?? ''; // If docB.type is undefined, use an empty string - break; - - case 'custom': - typeA = this.getButtonGroup(NumCast(this._props.Document.customSortNumber), docA.layout) ?? '9999' - typeB = this.getButtonGroup(NumCast(this._props.Document.customSortNumber), docB.layout) ?? '9999' - break; - - case 'gpt': - typeA = this.getButtonGroup(99999, docA.layout, true) ?? '9999'; - typeB = this.getButtonGroup(99999, docB.layout, true) ?? '9999'; - break; - - default: - typeA = docA.layout.type ?? ''; // If docA.type is undefined, use an empty string - typeB = docB.layout.type ?? ''; // If docB.type is undefined, use an empty string - break; - } - - // Perform a basic string comparison if types are strings - let out = 0; - if (typeA < typeB) out = -1; - if (typeA > typeB) out = 1; - if (isDesc) out *= -1; // Reverse the sort order if descending is true - return out; + const [typeA, typeB] = (() => { + switch (sortType) { + case cardSortings.Time: + return [DateCast(docA.author_date)?.date ?? Date.now(), + DateCast(docB.author_date)?.date ?? Date.now()]; + case cardSortings.Color: + return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string + DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string + case cardSortings.Custom: + return [CollectionCardView.getButtonGroup(this.customSortField, docA)??0, + CollectionCardView.getButtonGroup(this.customSortField, docB)??0]; + default: return [StrCast(docA.type), // If docA.type is undefined, use an empty string + StrCast(docB.type)]; // If docB.type is undefined, use an empty string + } // prettier-ignore + })(); + + const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; + return isDesc ? -out : out; // Reverse the sort order if descending is true }); - return { docs }; + return docs; }; - - displayDoc = (childPair: { layout: Doc; data: Doc }, screenToLocalTransform: () => Transform) => { + displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => { return ( <DocumentView {...this._props} - ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(childPair.layout, r))} - Document={childPair.layout} - TemplateDataDocument={childPair.data} + ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))} + Document={doc} NativeWidth={returnZero} NativeHeight={returnZero} layout_fitWidth={returnFalse} @@ -365,382 +276,219 @@ export class CollectionCardView extends CollectionSubView() { isContentActive={this.isChildContentActive} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight(childPair.layout)} + PanelHeight={this.panelHeight(doc)} /> ); }; - @observable docRefs = new ObservableMap<Doc, DocumentView>(); - - @observable maxRowCount = 10; - /** * Determines how many cards are in the row of a card at a specific index - * @param index - * @returns + * @param index + * @returns */ - overflowAmCardsCalc(index: number) { - if (this.inactiveDocs().length < this.maxRowCount) { + overflowAmCardsCalc = (index: number) => { + if (this.inactiveDocs().length < this._maxRowCount) { return this.inactiveDocs().length; } // 13 - 3 = 10 const totalCards = this.inactiveDocs().length; // if 9 or less if (index < totalCards - (totalCards % 10)) { - return this.maxRowCount; + return this._maxRowCount; } //(3) return totalCards % 10; - } + }; /** * Determines the index a card is in in a row - * @param realIndex - * @returns + * @param realIndex + * @returns */ - overflowIndexCalc(realIndex: number) { - if (realIndex < 10) { - return realIndex; - } - - return realIndex % 10; - } + overflowIndexCalc = (realIndex: number) => realIndex % 10; /** * Translates the cards in the second rows and beyond over to the right - * @param realIndex - * @param calcIndex - * @param calcRowCards - * @returns + * @param realIndex + * @param calcIndex + * @param calcRowCards + * @returns */ - translateOverflowX(realIndex: number, calcIndex: number, calcRowCards: number) { - - if (realIndex < this.maxRowCount) { - return 0; - } + translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (10 - calcRowCards) * (this.panelWidth() / 2)); - return (10 - calcRowCards) * (this.panelWidth() / 2); - } /** * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected - * @param isHovered - * @param isSelected - * @param realIndex - * @param amCards - * @param calcRowIndex - * @returns + * @param isHovered + * @param isSelected + * @param realIndex + * @param amCards + * @param calcRowIndex + * @returns */ - calculateTranslateY(isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) { - let trans = 0; - if (isHovered) { - trans += this.translateHover(realIndex); - } - - trans += this.translateY(amCards, calcRowIndex, realIndex); - if (isSelected) { - trans = 50 * this.fitContentScale; - } - return trans; - } - - @observable _forceChildXf = false; + calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => { + if (isSelected) return 50 * this.fitContentScale; + const trans = isHovered ? this.translateHover(realIndex) : 0; + return trans + this.translateY(amCards, calcRowIndex, realIndex); + }; /** - * Actually renders all the cards + * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt + * @param childPairIndex + * @param buttonID + * @param doc */ - @computed get contentSorted() { - const sortedDocs = this.sortedDocsType.docs; - // Map sorted documents to their rendered components - return sortedDocs.map((childPair, index) => { - - const isHovered = this.hoveredNodeIndex === index; - const childPairIndex = this.childLayoutPairs.filter(d => d.layout.type != DocumentType.LINK).indexOf(childPair) - const realIndex = this.sortedDocsType.docs.filter(d => !SelectionManager.IsSelected(d.layout)).indexOf(childPair); - const calcRowIndex = this.overflowIndexCalc(realIndex); - - const amCards = this.overflowAmCardsCalc(realIndex); - - const isSelected = SelectionManager.IsSelected(childPair.layout); - - const childScreenToLocal = () => { - this._forceChildXf; - const dref = this.docRefs.get(childPair.layout); - const { translateX, translateY, scale } = Utils.GetScreenTransform(dref?.ContentDiv); - return new Transform(-translateX + (dref?.centeringX || 0) * scale, -translateY + (dref?.centeringY || 0) * scale, 1).scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); - }; - - return ( - <div - key={childPair.layout[Id]} - className={`card-item${isSelected ? '-active' : ''}`} - onPointerUp={e => { - SnappingManager.SetIsResizing(this.Document); - setTimeout( - action(() => { - SnappingManager.SetIsResizing(undefined); - this._forceChildXf = !this._forceChildXf; - }), - 700 - ); - }} - style={{ - width: this.panelWidth(), - height: this.panelHeight(childPair.layout)(), - transform: ` - translateY(${this.calculateTranslateY(isHovered, isSelected, realIndex, amCards, calcRowIndex)}px) - translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, calcRowIndex, amCards)}px) - rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg) - scale(${isSelected ? 1.25 : 1}) - `, - }} - onMouseEnter={() => this.setHoveredNodeIndex(index)}> - {this.displayDoc(childPair, childScreenToLocal)} - - {this._props.Document.cardSort == 'custom' ? this.renderButtons(childPairIndex, childPair.layout, false) : ''} - {this._props.Document.cardSort == 'chat' ? this.renderButtons(childPairIndex, childPair.layout, true) : ''} - - </div> - ); - }); - } + toggleButton = undoable((buttonID: number, doc: Doc) => this.customSortField && (doc[this.customSortField] = buttonID), 'toggle custom button'); /** - * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt - * @param childPairIndex - * @param buttonID - * @param isChat - * @param doc - */ - - @undoBatch - @action toggleButton(childPairIndex: number, buttonID: number, isChat = false, doc: Doc) { - if (!isChat) { - const sortNumber = NumCast(this._props.Document.customSortNumber) - this.customGroupDictionary[sortNumber].set(childPairIndex, buttonID) - switch (sortNumber){ - case 0: - doc.custom1Group = buttonID - break - case 1: - doc.custom2Group = buttonID - break - case 2: - doc.custom3Group = buttonID - break - default: - break - } - } - - if (isChat && doc) { - this.gptGroups.set(doc, buttonID); - doc.chatGroup = buttonID - } - } - - /** - * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words. + * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words. * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is - * inputted into the gpt prompt to sort everything together - * @returns + * inputted into the gpt prompt to sort everything together + * @returns */ - async childPairStringList(): Promise<string> { - let string = ""; - for (let i = 0; i < this.childDocs.length; i++) { - switch (this.childDocs[i].type) { - case DocumentType.IMG: - string += `======${await this.getImageDesc(this.childDocs[i])}======`; - break; - - case DocumentType.PDF: - let pdfText = StrCast(this.childDocs[i].text); - let words = pdfText.split(/\s+/); - let first50Words = words.slice(0, 50); - string += `======${first50Words.join(' ')}======`; - - this.textToDoc.set(first50Words.join(' ').trim(), this.childDocs[i]); - break; - - case DocumentType.RTF: - let rtfText = StrCast((RTFCast(this.childDocs[i].text)).Text); - string += `======${StrCast((RTFCast(this.childDocs[i].text)).Text)}======`; - this.textToDoc.set(rtfText.trim(), this.childDocs[i]); - break; - - default: - string += `======${StrCast(this.childDocs[i].title)}======`; - this.textToDoc.set(StrCast(this.childDocs[i].title).trim(), this.childDocs[i]); - } - } - return string; - } - - @observable isLoading = false - - - imageUrlToBase64 = async (imageUrl: string): Promise<string> => { - try { - const response = await fetch(imageUrl); - const blob = await response.blob(); - - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => resolve(reader.result as string); - reader.onerror = error => reject(error); - }); - } catch (error) { - console.error('Error:', error); - throw error; - } + childPairStringList = () => { + const docToText = (doc: Doc) => { + switch (doc.type) { + case DocumentType.PDF: const words = StrCast(doc.text).split(/\s+/); + return words.slice(0, 50).join(' '); // first 50 words of pdf text + case DocumentType.IMG: return this.getImageDesc(doc); + case DocumentType.RTF: return StrCast(RTFCast(doc.text).Text); + default: return StrCast(doc.title); + } // prettier-ignore + }; + const docTextPromises = this.childDocsWithoutLinks.map(async doc => { + const docText = (await docToText(doc)) ?? ''; + this._textToDoc.set(docText.trim(), doc); + return `======${docText.replace(/\n/g, ' ').trim()}======`; + }); + return Promise.all<string>(docTextPromises); }; - textToDoc = new Map<string, Doc>() - gptProccessedImages = new Set<Doc>() - /** * Calls the gpt API to generate descriptions for the images in the view - * @param image - * @returns + * @param image + * @returns */ - @action async getImageDesc(image: Doc) { - if (this.gptProccessedImages.has(image)) { - // Return the already processed description - return Array.from(this.textToDoc.keys()).find(key => this.textToDoc.get(key) === image) || ''; - } - - let href = (image['data'] as URLField).url.href; - let hrefParts = href.split('.'); - let hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; + getImageDesc = async (image: Doc) => { + if (StrCast(image.description)) return StrCast(image.description); // Return existing description + const href = (image.data as URLField).url.href; + const hrefParts = href.split('.'); + const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; try { - let hrefBase64 = await this.imageUrlToBase64(hrefComplete); - let response = await gptImageLabel(hrefBase64); - this.textToDoc.set(response.trim(), image); - this.gptProccessedImages.add(image); - console.log(response); + const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete); + const response = await gptImageLabel(hrefBase64); + image[DocData].description = response.trim(); return response; // Return the response from gptImageLabel } catch (error) { - console.log("bad things have happened"); + console.log('bad things have happened'); } - } + return ''; + }; - //child doc to its group determind by gpt - gptGroups = new ObservableMap<Doc, number> - @observable amGPTGroups = 0 - /** * Converts the gpt output into a hashmap that can be used for sorting. lists are seperated by ==== while elements within the list are seperated by ~~~~~~ - * @param gptOutput + * @param gptOutput */ - processGptOutput(gptOutput: string) { - + processGptOutput = (gptOutput: string) => { // Split the string into individual list items const listItems = gptOutput.split('======').filter(item => item.trim() !== ''); - this.amGPTGroups = listItems.length - this._props.Document.chatAmGroups = this.amGPTGroups - listItems.forEach((item, index) => { // Split the item by '~~~~~~' to get all descriptors const parts = item.split('~~~~~~').map(part => part.trim()); parts.forEach(part => { - console.log(part + "part") // Find the corresponding Doc in the textToDoc map - if (this.textToDoc.has(part)) { - const doc = this.textToDoc.get(part); - if (doc) { - this.gptGroups.set(doc, index); - doc.chatGroup = index - } + const doc = this._textToDoc.get(part); + if (doc) { + doc.chat = index; } }); }); - } - - @observable isChatPopupOpen = false; + }; /** - * Opens up the chat popup and starts the process for smart sorting. + * Opens up the chat popup and starts the process for smart sorting. */ - @action openChatPopup = async () => { - this.isChatPopupOpen = true; + openChatPopup = async () => { GPTPopup.Instance.setVisible(true); GPTPopup.Instance.setMode(GPTPopupMode.SORT); - - // Await the promise to get the string result - const sortDesc = await this.childPairStringList(); + const sortDesc = await this.childPairStringList(); // Await the promise to get the string result GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded - GPTPopup.Instance.setSortDesc(sortDesc); - GPTPopup.Instance.onSortComplete = this.handleGptSortResult; -}; - @action handleGptSortResult = (sortResult: string) => { - this.processGptOutput(sortResult); + GPTPopup.Instance.setSortDesc(sortDesc.join()); + GPTPopup.Instance.onSortComplete = (sortResult: string) => this.processGptOutput(sortResult); }; /** - * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups - * @param childPairIndex - * @param doc - * @param isChat - * @returns + * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups + * @param childPairIndex + * @param doc + * @returns */ - renderButtons(childPairIndex: number, doc: Doc, isChat = false) { - const buttons = []; - const groupNumber = NumCast(this._props.Document.customSortNumber); - - let amButtons = 4; - let activeButtonIndex = this.getButtonGroup(groupNumber, doc) - - if (isChat && doc) { - if (this.amGPTGroups > 4){ - amButtons = this.amGPTGroups; - } - activeButtonIndex = this.getButtonGroup(9999, doc, true); - } - - for (let i = 0; i < amButtons; i++) { - const isActive = activeButtonIndex === i; - buttons.push( - <button - key={i} - style={{ backgroundColor: isActive ? '#4476f7' : '#323232' }} - onClick={() => this.toggleButton(childPairIndex, i, isChat, doc)} - ></button> - ); - } - + renderButtons = (doc: Doc, cardSort: cardSortings) => { + if (cardSort !== cardSortings.Custom) return ''; + const amButtons = Math.max(4, this.childDocs?.reduce((set, doc) => this.customSortField && set.add(NumCast(doc[this.customSortField])), new Set<number>()).size ?? 0); + const activeButtonIndex = CollectionCardView.getButtonGroup(this.customSortField, doc); const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6; return ( <div className="card-button-container" style={{ width: `${totalWidth}px` }}> - {buttons} + {numberRange(amButtons).map(i => ( + <button + key={i} + type="button" + style={{ backgroundColor: activeButtonIndex === i ? '#4476f7' : '#323232' }} // + onClick={() => this.toggleButton(i, doc)} + /> + ))} </div> ); - } - - getButtonGroup(groupNumber: number, doc: Doc, isChat?: boolean) { - if (isChat){ - return NumCast(doc.chatGroup) - } - switch (groupNumber){ - case 0: - return NumCast(doc.custom1Group) - case 1: - return NumCast(doc.custom2Group) - case 2: - return NumCast(doc.custom3Group) - default: - break - } - - - } - + }; + /** + * Actually renders all the cards + */ + renderCards = () => { + const anySelected = this.childDocs.some(doc => SelectionManager.IsSelected(doc)); + // Map sorted documents to their rendered components + return this.sortedDocs.map((doc, index) => { + const realIndex = this.sortedDocs.filter(sortDoc => !SelectionManager.IsSelected(sortDoc)).indexOf(doc); + const calcRowIndex = this.overflowIndexCalc(realIndex); + const amCards = this.overflowAmCardsCalc(realIndex); + const isSelected = SelectionManager.IsSelected(doc); - @computed get translateWrapperX() { - let translate = 0; + const childScreenToLocal = () => { + this._forceChildXf; + const dref = this._docRefs.get(doc); + const { translateX, translateY, scale } = Utils.GetScreenTransform(dref?.ContentDiv); + return new Transform(-translateX + (dref?.centeringX || 0) * scale, + -translateY + (dref?.centeringY || 0) * scale, 1) + .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore + }; - if (this.inactiveDocs().length != this.myChildLayoutPairs.length && this.inactiveDocs().length < 10) { - translate += this.panelWidth() / 2; - } - return translate; - } + return ( + <div + key={doc[Id]} + className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`} + onPointerUp={() => { + // this turns off documentDecorations during a transition, then turns them back on afterward. + SnappingManager.SetIsResizing(this.Document); + setTimeout( + action(() => { + SnappingManager.SetIsResizing(undefined); + this._forceChildXf = !this._forceChildXf; + }), + 700 + ); + }} + style={{ + width: this.panelWidth(), + height: 'max-content', // this.panelHeight(childPair.layout)(), + transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px) + translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px) + rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg) + scale(${isSelected ? 1.25 : 1})`, + }} + onMouseEnter={() => this.setHoveredNodeIndex(index)}> + {this.displayDoc(doc, childScreenToLocal)} + {this.renderButtons(doc, this.cardSort)} + </div> + ); + }); + }; render() { return ( <div @@ -754,11 +502,10 @@ export class CollectionCardView extends CollectionSubView() { className="card-wrapper" style={{ transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`, - transformOrigin: 'top left', height: `${100 * this.fitContentScale}%`, }} onMouseLeave={() => this.setHoveredNodeIndex(-1)}> - {this.contentSorted} + {this.renderCards()} </div> </div> ); diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 496d7482c..35a3a2e31 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -1,7 +1,7 @@ import { Colors } from 'browndash-components'; import { action, runInAction } from 'mobx'; import { aggregateBounds } from '../../../Utils'; -import { Doc, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; @@ -106,17 +106,15 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.CollectionFreeFormDocumentView?.float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); - - ScriptingGlobals.add(function showFreeform( - attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'custom1' | 'custom2' | 'custom3' | 'chat' | '1' | '2' | '3' | '4', + attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4', checkResult?: boolean, persist?: boolean, isDoubleClick?: boolean ) { const selected = SelectionManager.Docs.lastElement(); // prettier-ignore - const map: Map<'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'custom1' | 'custom2' | 'custom3' | 'chat' | '1' | '2' | '3' | '4', + const map: Map<'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'links' | 'like' | 'star' | 'idea' | 'chat' | '1' | '2' | '3' | '4', { waitForRender?: boolean; checkResult: (doc: Doc) => any; @@ -167,87 +165,48 @@ ScriptingGlobals.add(function showFreeform( checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "links", setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "links", }], - ['custom1', { - checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && NumCast(doc?.customSortNumber) === 0, + ['like', { + checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "like", setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort = "custom"; - doc.customSortNumber = 0; + doc.customSortField = "like"; doc.visibleGroupNumbers = new List<number>(); } }], - ['custom2', { - checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && NumCast(doc?.customSortNumber) === 1, + ['star', { + checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "star", setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort = "custom"; - doc.customSortNumber = 1; + doc.customSortField = "star"; doc.visibleGroupNumbers = new List<number>(); } }], - ['custom3', { - checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && NumCast(doc?.customSortNumber) === 2, + ['idea', { + checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "idea", setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort = "custom"; - doc.customSortNumber = 2; + doc.customSortField = "idea"; doc.visibleGroupNumbers = new List<number>(); } }], ['chat', { - checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "chat", - setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "chat", - }], - ['1', { - checkResult: (doc: Doc) => NumListCast(doc?.visibleGroupNumbers).includes(0), - setDoc: (doc: Doc, dv: DocumentView) => { - let list = NumListCast(doc.visibleGroupNumbers); - if (list.includes(0)) { - let newList = new List<number>(list.filter(d => d !== 0)); - doc.visibleGroupNumbers = newList; - } else { - list.push(0); - doc.visibleGroupNumbers = new List<number>(list); - } - } - }], - ['2', { - checkResult: (doc: Doc) => NumListCast(doc?.visibleGroupNumbers).includes(1), - setDoc: (doc: Doc, dv: DocumentView) => { - let list = NumListCast(doc.visibleGroupNumbers); - if (list.includes(1)) { - let newList = new List<number>(list.filter(d => d !== 1)); - doc.visibleGroupNumbers = newList; - } else { - list.push(1); - doc.visibleGroupNumbers = new List<number>(list); - } - } - }], - ['3', { - checkResult: (doc: Doc) => NumListCast(doc?.visibleGroupNumbers).includes(2), - setDoc: (doc: Doc, dv: DocumentView) => { - let list = NumListCast(doc.visibleGroupNumbers); - if (list.includes(2)) { - let newList = new List<number>(list.filter(d => d !== 2)); - doc.visibleGroupNumbers = newList; - } else { - list.push(2); - doc.visibleGroupNumbers = new List<number>(list); - } - } - }], - ['4', { - checkResult: (doc: Doc) => NumListCast(doc?.visibleGroupNumbers).includes(3), + checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "chat", setDoc: (doc: Doc, dv: DocumentView) => { - let list = NumListCast(doc.visibleGroupNumbers); - if (list.includes(3)) { - let newList = new List<number>(list.filter(d => d !== 3)); - doc.visibleGroupNumbers = newList; - } else { - list.push(3); - doc.visibleGroupNumbers = new List<number>(list); - } - } + doc.cardSort = "custom"; + doc.customSortField = "chat"; + doc.visibleGroupNumbers = new List<number>(); + }, }], ]); + for (let i = 0; i < 8; i++) { + map.set((i + 1 + '') as any, { + checkResult: (doc: Doc) => NumListCast(doc?.visibleGroupNumbers).includes(i), + setDoc: (doc: Doc, dv: DocumentView) => { + const list = NumListCast(doc.visibleGroupNumbers); + doc.visibleGroupNumbers = new List<number>(list.includes(i) ? list.filter(d => d !== i) : [...list, i]); + }, + }); + } if (checkResult) { return map.get(attr)?.checkResult(selected); @@ -257,13 +216,11 @@ ScriptingGlobals.add(function showFreeform( setTimeout(() => batch.end(), 100); }); - -ScriptingGlobals.add(function isThatCardGroup(n: number){ - const canvas = SelectionManager.Docs.lastElement(); - return canvas.customSortNumber == n - - -}) +ScriptingGlobals.add(function cardHasLabel(label: string) { + const selected = SelectionManager.Docs.lastElement(); + const labelNum = Number(label) - 1; + return labelNum < 4 || (selected && DocListCast(selected[Doc.LayoutFieldKey(selected)]).some(doc => doc[StrCast(selected.customSortField)] == labelNum)); +}, ''); // ScriptingGlobals.add(function setCardSortAttr(attr: 'time' | 'docType' | 'color', value: any, checkResult?: boolean) { // // const editorView = RichTextMenu.Instance?.TextView?.EditorView; |