diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 3 | ||||
| -rw-r--r-- | src/client/views/nodes/scrapbook/ScrapbookBox.tsx | 165 | ||||
| -rw-r--r-- | src/client/views/nodes/scrapbook/ScrapbookPicker.scss | 40 | ||||
| -rw-r--r-- | src/client/views/nodes/scrapbook/ScrapbookPicker.tsx | 84 | ||||
| -rw-r--r-- | src/client/views/nodes/scrapbook/ScrapbookPreset.tsx | 44 | ||||
| -rw-r--r-- | src/client/views/nodes/scrapbook/scrapbookleftover.ts | 46 |
7 files changed, 288 insertions, 100 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9067f7e0c..2473f1c0a 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -141,6 +141,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { that describes the image.` ); + const { nativeWidth, nativeHeight } = this.nativeSize; + const aspectRatio = nativeWidth && nativeHeight + ? (nativeWidth / nativeHeight).toFixed(2) + : '1.00'; + // 4) normalize and prefix const label = raw .trim() @@ -152,6 +157,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.Document.$tags_chat = new List<string>(); tokens.forEach(tok => { (this.Document.$tags_chat as List<string>).push(tok)}); + (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspectRatio}`); //!!! changed may 11 (this.Document.$tags_chat as List<string>).push(label); // 6) flip on “show tags” in the layout diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 0e7afbab1..404be6a1b 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -151,7 +151,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const tokens = label.split(/\s+/); this.Document.$tags_chat = new List<string>(); tokens.forEach(tok => (this.Document.$tags_chat as List<string>).push(tok)); - + const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1); + (this.Document.$tags_chat as List<string>).push(`ASPECT_${aspect}`); // 5) Turn on tag display in layout this.Document._layout_showTags = true; diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx index 5dd02295c..eb997024b 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { emptyFunction } from '../../../../Utils'; -import axios from 'axios'; import { Docs } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; import { CollectionView } from '../../collections/CollectionView'; @@ -35,54 +34,51 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Do for (const cfg of configs) { if (cfg.children && cfg.children.length) { - // --- nested container --- const childDocs = cfg.children.map(child => { const doc = Docs.Create.TextDocument("[placeholder] " + child.tag); doc.accepts_docType = child.type; + doc.accepts_tagType = new List<string>(child.acceptTags ?? [child.tag]); const ph = new Doc(); - ph.proto = doc; + ph.proto = doc; ph.original = doc; - ph.x = child.x; - ph.y = child.y; - if (child.width != null) ph._width = child.width; + ph.x = child.x; + ph.y = child.y; + if (child.width != null) ph._width = child.width; if (child.height != null) ph._height = child.height; return ph; }); - // wrap those children in a stacking container const protoW = cfg.containerWidth ?? cfg.width; const protoH = cfg.containerHeight ?? cfg.height; - const containerProto = Docs.Create.StackingDocument( - childDocs, - { - ...(protoW != null ? { _width: protoW } : {}), - ...(protoH != null ? { _height: protoH } : {}), - title: cfg.tag - } - ); + // Create a stacking document with the child placeholders + const containerProto = Docs.Create.StackingDocument(childDocs, { + ...(protoW != null ? { _width: protoW } : {}), + ...(protoH != null ? { _height: protoH } : {}), + title: cfg.tag, + }); const ph = new Doc(); - ph.proto = containerProto; + ph.proto = containerProto; ph.original = containerProto; - ph.x = cfg.x; - ph.y = cfg.y; - if (cfg.width != null) ph._width = cfg.width; + ph.x = cfg.x; + ph.y = cfg.y; + if (cfg.width != null) ph._width = cfg.width; if (cfg.height != null) ph._height = cfg.height; placeholders.push(ph); - - } else { - // --- flat placeholder --- + } + + else { const doc = Docs.Create.TextDocument("[placeholder] " + cfg.tag); doc.accepts_docType = cfg.type; - doc.accepts_tagType = cfg.acceptTag ?? cfg.tag; + doc.accepts_tagType = new List<string>(cfg.acceptTags ?? [cfg.tag]); const ph = new Doc(); - ph.proto = doc; + ph.proto = doc; ph.original = doc; - ph.x = cfg.x; - ph.y = cfg.y; - if (cfg.width != null) ph._width = cfg.width; + ph.x = cfg.x; + ph.y = cfg.y; + if (cfg.width != null) ph._width = cfg.width; if (cfg.height != null) ph._height = cfg.height; placeholders.push(ph); } @@ -90,7 +86,54 @@ export function buildPlaceholdersFromConfigs(configs: ScrapbookItemConfig[]): Do return placeholders; } -// Scrapbook view: a container that lays out its child items in a grid/template +export function slotRealDocIntoPlaceholders( + realDoc: Doc, + placeholders: Doc[] +): boolean { + const realTags = new Set<string>( + StrListCast(realDoc.$tags_chat ?? new List<string>()) + .map(t => t.toLowerCase()) +); + + // Find placeholder with most matching tags + let bestMatch: Doc | null = null; + let maxMatches = 0; +/* + (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type + RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type)))*/ + + placeholders.forEach(ph => { + // 1) Enforce that placeholder.accepts_docType === realDoc.$type + if (ph.accepts_docType !== realDoc.$type) { + // Skip this placeholder entirely if types do not match. + return; + }; + const phTagTypes = StrListCast(ph.accepts_tagType ?? new List<string>()) + .map(t => t.toLowerCase()); + console.log({ realTags, phTagTypes }); + const matches = phTagTypes.filter(tag => realTags.has(tag)); + + if (matches.length > maxMatches) { + maxMatches = matches.length; + bestMatch = ph; + } + + }); + + if (bestMatch && maxMatches > 0) { + setTimeout( + undoable(() => { + bestMatch!.proto = realDoc; + }, 'Scrapbook add'), + 0 + ); + return true; + } + + return false; +} + +// Scrapbook view: a container that lays out its child items in a template @observer export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @observable selectedPreset = getPresetNames()[0]; @@ -406,56 +449,19 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() // 3) produce a flat list of every doc, unwrapping any number of nested COLs const allDocs: Doc[] = unwrap(slots); - + if (docs?.length === 1) { + return slotRealDocIntoPlaceholders( + docs[0], + allDocs, + ) + ? false + : false; + } - if (docs?.length === 1) { - const placeholder = allDocs.filter(d => + return false; +}; + - (d.accepts_docType === docs[0].$type || // match fields based on type, or by analyzing content .. simple example of matching text in placeholder to dropped doc's type - RTFCast(d[Doc.LayoutDataKey(d)])?.Text.includes(StrCast(docs[0].$type))) - ); // prettier-ignore - - //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)]) - - if (placeholder) { - /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/ - // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it. - // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo. - setTimeout( - undoable(() => { - - const slotTagsList: Set<string>[] = placeholder.map(doc => - new Set<string>(StrListCast(doc.$tags_chat)) - ); - // turn docs[0].$tags_chat into a Set - const targetTags = new Set(StrListCast(docs[0].$tags_chat)); - - //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); // // shouldn't need to do this for layout fields since the placeholder already overrides its protos - - // find the first placeholder that shares *any* tag - const match = placeholder.find(ph => - ph.accepts_tagType != null && // make sure it actually has one - targetTags.has(StrCast(ph.accepts_tagType)) // test membership in the Set - //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag)) - ); - if (match) { - match.proto = docs[0]; - } - - /*const chosenPlaceholder = placeholder.find(d => - pl = new Set<string>(StrListCast(d.$tags_chat) - - d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0] - if (chosenPlaceholder){ - chosenPlaceholder.proto = docs[0];}*/ - //excess if statement?? - }, 'Scrapbook add') - ); - return false; - } - } - return false; - }; @@ -474,10 +480,15 @@ export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() allDocs.forEach(doc => { const tags = StrListCast(doc.$tags_chat ?? new List<string>()); - tags.forEach(tag => internalTagsSet.add(tag)); + tags.forEach(tag => + {if (!tag.startsWith("ASPECT_")) { + internalTagsSet.add(tag); + } + }); }); const internalTags = Array.from(internalTagsSet).join(', '); + return internalTags ? `Create a new scrapbook background featuring: ${internalTags}` diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.scss b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss new file mode 100644 index 000000000..237274433 --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookPicker.scss @@ -0,0 +1,40 @@ +/* ScrapbookPicker.scss */ + +.scrapbook-picker-popup { + background: rgba(255, 0, 0, 0.5); /* semi-transparent red */ + position: absolute; /* ← make it float */ + z-index: 10000; /* so it sits above the overlay‐window’s background */ + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + padding: 8px; + min-width: 200px; /* at least give it some size */ +} + +.scrapbook-picker-close { + position: absolute; + top: 4px; + right: 8px; + cursor: pointer; + font-size: 14px; +} + +.scrapbook-picker-thumbnails { + margin-top: 24px; /* room under the close button */ + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.scrapbook-picker-thumb { + cursor: pointer; + border: 1px solid #ddd; + border-radius: 2px; + padding: 4px; + background: #f9f9f9; +} + +.scrapbook-picker-thumb-inner { + font-size: 12px; + text-align: center; +} diff --git a/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx b/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx new file mode 100644 index 000000000..6054cb98d --- /dev/null +++ b/src/client/views/nodes/scrapbook/ScrapbookPicker.tsx @@ -0,0 +1,84 @@ +// src/client/views/nodes/scrapbook/ScrapbookPicker.tsx +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { Doc } from '../../../../fields/Doc'; +import { Id } from '../../../../fields/FieldSymbols'; +import { StrCast } from '../../../../fields/Types'; +import './ScrapbookPicker.scss'; + +export interface ScrapbookPickerProps { + choices: Doc[]; + x: number; + y: number; + onSelect: (index: number) => void; + onCancel: () => void; +} + +/** + * A floating popup that shows N “temporary” Scrapbook documents. + * When the user clicks one thumbnail, we call onSelect(i). + * When the user clicks × or outside, we call onCancel(). + * + * This component itself does not control its own visibility; MarqueeView / OverlayView will mount/unmount it. + */ +@observer +export class ScrapbookPicker extends React.Component<ScrapbookPickerProps> { + containerRef = React.createRef<HTMLDivElement>(); + + // Close when user clicks outside the popup + handleClickOutside = (e: MouseEvent) => { + if ( + this.containerRef.current && + !this.containerRef.current.contains(e.target as Node) + ) { + this.props.onCancel(); + } + }; + + componentDidMount() { + document.addEventListener('mousedown', this.handleClickOutside); + } + componentWillUnmount() { + document.removeEventListener('mousedown', this.handleClickOutside); + } + + render() { + const { choices, x, y, onSelect, onCancel } = this.props; + return ( + <div + className="scrapbook-picker-popup" + ref={this.containerRef} + style={{ + top: `${y}px`, + left: `${x}px`, + }} + > + {/* close icon */} + <div className="scrapbook-picker-close" onClick={onCancel}> + × + </div> + <div className="scrapbook-picker-thumbnails"> + {choices.map((doc, i) => { + // We simply show a small thumbnail representation of each temp scrapbook + // You could replace this with DocumentThumbnail or a custom mini‐preview. + return ( + <div + key={`${doc[Id]}_${i}`} + className="scrapbook-picker-thumb" + onClick={() => onSelect(i)} + > + {/* + For a minimal example, use the document’s title or ID as a placeholder. + In a real version, you might render a proper thumbnail/view of doc. + */} + <div className="scrapbook-picker-thumb-inner"> + {StrCast(doc.title)} + </div> + </div> + ); + })} + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx index 87821c7bf..96a8e9b5f 100644 --- a/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx +++ b/src/client/views/nodes/scrapbook/ScrapbookPreset.tsx @@ -13,7 +13,7 @@ export interface ScrapbookItemConfig { /** text shown in the placeholder bubble */ tag: string; /** what this slot actually accepts (defaults to `tag`) */ - acceptTag?: string; + acceptTags?: string[]; x: number; y: number; @@ -48,22 +48,22 @@ export class ScrapbookPreset { return [ { type: DocumentType.IMG, tag: '[placeholder] LANDSCAPE', - acceptTag: 'LANDSCAPE', + acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, { type: DocumentType.RTF, tag: '[placeholder] caption', - acceptTag: 'caption', + acceptTags: ['caption'], x: 0, y: 200, width: 250, height: 50 }, { type: DocumentType.RTF, tag: '[placeholder] lengthy description', - acceptTag: 'lengthy description', + acceptTags: ['lengthy description'], x: 280, y: -50, width: 50, height: 200 }, { type: DocumentType.IMG, tag: '[placeholder] PERSON', - acceptTag: 'PERSON', + acceptTags: ['PERSON'], x: -200, y: -100, width: 100, height: 200 }, ]; @@ -71,12 +71,12 @@ export class ScrapbookPreset { private static createGalleryPreset(): ScrapbookItemConfig[] { return [ - { type: DocumentType.IMG, tag: 'Gallery 1', acceptTag: 'LANDSCAPE', x: -150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 2', acceptTag: 'LANDSCAPE', x: 0, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 3', acceptTag: 'LANDSCAPE', x: 150, y: -150, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 4', acceptTag: 'LANDSCAPE', x: -150, y: 0, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 5', acceptTag: 'LANDSCAPE', x: 0, y: 0, width: 150, height: 150 }, - { type: DocumentType.IMG, tag: 'Gallery 6', acceptTag: 'LANDSCAPE', x: 150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 1', acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 2', acceptTags: ['LANDSCAPE'], x: 0, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 3', acceptTags: ['LANDSCAPE'], x: 150, y: -150, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 4', acceptTags: ['LANDSCAPE'], x: -150, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 5', acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 150, height: 150 }, + { type: DocumentType.IMG, tag: 'Gallery 6', acceptTags: ['LANDSCAPE'], x: 150, y: 0, width: 150, height: 150 }, ]; } @@ -85,17 +85,17 @@ export class ScrapbookPreset { return [ { type: DocumentType.IMG, tag: 'image', - acceptTag: 'LANDSCAPE', + acceptTags: ['LANDSCAPE'], x: 0, y: -100, width: 250, height: 200 }, { type: DocumentType.RTF, tag: 'summary', - acceptTag: 'caption', + acceptTags: ['caption'], x: 0, y: 200, width: 250 }, { type: DocumentType.RTF, tag: 'sidebar', - acceptTag: 'lengthy description', + acceptTags: ['lengthy description'], x: 280, y: -50, width: 50, height: 200 }, { @@ -106,7 +106,7 @@ export class ScrapbookPreset { children: [ { type: DocumentType.IMG, tag: 'image internal', - acceptTag: 'PERSON', + acceptTags: ['PERSON'], x: 0, y: 0, width: 50, height: 100 } ] @@ -118,22 +118,22 @@ export class ScrapbookPreset { return [ { type: DocumentType.IMG, tag: 'LANDSCAPE', - acceptTag: 'LANDSCAPE', + acceptTags: ['LANDSCAPE'], x: -150, y: -150, width: 150, height: 150 }, { type: DocumentType.IMG, tag: 'PERSON', - acceptTag: 'PERSON', + acceptTags: ['PERSON'], x: 0, y: -150, width: 150, height: 150 }, { type: DocumentType.RTF, tag: 'caption', - acceptTag: 'caption', + acceptTags: ['caption'], x: -150, y: 0, width: 300, height: 100 }, { type: DocumentType.RTF, tag: 'lengthy description', - acceptTag: 'lengthy description', + acceptTags: ['lengthy description'], x: 0, y: 100, width: 300, height: 100 } ]; @@ -143,17 +143,17 @@ export class ScrapbookPreset { return [ { type: DocumentType.RTF, tag: 'title', - acceptTag: 'title', + acceptTags: ['title'], x: 0, y: -180, width: 300, height: 40 }, { type: DocumentType.IMG, tag: 'LANDSCAPE', - acceptTag: 'LANDSCAPE', + acceptTags: ['LANDSCAPE'], x: 0, y: 0, width: 300, height: 200 }, { type: DocumentType.RTF, tag: 'caption', - acceptTag: 'caption', + acceptTags: ['caption'], x: 0, y: 230, width: 300, height: 50 } ]; diff --git a/src/client/views/nodes/scrapbook/scrapbookleftover.ts b/src/client/views/nodes/scrapbook/scrapbookleftover.ts new file mode 100644 index 000000000..2f381ab95 --- /dev/null +++ b/src/client/views/nodes/scrapbook/scrapbookleftover.ts @@ -0,0 +1,46 @@ + + + + + //DocListCast(this.Document.items).map(doc => DocListCast(doc[Doc.LayoutDataKey(doc)]) + + if (placeholder) { + /**Look at the autotags and see what matches*RTFCast(d[Doc.LayoutDataKey(d)])?.Text*/ + // ugh. we have to tell the underlying view not to add the Doc so that we can add it where we want it. + // However, returning 'false' triggers an undo. so this settimeout is needed to make the assignment happen after the undo. + setTimeout( + undoable(() => { + + const slotTagsList: Set<string>[] = placeholder.map(doc => + new Set<string>(StrListCast(doc.$tags_chat)) + ); + // turn docs[0].$tags_chat into a Set + const targetTags = new Set(StrListCast(docs[0].$tags_chat)); + + //StrListCast(placeholder.overrideFields).map(field => (docs[0][field] = placeholder[field])); + // // // shouldn't need to do this for layout fields since the placeholder already overrides its protos + + // find the first placeholder that shares *any* tag + const match = placeholder.find(ph => + ph.accepts_tagType != null && // make sure it actually has one + targetTags.has(StrCast(ph.accepts_tagType)) // test membership in the Set + //StrListCast(ph.$tags_chat).some(tag => targetTags.has(tag)) + ); + if (match) { + match.proto = docs[0]; + } + + /*const chosenPlaceholder = placeholder.find(d => + pl = new Set<string>(StrListCast(d.$tags_chat) + + d.$tags_chat && d.$tags_chat[0].equals(docs[0].$tags_chat)); //why [0] + if (chosenPlaceholder){ + chosenPlaceholder.proto = docs[0];}*/ + //excess if statement?? + }, 'Scrapbook add') + ); + return false; + } + } + return false; + };
\ No newline at end of file |
