aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/documents/DocUtils.ts53
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts47
-rw-r--r--src/client/util/CurrentUserUtils.ts3
-rw-r--r--src/client/views/DocumentDecorations.tsx163
-rw-r--r--src/client/views/InkingStroke.tsx2
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx5
-rw-r--r--src/client/views/collections/CollectionSubView.tsx3
-rw-r--r--src/client/views/collections/CollectionView.tsx20
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx2
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx1
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx1
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx1
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx1
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.scss32
-rw-r--r--src/client/views/nodes/ImageBox.tsx347
-rw-r--r--src/client/views/nodes/scrapbook/EmbeddedDocView.tsx52
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookBox.tsx111
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookContent.tsx23
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.scss85
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlot.tsx28
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts25
-rw-r--r--src/client/views/nodes/scrapbook/ScrapbookVersionTwo.tsx125
-rw-r--r--src/fields/Doc.ts24
-rw-r--r--src/server/ApiManagers/FireflyManager.ts97
27 files changed, 1055 insertions, 201 deletions
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 7b36c3a66..df14dce5a 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -308,6 +308,7 @@ export namespace DocUtils {
*/
export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> {
let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined;
+
if (type.indexOf('image') !== -1) {
ctor = Docs.Create.ImageDocument;
if (!options._width) options._width = 300;
@@ -711,33 +712,31 @@ export namespace DocUtils {
nativeWidth: 40,
nativeHeight: 40,
})
- : (defaultTextTemplate?.type === DocumentType.JOURNAL ? Docs.Create.DailyJournalDocument:Docs.Create.TextDocument)(
- '',
- {
- annotationOn,
- backgroundColor,
- x,
- y,
- title,
- ...(defaultTextTemplate
- ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
- : {
- _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
- _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
- _layout_autoHeight: true,
- backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
- borderColor: Doc.UserDoc().borderColor as string,
- borderWidth: Doc.UserDoc().borderWidth as number,
- text_centered: BoolCast(Doc.UserDoc().textCentered),
- text_fitBox: BoolCast(Doc.UserDoc().fitBox),
- text_align: StrCast(Doc.UserDoc().textAlign),
- text_fontColor: StrCast(Doc.UserDoc().fontColor),
- text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
- text_fontWeight: StrCast(Doc.UserDoc().fontWeight),
- text_fontStyle: StrCast(Doc.UserDoc().fontStyle),
- text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration),
- }),
- });
+ : (defaultTextTemplate?.type === DocumentType.JOURNAL ? Docs.Create.DailyJournalDocument : Docs.Create.TextDocument)('', {
+ annotationOn,
+ backgroundColor,
+ x,
+ y,
+ title,
+ ...(defaultTextTemplate
+ ? {} // if the new doc will inherit from a template, don't set any layout fields since that would block the inheritance
+ : {
+ _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200,
+ _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35,
+ _layout_autoHeight: true,
+ backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor),
+ borderColor: Doc.UserDoc().borderColor as string,
+ borderWidth: Doc.UserDoc().borderWidth as number,
+ text_centered: BoolCast(Doc.UserDoc().textCentered),
+ text_fitBox: BoolCast(Doc.UserDoc().fitBox),
+ text_align: StrCast(Doc.UserDoc().textAlign),
+ text_fontColor: StrCast(Doc.UserDoc().fontColor),
+ text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ text_fontWeight: StrCast(Doc.UserDoc().fontWeight),
+ text_fontStyle: StrCast(Doc.UserDoc().fontStyle),
+ text_fontDecoration: StrCast(Doc.UserDoc().fontDecoration),
+ }),
+ });
if (defaultTextTemplate) {
tbox.layout_fieldKey = 'layout_' + StrCast(defaultTextTemplate.title);
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 5c6559836..cef44e999 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -44,6 +44,7 @@ export enum DocumentType {
SCRIPTDB = 'scriptdb', // database of scripts
GROUPDB = 'groupdb', // database of groups
+ SCRAPBOOK = 'scrapbook',
JOURNAL = 'journal', // AARAV ADD
}
export enum CollectionViewType {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index bf9cc5bd4..2a2f9d342 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -525,6 +525,19 @@ export class DocumentOptions {
ai?: string; // to mark items as ai generated
ai_firefly_seed?: number;
ai_firefly_prompt?: string;
+
+ /**
+ * JSON‐stringified slot configuration for ScrapbookBox
+ */
+ scrapbookConfig?: string;
+
+ /**
+ * The list of embedded Doc instances in each Scrapbook slot
+ */
+ scrapbookContents?: List<Doc>;
+
+ _outpaintingMetadata?: STRt = new StrInfo('serialized JSON metadata needed for image outpainting', false);
+
}
export const DocOptions = new DocumentOptions();
@@ -587,6 +600,18 @@ export namespace Docs {
},
],
// AARAV ADD //
+
+ [
+ DocumentType.SCRAPBOOK,
+ {
+ layout: { view: EmptyBox, dataField: 'text' },
+ options: {
+ title: 'Scrapbook',
+ acl_Guest: SharingPermissions.View,
+ },
+
+ },
+ ],
]);
const suffix = 'Proto';
@@ -919,6 +944,26 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
}
+ export function ScrapbookDocument(
+ items: Doc[] = [],
+ options: DocumentOptions = {},
+ fieldKey: string = 'items'
+ ) {
+ return InstanceFromProto(
+ Prototypes.get(DocumentType.SCRAPBOOK),
+ new List<Doc>(items),
+ {
+ title: options.title
+ ?? new Date().toLocaleDateString(undefined, {
+ year: 'numeric', month: 'short', day: 'numeric'
+ }),
+ ...options,
+ },
+ undefined,
+ fieldKey
+ );
+ }
+
// AARAV ADD //
export function DailyJournalDocument(text: string | RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
@@ -961,6 +1006,8 @@ export namespace Docs {
// AARAV ADD //
+
+
export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
const linkDoc = InstanceFromProto(
Prototypes.get(DocumentType.LINK),
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 99a67ebb2..8f4d568ab 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -407,6 +407,8 @@ pie title Minerals in my tap water
{key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300, }},
// AARAV ADD //
{key: "DailyJournal",creator:opts => Docs.Create.DailyJournalDocument("", opts),opts: { _width: 300, _height: 300, }},
+ {key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}},
+ //{key: "Scrapbook",creator:opts => Docs.Create.ScrapbookDocument([], opts),opts:{ _width: 300, _height: 300}},
{key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 500, _height: 500, _layout_fitWidth: true, }},
{key: "MetaNote", creator: metaNoteTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}},
{key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
@@ -449,6 +451,7 @@ pie title Minerals in my tap water
{ toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)},
{ toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
+ { toolTip: "Tap or drag to create a scrapbook template", title: "Scrapbook", icon: "palette", dragFactory: doc.emptyScrapbook as Doc,clickFactory:DocCast(doc.emptyScrapbook), },
{ toolTip: "Tap or drag to create a journal entry", title: "Journal", icon: "book", dragFactory:doc.emptyDailyJournal as Doc,clickFactory: DocCast(doc.emptyDataJournal), },
{ toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon:"person-chalkboard",dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc, clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4b7fdc560..3f11a4713 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -473,6 +473,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const tl = docView.screenToContentsTransform().inverse().transformPoint(0, 0);
return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]);
};
+
+ // Modify the onPointerMove method to handle shift+click during resize
onPointerMove = (e: PointerEvent): boolean => {
const first = DocumentView.Selected()[0];
const effectiveAcl = GetEffectiveAcl(first.Document);
@@ -488,20 +490,136 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
const { scale, refPt } = this.getResizeVals(thisPt, dragHdl);
- !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
- this._interactionLock = true;
- this._snapPt = thisPt;
- e.ctrlKey && (DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions()));
- const hasFixedAspect = DocumentView.Selected().map(dv => dv.Document).some(this.hasFixedAspect);
- const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y};
- DocumentView.Selected().forEach(docView =>
- this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore
- await new Promise<void>(res => { setTimeout(() => { res(this._interactionLock = undefined)})});
- }); // prettier-ignore
+ !this._interactionLock &&
+ runInAction(async () => {
+ // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate)
+ this._interactionLock = true;
+ this._snapPt = thisPt;
+
+ // Special handling for shift+click (outpainting mode)
+ if (e.shiftKey && DocumentView.Selected().some(dv => dv.ComponentView instanceof ImageBox)) {
+ DocumentView.Selected().forEach(docView => {
+ if (docView.ComponentView instanceof ImageBox) {
+ // Set flag for outpainting mode
+ docView.Document._outpaintingResize = true;
+
+ // Adjust only the document dimensions without scaling internal content
+ this.resizeViewForOutpainting(docView, refPt, scale, { dragHdl, shiftKey: e.shiftKey });
+ } else {
+ // Handle regular resize for non-image components
+ e.ctrlKey && !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions();
+ const hasFixedAspect = this.hasFixedAspect(docView.Document);
+ const scaleAspect = { x: scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y };
+ this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey: e.ctrlKey });
+ }
+ });
+ } else {
+ // Regular resize behavior (existing code)
+ e.ctrlKey && DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions());
+ const hasFixedAspect = DocumentView.Selected()
+ .map(dv => dv.Document)
+ .some(this.hasFixedAspect);
+ const scaleAspect = { x: scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y };
+ DocumentView.Selected().forEach(docView => this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey: e.ctrlKey }));
+ }
+
+ await new Promise<void>(res => {
+ setTimeout(() => {
+ res((this._interactionLock = undefined));
+ });
+ });
+ });
return false;
};
+ resizeViewForOutpainting = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; shiftKey: boolean }) => {
+ const doc = docView.Document;
+
+ if (doc.isGroup) {
+ DocListCast(doc.data)
+ .map(member => DocumentView.getDocumentView(member, docView)!)
+ .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts));
+ doc.xPadding = NumCast(doc.xPadding) * scale.x;
+ doc.yPadding = NumCast(doc.yPadding) * scale.y;
+ return;
+ }
+
+ if (!doc._outpaintingOriginalWidth || !doc._outpaintingOriginalHeight) {
+ doc._outpaintingOriginalWidth = NumCast(doc._width);
+ doc._outpaintingOriginalHeight = NumCast(doc._height);
+ }
+
+ // Calculate new boundary dimensions
+ const originalWidth = NumCast(doc._width);
+ const originalHeight = NumCast(doc._height);
+ const newWidth = Math.max(NumCast(doc._width_min, 25), originalWidth * scale.x);
+ const newHeight = Math.max(NumCast(doc._height_min, 10), originalHeight * scale.y);
+
+ // Apply new dimensions
+ doc._width = newWidth;
+ doc._height = newHeight;
+
+ const refCent = docView.screenToViewTransform().transformPoint(refPt[0], refPt[1]);
+ const { deltaX, deltaY } = this.realignRefPt(doc, refCent, originalWidth, originalHeight);
+ doc.x = NumCast(doc.x) + deltaX;
+ doc.y = NumCast(doc.y) + deltaY;
+
+ doc._layout_modificationDate = new DateField();
+
+ // Trigger outpainting
+ doc._needsOutpainting = true;
+
+ // Store metadata needed for outpainting
+ doc._outpaintingMetadata = JSON.stringify({
+ originalWidth: doc._outpaintingOriginalWidth,
+ originalHeight: doc._outpaintingOriginalHeight,
+ newWidth,
+ newHeight,
+ scaleX: scale.x,
+ scaleY: scale.y,
+ anchorHandle: opts.dragHdl,
+ });
+ };
+
+ @action
+ onPointerUp = (): void => {
+ SnappingManager.SetIsResizing(undefined);
+ SnappingManager.clearSnapLines();
+
+ // Check if any outpainting needs to be processed
+ DocumentView.Selected().forEach(view => {
+ if (view.Document._needsOutpainting && view.ComponentView instanceof ImageBox) {
+ // Trigger outpainting process in the ImageBox component
+ (view.ComponentView as ImageBox).processOutpainting();
+
+ // Clear flags
+ view.Document._needsOutpainting = false;
+ view.Document._outpaintingResize = false;
+ }
+ });
+
+ this._resizeHdlId = '';
+ this._resizeUndo?.end();
+
+ // detect layout_autoHeight gesture and apply
+ DocumentView.Selected().forEach(view => {
+ NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true);
+ });
+ // need to change points for resize, or else rotation/control points will fail.
+ this._inkDragDocs
+ .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
+ .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
+ doc.$data = new InkField(inkPts.map(
+ (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth)
+ X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width,
+ Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height,
+ }))); // prettier-ignore
+ Doc.SetNativeWidth(doc, undefined);
+ Doc.SetNativeHeight(doc, undefined);
+ });
+ };
+
//
// determines how much to resize, and determines the resize reference point
//
@@ -606,31 +724,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
};
};
- @action
- onPointerUp = (): void => {
- SnappingManager.SetIsResizing(undefined);
- SnappingManager.clearSnapLines();
- this._resizeHdlId = '';
- this._resizeUndo?.end();
-
- // detect layout_autoHeight gesture and apply
- DocumentView.Selected().forEach(view => {
- NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true);
- });
- // need to change points for resize, or else rotation/control points will fail.
- this._inkDragDocs
- .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
- .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
- doc.$data = new InkField(inkPts.map(
- (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth)
- X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width,
- Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height,
- }))); // prettier-ignore
- Doc.SetNativeWidth(doc, undefined);
- Doc.SetNativeHeight(doc, undefined);
- });
- };
-
@computed get selectionTitle(): string {
if (DocumentView.Selected().length === 1) {
const selected = DocumentView.Selected()[0];
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f555808ef..0a49ea28e 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -245,7 +245,7 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
* factor for converting between ink and screen space.
*/
inkScaledData = () => {
- const inkData = Cast(this.dataDoc[this.fieldKey], InkField, Cast(this.layoutDoc[this.fieldKey], InkField, null))?.inkData ?? [];
+ const inkData = Cast(this.dataDoc[this.fieldKey], InkField, Cast(this.layoutDoc[this.fieldKey], InkField, null) ?? null)?.inkData ?? [];
const inkStrokeWidth = NumCast(this.layoutDoc.stroke_width, 1);
const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2;
const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index e4bbb1c0f..b884eb8c8 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -65,6 +65,7 @@ import { PresBox, PresSlideBox } from './nodes/trails';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
import { SearchBox } from './search/SearchBox';
import { StickerPalette } from './smartdraw/StickerPalette';
+import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
dotenv.config();
@@ -134,6 +135,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
VideoBox,
AudioBox,
RecordingBox,
+ ScrapbookBox,
PresBox,
PresSlideBox,
SearchBox,
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 9155227dd..f11e646cc 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -343,7 +343,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null) ?? null);
// this is what renders the document that you see on the screen
// called in Children: this actually adds a document to our children list
- getDisplayDoc(doc: Doc, trans: () => string, count: number) {
+ getDisplayDoc = (doc: Doc, trans: () => string, count: number) => {
const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined;
this._docXfs.push({ stackedDocTransform: this.getDocTransform(doc), width: this.getDocWidth(doc), height: this.getDocHeight(doc) });
return count > this._renderCount ? null : (
@@ -384,6 +384,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
searchFilterDocs={this.searchFilterDocs}
xPadding={NumCast(this.layoutDoc._childXPadding, this._props.childXPadding)}
yPadding={NumCast(this.layoutDoc._childYPadding, this._props.childYPadding)}
+ rejectDrop={this._props.childRejectDrop}
addDocument={this._props.addDocument}
moveDocument={this._props.moveDocument}
removeDocument={this._props.removeDocument}
@@ -393,7 +394,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
pinToPres={this._props.pinToPres}
/>
);
- }
+ };
getDocTransform = computedFn((doc: Doc) => () => {
// these must be referenced for document decorations to update when the text box container is scrolled
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index bc7d6f897..9a0fda3f8 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -58,6 +58,7 @@ export interface CollectionViewProps extends React.PropsWithChildren<FieldViewPr
childOpacity?: () => number;
childContextMenuItems?: () => { script: ScriptField; label: string }[];
childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
+ childRejectDrop?: (draggedDoc: Doc[] | undefined, subView?: DocumentView) => boolean; // whether a child document can be dropped on this document
childHideDecorationTitle?: boolean;
childHideResizeHandles?: boolean;
childHideDecorations?: boolean;
@@ -323,7 +324,7 @@ export function CollectionSubView<X>() {
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
const { docDragData } = de.complete;
- if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) {
+ if (docDragData && !docDragData.draggedDocuments.includes(this.Document) && !this._props.rejectDrop?.(docDragData.draggedDocuments, this.DocumentView?.())) {
let added;
const dropAction = docDragData.dropAction || docDragData.userDropAction;
const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index c9af92a1b..eb9caf29d 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -183,15 +183,17 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
},
})
);
- DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data).forEach(childClick =>
- onClicks.push({
- description: `Set child ${childClick.title}`,
- icon: 'edit',
- event: () => {
- this.dataDoc[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data));
- },
- })
- );
+ DocListCast(Cast(Doc.UserDoc()['clickFuncs-child'], Doc, null)?.data)
+ .filter(childClick => ScriptCast(childClick.data))
+ .forEach(childClick =>
+ onClicks.push({
+ description: `Set child ${childClick.title}`,
+ icon: 'edit',
+ event: () => {
+ this.dataDoc[StrCast(childClick.targetScriptKey)] = ObjectField.MakeCopy(ScriptCast(childClick.data)!);
+ },
+ })
+ );
!Doc.IsSystem(this.Document) && !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 842293358..08126d4fe 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -496,6 +496,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}, 'link drop');
onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => {
+ if (this._props.rejectDrop?.(de.complete.docDragData?.draggedDocuments, this._props.DocumentView?.())) return false;
if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData);
if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData);
if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData);
@@ -1562,6 +1563,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
pinToPres={this._props.pinToPres}
whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
+ rejectDrop={this._props.childRejectDrop}
showTitle={this._props.childlayout_showTitle}
dontRegisterView={this._props.dontRegisterView}
pointerEvents={this.childPointerEventsFunc}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index a7603a45b..1d5e70be7 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -201,6 +201,7 @@ export class CollectionGridView extends CollectionSubView() {
isContentActive={this.isChildContentActive}
PanelWidth={width}
PanelHeight={height}
+ rejectDrop={this._props.childRejectDrop}
ScreenToLocalTransform={dxf}
setContentViewBox={emptyFunction}
whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 8d3947653..3c2a99b1e 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -192,6 +192,7 @@ export class CollectionLinearView extends CollectionSubView() {
renderDepth={this._props.renderDepth + 1}
dontRegisterView={BoolCast(this.Document.childDontRegisterViews)}
focus={emptyFunction}
+ rejectDrop={this._props.childRejectDrop}
styleProvider={this._props.styleProvider}
containerViewPath={this.childContainerViewPath}
whenChildContentsActiveChanged={emptyFunction}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index adaceb053..caad1c7e0 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -281,6 +281,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
PanelWidth={this.childWidth(childLayout)}
PanelHeight={this.childHeight}
rootSelected={this.rootSelected}
+ rejectDrop={this._props.childRejectDrop}
dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType}
onClickScript={this.onChildClickHandler}
onDoubleClickScript={this.onChildDoubleClickHandler}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 27c41583c..593598479 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -263,6 +263,7 @@ export class CollectionMultirowView extends CollectionSubView() {
PanelWidth={this.childWidth}
PanelHeight={this.childHeight(childLayout)}
rootSelected={this.rootSelected}
+ rejectDrop={this._props.childRejectDrop}
dragAction={StrCast(this.Document.childDragAction, this._props.childDragAction) as dropActionType}
onClickScript={this.onChildClickHandler}
onDoubleClickScript={this.onChildDoubleClickHandler}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index de49f502f..b4473d9dc 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -68,7 +68,7 @@ export interface FieldViewSharedProps {
isGroupActive?: () => string | undefined; // is this document part of a group that is active
// eslint-disable-next-line no-use-before-define
setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
-
+ rejectDrop?: (draggedDoc: Doc[] | undefined, subView?: DocumentView) => boolean; // whether a document drop is rejected
PanelWidth: () => number;
PanelHeight: () => number;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 4a6e8eb49..9fc20ffd4 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -241,3 +241,35 @@
color: black;
}
}
+.imageBox-regenerate-dialog {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: white;
+ padding: 20px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+ z-index: 10000;
+
+ h3 { margin-top: 0; }
+
+ input {
+ width: 300px;
+ padding: 8px;
+ margin-bottom: 10px;
+ }
+
+ .buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+
+ .generate-btn {
+ background: #0078d4;
+ color: white;
+ border: none;
+ padding: 8px 16px;
+ }
+ }
+ } \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0c475b7bb..c59c38fd8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,23 +1,24 @@
-import { Button, Colors, Size, Type } from '@dash/components';
+import { Button, Colors, EditableText, Size, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Slider, Tooltip } from '@mui/material';
import axios from 'axios';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
import { AiOutlineSend } from 'react-icons/ai';
import ReactLoading from 'react-loading';
-import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
+import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
+import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { Upload } from '../../../server/SharedMediaTypes';
import { emptyFunction } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -26,7 +27,7 @@ import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
-import { undoable, undoBatch } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -45,7 +46,6 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FocusViewOptions } from './FocusViewOptions';
import './ImageBox.scss';
import { OpenWhere } from './OpenWhere';
-import { ComputedField } from '../../../fields/ScriptField';
const DefaultPath = '/assets/unknown-file-icon-hi.png';
export class ImageEditorData {
@@ -104,6 +104,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable private _regenerateLoading = false;
@observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
+ // Add these observable properties to the ImageBox class
+ @observable private _outpaintingInProgress = false;
+ @observable private _outpaintingPrompt = '';
+
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
@@ -136,6 +140,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
componentDidMount() {
+ super.componentDidMount?.();
this._disposers.sizer = reaction(
() => ({
forceFull: this._props.renderDepth < 1 || this.layoutDoc._showFullRes,
@@ -166,6 +171,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
},
{ fireImmediately: true }
);
+ this._disposers.outpainting = reaction(
+ () => this.Document?._needsOutpainting,
+ needsOutpainting => {
+ if (needsOutpainting && this.Document?._outpaintingResize) {
+ this.processOutpainting();
+ }
+ }
+ );
}
componentWillUnmount() {
@@ -200,7 +213,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
drop = undoable(
action((e: Event, de: DragManager.DropEvent) => {
- if (de.complete.docDragData) {
+ if (de.complete.docDragData && this._props.rejectDrop?.(de.complete.docDragData?.draggedDocuments, this.DocumentView?.())) {
let added: boolean | undefined;
const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => {
if (!ele) return false;
@@ -339,6 +352,145 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
});
+ @observable _showOutpaintPrompt: boolean = false;
+ @observable _outpaintPromptInput: string = 'Extend this image naturally with matching content';
+
+ @action
+ openOutpaintPrompt = () => {
+ this._showOutpaintPrompt = true;
+ };
+
+ @action
+ closeOutpaintPrompt = () => {
+ this._showOutpaintPrompt = false;
+ };
+
+ @action
+ handlePromptChange = (val: string | number) => {
+ this._outpaintPromptInput = '' + val;
+ };
+
+ @action
+ submitOutpaintPrompt = () => {
+ this.closeOutpaintPrompt();
+ this.processOutpaintingWithPrompt(this._outpaintPromptInput);
+ };
+
+ @action
+ processOutpaintingWithPrompt = async (customPrompt: string) => {
+ const field = Cast(this.dataDoc[this.fieldKey], ImageField);
+ if (!field) return;
+
+ const origWidth = NumCast(this.Document._outpaintingOriginalWidth);
+ const origHeight = NumCast(this.Document._outpaintingOriginalHeight);
+
+ if (!origWidth || !origHeight) {
+ console.error('Original dimensions (_outpaintingOriginalWidth/_outpaintingOriginalHeight) not set. Ensure resizeViewForOutpainting was called first.');
+ return;
+ }
+
+ // Set flag that outpainting is in progress
+ this._outpaintingInProgress = true;
+
+ // Revert dimensions if prompt is blank (acts like Cancel)
+ if (!customPrompt) {
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ this._outpaintingInProgress = false;
+ return;
+ }
+
+ try {
+ const currentPath = this.choosePath(field.url);
+ const newWidth = NumCast(this.Document._width);
+ const newHeight = NumCast(this.Document._height);
+
+ // Optional: add loading indicator
+ const loadingOverlay = document.createElement('div');
+ loadingOverlay.style.position = 'absolute';
+ loadingOverlay.style.top = '0';
+ loadingOverlay.style.left = '0';
+ loadingOverlay.style.width = '100%';
+ loadingOverlay.style.height = '100%';
+ loadingOverlay.style.background = 'rgba(0,0,0,0.5)';
+ loadingOverlay.style.display = 'flex';
+ loadingOverlay.style.justifyContent = 'center';
+ loadingOverlay.style.alignItems = 'center';
+ loadingOverlay.innerHTML = '<div style="color: white; font-size: 16px;">Generating outpainted image...</div>';
+ this._mainCont?.appendChild(loadingOverlay);
+
+ const response = await Networking.PostToServer('/outpaintImage', {
+ imageUrl: currentPath,
+ prompt: customPrompt,
+ originalDimensions: { width: origWidth, height: origHeight },
+ newDimensions: { width: newWidth, height: newHeight },
+ });
+
+ const batch = UndoManager.StartBatch('outpaint image');
+ if (response && typeof response === 'object' && 'url' in response && typeof response.url === 'string') {
+ console.log('Received outpainted image:', response.url);
+
+ if (!this.dataDoc[this.fieldKey + '_alternates']) {
+ this.dataDoc[this.fieldKey + '_alternates'] = new List<Doc>();
+ }
+
+ const originalDoc = Docs.Create.ImageDocument(field.url.href, {
+ title: `Original: ${this.Document.title}`,
+ _nativeWidth: Doc.NativeWidth(this.dataDoc),
+ _nativeHeight: Doc.NativeHeight(this.dataDoc),
+ });
+
+ Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', originalDoc);
+
+ // Replace with new outpainted image
+ this.dataDoc[this.fieldKey] = new ImageField(response.url);
+
+ Doc.SetNativeWidth(this.dataDoc, newWidth);
+ Doc.SetNativeHeight(this.dataDoc, newHeight);
+
+ this.Document.$ai = true;
+ this.Document.$ai_outpainted = true;
+ this.Document.$ai_outpaint_prompt = customPrompt;
+ } else {
+ console.error('Unexpected API response:', response);
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ alert('Failed to receive a valid image URL from server.');
+ }
+ batch.end();
+
+ this._mainCont?.removeChild(loadingOverlay);
+ } catch (error) {
+ console.error('Error during outpainting:', error);
+ this.Document._width = origWidth;
+ this.Document._height = origHeight;
+ alert('An error occurred while outpainting. Please try again.');
+ } finally {
+ runInAction(() => (this._outpaintingInProgress = false));
+ }
+ };
+
+ processOutpainting = () => this.openOutpaintPrompt();
+
+ componentUI = () =>
+ !this._showOutpaintPrompt ? null : (
+ <div className="imageBox-regenerate-dialog" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+ <h3>Outpaint Image</h3>
+ <EditableText
+ placeholder="Enter a prompt for extending the image:"
+ setVal={val => this.handlePromptChange(val)}
+ val={this._outpaintPromptInput}
+ type={Type.TERT}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userBackgroundColor}
+ />
+ <div className="buttons">
+ <Button text="Cancel" type={Type.TERT} onClick={this.closeOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} />
+ <Button text="Generate" type={Type.TERT} onClick={this.submitOutpaintPrompt} color={SnappingManager.userColor} background={SnappingManager.userVariantColor} />
+ </div>
+ </div>
+ );
+
specificContextMenu = (): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
@@ -346,9 +498,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
- funcs.push({
- description: 'GetImageText',
- event: () => {
+ funcs.push({ description: 'GetImageText', event: () => {
Networking.PostToServer('/queryFireflyImageText', {
file: (file => {
const ext = file ? extname(file) : '';
@@ -357,25 +507,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}).then(text => alert(text));
},
icon: 'expand-arrows-alt',
- });
- funcs.push({
- description: 'Expand Image',
- event: () => {
- Networking.PostToServer('/expandImage', {
- prompt: 'sunny skies',
- file: (file => {
- const ext = file ? extname(file) : '';
- return file?.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
- })(ImageCast(this.Document[Doc.LayoutDataKey(this.Document)])?.url.href),
- }).then(res => {
- const info = res as Upload.ImageInformation;
- const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
- DocUtils.assignImageInfo(info, img);
- this._props.addDocTab(img, OpenWhere.addRight);
- });
- },
- icon: 'expand-arrows-alt',
- });
+ }); // prettier-ignore
funcs.push({ description: 'Copy path', event: () => ClientUtils.CopyText(this.choosePath(field.url)), icon: 'copy' });
funcs.push({ description: 'Open Image Editor', event: this.docEditorView, icon: 'pencil-alt' });
this.layoutDoc.ai &&
@@ -396,6 +528,22 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')),
icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down',
});
+ // Add new outpainting option
+ funcs.push({ description: 'Outpaint Image', event: () => this.openOutpaintPrompt(), icon: 'brush' });
+
+ // Add outpainting history option if the image was outpainted
+ this.Document.ai_outpainted &&
+ funcs.push({
+ description: 'View Original Image',
+ event: action(() => {
+ const alternates = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
+ if (alternates && alternates.length) {
+ // Toggle to show the original image
+ this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate';
+ }
+ }),
+ icon: 'image',
+ });
ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
@@ -432,7 +580,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const ext = extname(url.href);
return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
};
- getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+ getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc.freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
@computed get usingAlternate() {
const usePath = StrCast(this.Document[this.fieldKey + '_usePath']);
@@ -752,82 +900,61 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad;
return (
- <div
- className="imageBox"
- onContextMenu={this.specificContextMenu}
- ref={this.createDropTarget}
- onScroll={action(() => {
- if (!this._forcedScroll) {
- if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
- this._ignoreScroll = true;
- this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
- this._ignoreScroll = false;
+ <>
+ <div
+ className="imageBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this.createDropTarget}
+ onScroll={action(() => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._mainCont?.scrollTop;
+ this._ignoreScroll = false;
+ }
}
- }
- })}
- style={{
- width: this._props.PanelWidth() ? undefined : `100%`,
- height: this._props.PanelHeight() ? undefined : `100%`,
- pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
- borderRadius,
- overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
- }}>
- <CollectionFreeFormView
- ref={this._ffref}
- {...this._props}
- Document={this.Document}
- setContentViewBox={emptyFunction}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- renderDepth={this._props.renderDepth + 1}
- fieldKey={this.annotationKey}
- styleProvider={this._props.styleProvider}
- isAnnotationOverlay
- annotationLayerHostsContent
- PanelWidth={this._props.PanelWidth}
- PanelHeight={this._props.PanelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- select={emptyFunction}
- focus={this.focus}
- getScrollHeight={this.getScrollHeight}
- NativeDimScaling={returnOne}
- isAnyChildContentActive={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}>
- {this.content}
- </CollectionFreeFormView>
- {this.Loading ? (
- <div className="loading-spinner" style={{ position: 'absolute' }}>
- <ReactLoading type="spin" height={50} width={50} color={'blue'} />
- </div>
- ) : null}
- {this.regenerateImageIcon}
- {this.overlayImageIcon}
- {this.annotationLayer}
- {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
- <MarqueeAnnotator
+ })}
+ style={{
+ width: this._props.PanelWidth() ? undefined : `100%`,
+ height: this._props.PanelHeight() ? undefined : `100%`,
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ overflow: this.layoutDoc.layout_fitWidth || this._props.fitWidth?.(this.Document) ? 'auto' : 'hidden',
+ }}>
+ <CollectionFreeFormView
+ ref={this._ffref}
+ {...this._props}
Document={this.Document}
- ref={this.marqueeref}
- scrollTop={0}
- annotationLayerScrollTop={0}
- scaling={returnOne}
- annotationLayerScaling={this._props.NativeDimScaling}
- screenTransform={this.DocumentView().screenToViewTransform}
- docView={this.DocumentView}
- addDocument={this.addDocument}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this.savedAnnotations}
- selectionText={returnEmptyString}
- annotationLayer={this._annotationLayer.current}
- marqueeContainer={this._mainCont}
- highlightDragSrcColor=""
- anchorMenuCrop={this.crop}
- // anchorMenuFlashcard={() => this.getImageDesc()}
- />
- )}
- </div>
+ setContentViewBox={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ renderDepth={this._props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ styleProvider={this._props.styleProvider}
+ isAnnotationOverlay
+ annotationLayerHostsContent
+ PanelWidth={this._props.PanelWidth}
+ PanelHeight={this._props.PanelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ select={emptyFunction}
+ focus={this.focus}
+ rejectDrop={this._props.rejectDrop}
+ getScrollHeight={this.getScrollHeight}
+ NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}>
+ {this.content}
+ </CollectionFreeFormView>
+ {this._outpaintingInProgress && (
+ <div className="imageBox-outpaintingSpinner">
+ <ReactLoading type="spin" color="#666" height={60} width={60} />
+ </div>
+ )}
+ </div>
+ </>
);
}
@@ -840,10 +967,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const file = input.files?.[0];
if (file) {
const disposer = OverlayView.ShowSpinner();
- DocUtils.uploadFileToDoc(file, {}, this.Document).then(doc => {
- disposer();
- doc && (doc.height = undefined);
- });
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
+ if (result instanceof Error) {
+ alert('Error uploading files - possibly due to unsupported file types');
+ } else {
+ this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+ !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+ }
+ disposer();
} else {
console.log('No file selected');
}
diff --git a/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
new file mode 100644
index 000000000..e99bf67c7
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/EmbeddedDocView.tsx
@@ -0,0 +1,52 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+import * as React from "react";
+import { observer } from "mobx-react";
+import { Doc } from "../../../../fields/Doc";
+import { DocumentView } from "../DocumentView";
+import { Transform } from "../../../util/Transform";
+
+interface EmbeddedDocViewProps {
+ doc: Doc;
+ width?: number;
+ height?: number;
+ slotId?: string;
+}
+
+@observer
+export class EmbeddedDocView extends React.Component<EmbeddedDocViewProps> {
+ render() {
+ const { doc, width = 300, height = 200, slotId } = this.props;
+
+ // Use either an existing embedding or create one
+ let docToDisplay = doc;
+
+ // If we need an embedding, create or use one
+ if (!docToDisplay.isEmbedding) {
+ docToDisplay = Doc.BestEmbedding(doc) || Doc.MakeEmbedding(doc);
+ // Set the container to the slot's ID so we can track it
+ if (slotId) {
+ docToDisplay.embedContainer = `scrapbook-slot-${slotId}`;
+ }
+ }
+
+ return (
+ <DocumentView
+ Document={docToDisplay}
+ renderDepth={0}
+ // Required sizing functions
+ NativeWidth={() => width}
+ NativeHeight={() => height}
+ PanelWidth={() => width}
+ PanelHeight={() => height}
+ // Required state functions
+ isContentActive={() => true}
+ childFilters={() => []}
+ ScreenToLocalTransform={() => new Transform()}
+ // Display options
+ hideDeleteButton={true}
+ hideDecorations={true}
+ hideResizeHandles={true}
+ />
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookBox.tsx b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
new file mode 100644
index 000000000..b02976067
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookBox.tsx
@@ -0,0 +1,111 @@
+import { action, makeObservable, observable } from 'mobx';
+import * as React from 'react';
+import { Doc } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { emptyFunction } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { DocumentView } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { DragManager } from '../../../util/DragManager';
+// Scrapbook view: a container that lays out its child items in a grid/template
+export class ScrapbookBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ @observable createdDate: string;
+ private _dropDisposer?: DragManager.DragDropDisposer;
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ makeObservable(this);
+ this.createdDate = this.getFormattedDate();
+
+ // ensure we always have a List<Doc> in dataDoc['items']
+ if (!this.dataDoc[this.fieldKey]) {
+ this.dataDoc[this.fieldKey] = new List<Doc>();
+ }
+ this.createdDate = this.getFormattedDate();
+ this.setTitle();
+ }
+
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(ScrapbookBox, fieldStr);
+ }
+
+ getFormattedDate(): string {
+ return new Date().toLocaleDateString(undefined, {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+ }
+
+ @action
+ setTitle() {
+ const title = `Scrapbook - ${this.createdDate}`;
+ if (this.dataDoc.title !== title) {
+ this.dataDoc.title = title;
+ }
+ }
+
+ componentDidMount() {
+ this.setTitle();
+ }
+
+ childRejectDrop = (draggedDoc: Doc[] | undefined, subView?: DocumentView) => {
+ if (draggedDoc?.length === 1 && subView) {
+ if (subView.Document.type === DocumentType.IMG && draggedDoc[0].$type !== DocumentType.IMG) {
+ return true;
+ }
+ }
+ return false;
+ };
+ rejectDrop = (draggedDoc: Doc[] | undefined, subView?: DocumentView) => {
+ if (draggedDoc?.length === 1 && draggedDoc[0].$type !== DocumentType.IMG) {
+ return true;
+ }
+ return false;
+ };
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData?.draggedDocuments[0]?.$type === DocumentType.IMG) {
+ return true;
+ }
+ return false;
+ };
+
+ protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+ this._dropDisposer?.();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
+ }
+ };
+
+ render() {
+ return (
+ <div style={{ background: 'beige', width: '100%', height: '100%' }} ref={r => r && this.createDashEventsTarget(r)}>
+ <CollectionView
+ {...this._props} //
+ setContentViewBox={emptyFunction}
+ rejectDrop={this.rejectDrop}
+ childRejectDrop={this.childRejectDrop}
+ />
+ </div>
+ );
+ }
+}
+
+// Register scrapbook
+Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
+ layout: { view: ScrapbookBox, dataField: 'items' },
+ options: {
+ acl: '',
+ _height: 200,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_autoHeight: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsImages',
+ },
+});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookContent.tsx b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
new file mode 100644
index 000000000..ad1d308e8
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookContent.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+import { observer } from "mobx-react-lite";
+// Import the Doc type from your actual module.
+import { Doc } from "../../../../fields/Doc";
+
+export interface ScrapbookContentProps {
+ doc: Doc;
+}
+
+// A simple view that displays a document's title and content.
+// Adjust how you extract the text if your Doc fields are objects.
+export const ScrapbookContent: React.FC<ScrapbookContentProps> = observer(({ doc }) => {
+ // If doc.title or doc.content are not plain strings, convert them.
+ const titleText = doc.title ? doc.title.toString() : "Untitled";
+ const contentText = doc.content ? doc.content.toString() : "No content available.";
+
+ return (
+ <div className="scrapbook-content">
+ <h3>{titleText}</h3>
+ <p>{contentText}</p>
+ </div>
+ );
+});
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.scss b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
new file mode 100644
index 000000000..ae647ad36
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.scss
@@ -0,0 +1,85 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+.scrapbook-slot {
+ position: absolute;
+ background-color: rgba(245, 245, 245, 0.7);
+ border: 2px dashed #ccc;
+ border-radius: 5px;
+ box-sizing: border-box;
+ transition: all 0.2s ease;
+ overflow: hidden;
+
+ &.scrapbook-slot-over {
+ border-color: #4a90e2;
+ background-color: rgba(74, 144, 226, 0.1);
+ }
+
+ &.scrapbook-slot-filled {
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.1);
+ background-color: transparent;
+
+ &.scrapbook-slot-over {
+ border-color: #4a90e2;
+ background-color: rgba(74, 144, 226, 0.1);
+ }
+ }
+
+ .scrapbook-slot-empty {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ .scrapbook-slot-placeholder {
+ text-align: center;
+ color: #888;
+ }
+
+ .scrapbook-slot-title {
+ font-weight: bold;
+ margin-bottom: 5px;
+ }
+
+ .scrapbook-slot-instruction {
+ font-size: 0.9em;
+ font-style: italic;
+ }
+
+ .scrapbook-slot-content {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ }
+
+ .scrapbook-slot-controls {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 10;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+
+ .scrapbook-slot-remove-btn {
+ background-color: rgba(255, 255, 255, 0.8);
+ border: 1px solid #ccc;
+ border-radius: 50%;
+ width: 20px;
+ height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 10px;
+
+ &:hover {
+ background-color: rgba(255, 0, 0, 0.1);
+ }
+ }
+ }
+
+ &:hover .scrapbook-slot-controls {
+ opacity: 1;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
new file mode 100644
index 000000000..2c8f93778
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlot.tsx
@@ -0,0 +1,28 @@
+
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+export interface SlotDefinition {
+ id: string;
+ x: number; y: number;
+ defaultWidth: number;
+ defaultHeight: number;
+ }
+
+ export interface SlotContentMap {
+ slotId: string;
+ docId?: string;
+ }
+
+ export interface ScrapbookConfig {
+ slots: SlotDefinition[];
+ contents?: SlotContentMap[];
+ }
+
+ export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
+ slots: [
+ { id: "slot1", x: 10, y: 10, defaultWidth: 180, defaultHeight: 120 },
+ { id: "slot2", x: 200, y: 10, defaultWidth: 180, defaultHeight: 120 },
+ // …etc
+ ],
+ contents: []
+ };
+ \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
new file mode 100644
index 000000000..686917d9a
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookSlotTypes.ts
@@ -0,0 +1,25 @@
+// ScrapbookSlotTypes.ts
+export interface SlotDefinition {
+ id: string;
+ title: string;
+ x: number;
+ y: number;
+ defaultWidth: number;
+ defaultHeight: number;
+ }
+
+ export interface ScrapbookConfig {
+ slots: SlotDefinition[];
+ contents?: { slotId: string; docId: string }[];
+ }
+
+ // give it three slots by default:
+ export const DEFAULT_SCRAPBOOK_CONFIG: ScrapbookConfig = {
+ slots: [
+ { id: "main", title: "Main Content", x: 20, y: 20, defaultWidth: 360, defaultHeight: 200 },
+ { id: "notes", title: "Notes", x: 20, y: 240, defaultWidth: 360, defaultHeight: 160 },
+ { id: "resources", title: "Resources", x: 400, y: 20, defaultWidth: 320, defaultHeight: 380 },
+ ],
+ contents: [],
+ };
+ \ No newline at end of file
diff --git a/src/client/views/nodes/scrapbook/ScrapbookVersionTwo.tsx b/src/client/views/nodes/scrapbook/ScrapbookVersionTwo.tsx
new file mode 100644
index 000000000..d15d2fe56
--- /dev/null
+++ b/src/client/views/nodes/scrapbook/ScrapbookVersionTwo.tsx
@@ -0,0 +1,125 @@
+//IGNORE FOR NOW, CURRENTLY NOT USED IN SCRAPBOOK IMPLEMENTATION
+import { action, makeObservable, observable } from 'mobx';
+import * as React from 'react';
+import { RichTextField } from '../../../../fields/RichTextField';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { FormattedTextBox, FormattedTextBoxProps } from '../formattedText/FormattedTextBox';
+
+export class ScrapbookVersionTwo extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ @observable scrapbookDate: string;
+
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(ScrapbookVersionTwo, fieldStr);
+ }
+
+ constructor(props: FormattedTextBoxProps) {
+ super(props);
+ makeObservable(this);
+ this.scrapbookDate = this.getFormattedDate();
+
+ console.log('Constructor: Setting initial title and text...');
+ this.setDailyTitle();
+ this.setDailyText();
+ }
+
+ getFormattedDate(): string {
+ const date = new Date().toLocaleDateString(undefined, {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ console.log('getFormattedDate():', date);
+ return date;
+ }
+
+ @action
+ setDailyTitle() {
+ console.log('setDailyTitle() called...');
+ console.log('Current title before update:', this.dataDoc.title);
+
+ if (!this.dataDoc.title || this.dataDoc.title !== this.scrapbookDate) {
+ console.log('Updating title to:', this.scrapbookDate);
+ this.dataDoc.title = this.scrapbookDate;
+ }
+
+ console.log('New title after update:', this.dataDoc.title);
+ }
+
+ @action
+ setDailyText() {
+ console.log('setDailyText() called...');
+ const placeholderText = 'Start writing here...';
+ const initialText = `Scrapbook - $\n${placeholderText}`;
+
+ console.log('Checking if dataDoc has text field...');
+
+ const styles = {
+ bold: true, // Make the journal date bold
+ color: 'red', // Set the journal date color to blue
+ fontSize: 12, // Set the font size to 18px for the whole text
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))',
+ gap: '8px',
+ padding: '10px',
+ background: '#fafafa',
+ width: '100%',
+ height: '100%',
+ };
+
+ console.log('Setting new text field with:', initialText);
+ this.dataDoc[this.fieldKey] = RichTextField.textToRtf(
+ initialText,
+ undefined, // No image DocId
+ styles, // Pass the styles object here
+ placeholderText.length // The position for text selection
+ );
+
+ console.log('Current text field:', this.dataDoc[this.fieldKey]);
+ }
+
+ componentDidMount(): void {
+ console.log('componentDidMount() triggered...');
+ // bcz: This should be moved into Docs.Create.DailyJournalDocument()
+ // otherwise, it will override all the text whenever the note is reloaded
+ this.setDailyTitle();
+ this.setDailyText();
+ }
+
+ render() {
+ return (
+ <div
+ style={{
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))',
+ gap: '8px',
+ padding: '10px',
+ background: '#fafafa',
+ width: '100%',
+ height: '100%',
+ }}
+ >
+ <FormattedTextBox {...this._props} fieldKey={'text'} Document={this.Document} TemplateDataDocument={undefined} />
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.SCRAPBOOK, {
+ layout: { view: ScrapbookVersionTwo, dataField: 'text' },
+ options: {
+ acl: '',
+ _height: 35,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_autoHeight: true,
+ _layout_nativeDimEditable: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsFileEarmarkTextFill',
+ },
+}); \ No newline at end of file
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index abce7ed26..ad8585bf4 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -491,6 +491,10 @@ export class Doc extends RefField {
}
}
export namespace Doc {
+ export let SelectOnLoad: Doc | undefined;
+ export function SetSelectOnLoad(doc: Doc | undefined) {
+ SelectOnLoad = doc;
+ }
export let DocDragDataName: string = '';
export function SetDocDragDataName(name: string) {
DocDragDataName = name;
@@ -1192,6 +1196,26 @@ export namespace Doc {
const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
return NumCast(doc._nativeHeight, nheight || dheight);
}
+
+ export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
+ return !doc ? 0 : NumCast(doc._outpaintingWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingWidth'], useWidth ? NumCast(doc._width) : 0));
+ }
+
+ export function OutpaintingHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
+ if (!doc) return 0;
+ const oheight = (Doc.OutpaintingWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height);
+ const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_outpaintingHeight'], useHeight ? NumCast(doc._height) : 0);
+ return NumCast(doc._outpaintingHeight, oheight || dheight);
+ }
+
+ export function SetOutpaintingWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
+ doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingWidth'] = width;
+ }
+
+ export function SetOutpaintingHeight(doc: Doc, height: number | undefined, fieldKey?: string) {
+ doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_outpaintingHeight'] = height;
+ }
+
export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
doc[(fieldKey || Doc.LayoutDataKey(doc)) + '_nativeWidth'] = width;
}
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index d22142d7d..fd61f6c9e 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -8,6 +8,7 @@ import { DashUploadUtils } from '../DashUploadUtils';
import { _error, _invalid, _success, Method } from '../RouteManager';
import { Directory, filesDirectory } from '../SocketData';
import ApiManager, { Registration } from './ApiManager';
+import { Upload } from '../SharedMediaTypes';
export default class FireflyManager extends ApiManager {
getBearerToken = () =>
@@ -313,6 +314,85 @@ export default class FireflyManager extends ApiManager {
})
), // prettier-ignore
});
+
+ register({
+ method: Method.POST,
+ subscription: '/outpaintImage',
+ secureHandler: ({ req, res }) =>
+ new Promise<void>(resolver =>
+ this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel)
+ .then(uploadUrl =>
+ this.getBearerToken()
+ .then(tokenResponse => tokenResponse?.json())
+ .then((tokenData: { access_token: string }) =>
+ fetch('https://firefly-api.adobe.io/v3/images/expand', {
+ method: 'POST',
+ headers: [
+ ['Content-Type', 'application/json'],
+ ['Accept', 'application/json'],
+ ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+ ['Authorization', `Bearer ${tokenData.access_token}`],
+ ],
+ body: JSON.stringify({
+ image: {
+ source: { url: uploadUrl },
+ },
+ size: {
+ width: Math.round(req.body.newDimensions.width),
+ height: Math.round(req.body.newDimensions.height),
+ },
+ prompt: req.body.prompt ?? '',
+ numVariations: 1,
+ placement: {
+ inset: {
+ left: 0,
+ top: 0,
+ right: Math.round(req.body.newDimensions.width - req.body.originalDimensions.width),
+ bottom: Math.round(req.body.newDimensions.height - req.body.originalDimensions.height),
+ },
+ alignment: {
+ horizontal: 'center',
+ vertical: 'center',
+ },
+ },
+ }),
+ })
+ .then(expandResp => expandResp?.json())
+ .then(expandData => {
+ if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) {
+ console.error('Firefly validation error:', expandData);
+ _error(res, expandData.message ?? 'Failed to generate image');
+ } else {
+ return DashUploadUtils.UploadImage(expandData.outputs[0].image.url)
+ .then((info: Upload.ImageInformation | Error) => {
+ if (info instanceof Error) {
+ _invalid(res, info.message);
+ } else {
+ _success(res, { url: info.accessPaths.agnostic.client });
+ }
+ })
+ .catch(uploadErr => {
+ console.error('DashUpload Error:', uploadErr);
+ _error(res, 'Failed to upload generated image.');
+ });
+ }
+ })
+ )
+ )
+ .catch(e => {
+ _invalid(res, e.message);
+ resolver();
+ })
+ ),
+ });
+
+ /* register({
+ method: Method.POST
+ subscription: '/queryFireflyOutpaint',
+ secureHandler: ({req, res}) =>
+ this.outpaintImage()
+ })*/
+
register({
method: Method.POST,
subscription: '/queryFireflyImage',
@@ -339,23 +419,6 @@ export default class FireflyManager extends ApiManager {
)
),
});
- register({
- method: Method.POST,
- subscription: '/expandImage',
- secureHandler: ({ req, res }) =>
- this.uploadImageToDropbox(req.body.file, req.user as DashUserModel)
- .then(uploadUrl =>
- this.expandImage(uploadUrl, req.body.prompt).then(text => {
- if (text.error_code) _error(res, text.message);
- else
- DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
- if (info instanceof Error) _invalid(res, info.message);
- else _success(res, info);
- });
- })
- )
- .catch(e => _invalid(res, e.message)),
- });
// construct this url and send user to it. It will allow them to authorize their dropbox account and will send the resulting token to our endpoint /refreshDropbox
// https://www.dropbox.com/oauth2/authorize?client_id=DROPBOX_CLIENT_ID&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox