From e26b418f4c108177921a5554bae099c6b4a948ce Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Fri, 22 Nov 2024 17:39:25 +0100 Subject: [PATCH 1/9] add drop eventListener for files Signed-off-by: grnd-alt --- appinfo/info.xml | 2 +- src/App.tsx | 5 +++++ src/files/files.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/files/files.ts diff --git a/appinfo/info.xml b/appinfo/info.xml index 0c2a11e..c9b2bf0 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -40,7 +40,7 @@ The official whiteboard app for Nextcloud. It allows users to create and share w https://raw.githubusercontent.com/nextcloud/whiteboard/main/screenshots/screenshot1.png - + diff --git a/src/App.tsx b/src/App.tsx index 55b6b0d..905ea4f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,6 +29,7 @@ import type { ResolvablePromise } from '@excalidraw/excalidraw/types/utils' import type { NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types' import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js' import { useExcalidrawLang } from './hooks/useExcalidrawLang' +import { registerFilesHandler } from './files/files' interface WhiteboardAppProps { fileId: number @@ -95,6 +96,10 @@ export default function App({ if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken, setViewModeEnabled)) } if (collab && !collab.portal.socket) collab.startCollab() useEffect(() => { + if (excalidrawAPI) { + registerFilesHandler(excalidrawAPI) + } + const extraTools = document.getElementsByClassName( 'App-toolbar__extra-tools-trigger', )[0] diff --git a/src/files/files.ts b/src/files/files.ts new file mode 100644 index 0000000..be731fe --- /dev/null +++ b/src/files/files.ts @@ -0,0 +1,46 @@ +import { convertToExcalidrawElements } from '@excalidraw/excalidraw' +import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types' + +function addCustomFileElement(excalidrawApi: ExcalidrawImperativeAPI, link: string) { + const elements = excalidrawApi.getSceneElementsIncludingDeleted().slice() + const newElements = convertToExcalidrawElements([{ + text: link, + type: 'text', + fontSize: 16, + textAlign: 'left', + fontFamily: 1, + x: 0, + y: 0, + }]) + elements.push(newElements[0]) + excalidrawApi.updateScene({ elements }) +} + +// TODO: Implement uploading to nextcloud +function UploadFileToNextcloud(file: File) { + return file +} + +function filesEventListener(ev: Event, excalidrawApi: ExcalidrawImperativeAPI) { + if (ev instanceof DragEvent) { + if (ev.dataTransfer?.files[0]) UploadFileToNextcloud(ev.dataTransfer?.files[0]) + const types = ['image/webp'] + if (!types.includes(ev.dataTransfer?.files[0].type || '')) { + addCustomFileElement(excalidrawApi, ev.dataTransfer?.files[0].name || 'no file name') + ev.stopImmediatePropagation() + } + } +} + +/** + * adds drop eventlistener to excalidraw + * uploads file to nextcloud server, to be shared with all users + * if filetype not supported by excalidraw inserts link to file + * @param {ExcalidrawImperativeAPI} excalidrawApi excalidrawApi +*/ +export function registerFilesHandler(excalidrawApi: ExcalidrawImperativeAPI) { + const containerRef = document.getElementsByClassName('excalidraw-container')[0] + if (containerRef) { + containerRef.addEventListener('drop', (ev) => filesEventListener(ev, excalidrawApi)) + } +} From ed4f629245c90de10b1c41a3e8e23d6154ee3986 Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Mon, 2 Dec 2024 15:43:05 +0100 Subject: [PATCH 2/9] feat(fileuploads): add custom file element for unknown types Signed-off-by: grnd-alt --- .gitignore | 1 + src/App.tsx | 4 - src/collaboration/collab.ts | 3 + src/files/files.ts | 148 +++++++++++++++++++++++++++--------- 4 files changed, 116 insertions(+), 40 deletions(-) diff --git a/.gitignore b/.gitignore index 4416880..bf3e11c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ \.idea/ /build/ +/backup/ /js/ /dist/ /css/ diff --git a/src/App.tsx b/src/App.tsx index 905ea4f..b779178 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -96,10 +96,6 @@ export default function App({ if (excalidrawAPI && !collab) { setCollab(new Collab(excalidrawAPI, fileId, publicSharingToken, setViewModeEnabled)) } if (collab && !collab.portal.socket) collab.startCollab() useEffect(() => { - if (excalidrawAPI) { - registerFilesHandler(excalidrawAPI) - } - const extraTools = document.getElementsByClassName( 'App-toolbar__extra-tools-trigger', )[0] diff --git a/src/collaboration/collab.ts b/src/collaboration/collab.ts index 8e1a471..91268ad 100644 --- a/src/collaboration/collab.ts +++ b/src/collaboration/collab.ts @@ -9,6 +9,7 @@ import { Portal } from './Portal' import { restoreElements } from '@excalidraw/excalidraw' import { throttle } from 'lodash' import { hashElementsVersion, reconcileElements } from './util' +import { registerFilesHandler, type FileHandle } from '../files/files' export class Collab { @@ -20,6 +21,7 @@ export class Collab { lastBroadcastedOrReceivedSceneVersion: number = -1 private collaborators = new Map() private files = new Map() + private fileHandle: FileHandle constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch>) { this.excalidrawAPI = excalidrawAPI @@ -28,6 +30,7 @@ export class Collab { this.setViewModeEnabled = setViewModeEnabled this.portal = new Portal(`${fileId}`, this, publicSharingToken) + this.fileHandle = registerFilesHandler(this.excalidrawAPI, this) } async startCollab() { diff --git a/src/files/files.ts b/src/files/files.ts index be731fe..e2f670a 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -1,46 +1,122 @@ import { convertToExcalidrawElements } from '@excalidraw/excalidraw' -import type { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types' +import type { + BinaryFileData, + DataURL, + ExcalidrawImperativeAPI, +} from '@excalidraw/excalidraw/types/types' +import { Collab } from '../collaboration/collab' +import type { FileId } from '@excalidraw/excalidraw/types/element/types' -function addCustomFileElement(excalidrawApi: ExcalidrawImperativeAPI, link: string) { - const elements = excalidrawApi.getSceneElementsIncludingDeleted().slice() - const newElements = convertToExcalidrawElements([{ - text: link, - type: 'text', - fontSize: 16, - textAlign: 'left', - fontFamily: 1, - x: 0, - y: 0, - }]) - elements.push(newElements[0]) - excalidrawApi.updateScene({ elements }) -} +export class FileHandle { + private collab: Collab + private excalidrawApi: ExcalidrawImperativeAPI + private types: string[] + constructor( + excalidrawApi: ExcalidrawImperativeAPI, + collab: Collab, + types: string[], + ) { + this.collab = collab + this.excalidrawApi = excalidrawApi + this.types = types + const containerRef = document.getElementsByClassName( + 'excalidraw-container', + )[0] + let constructedFile: BinaryFileData = { + mimeType: 'image/png', + created: 0o0, + id: 'placeholder_image' as FileId, + dataURL: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE0LDJMMjAsOFYyMEEyLDIgMCAwLDEgMTgsMjJINkEyLDIgMCAwLDEgNCwyMFY0QTIsMiAwIDAsMSA2LDJIMTRNMTgsMjBWOUgxM1Y0SDZWMjBIMThNMTIsMTlMOCwxNUgxMC41VjEySDEzLjVWMTVIMTZMMTIsMTlaIiAvPjwvc3ZnPg==' as DataURL, + } + this.collab.addFile(constructedFile) + if (containerRef) { + containerRef.addEventListener('drop', (ev) => + this.filesDragEventListener(ev, excalidrawApi), + ) + } + this.excalidrawApi.onPointerDown((tool, state, event) => { + }) + } -// TODO: Implement uploading to nextcloud -function UploadFileToNextcloud(file: File) { - return file -} + private filesDragEventListener(ev: Event, excalidrawApi: ExcalidrawImperativeAPI) { + if (ev instanceof DragEvent) { + for (let file of Array.from(ev.dataTransfer?.files || [])) { + this.handleFileInsert(file, ev) + } + } + } + + private handleFileInsert(file: File, ev: Event) { + // if excalidraw can handle it, do nothing + if (this.types.includes(file.type)) { + return + } + ev.stopImmediatePropagation() -function filesEventListener(ev: Event, excalidrawApi: ExcalidrawImperativeAPI) { - if (ev instanceof DragEvent) { - if (ev.dataTransfer?.files[0]) UploadFileToNextcloud(ev.dataTransfer?.files[0]) - const types = ['image/webp'] - if (!types.includes(ev.dataTransfer?.files[0].type || '')) { - addCustomFileElement(excalidrawApi, ev.dataTransfer?.files[0].name || 'no file name') - ev.stopImmediatePropagation() + const fr = new FileReader() + fr.readAsDataURL(file) + fr.onload = () => { + let constructedFile: BinaryFileData = { + mimeType: 'image/png', + created: 0o0, + id: (Math.random() + 1).toString(36).substring(7) as FileId, + dataURL: fr.result as DataURL, + } + this.addCustomFileElement(constructedFile, file.name) } } + + private addCustomFileElement(constructedFile: BinaryFileData, filename: string) { + this.collab.addFile(constructedFile) + const elements = this.excalidrawApi + .getSceneElementsIncludingDeleted() + .slice() + const newElements = convertToExcalidrawElements([ + { + type: 'text', + text: filename, + customData: { filedata: { constructedFile } }, + groupIds: ['1'], + y: 0, + x: 0, + }, + { + type: 'image', + fileId: 'placeholder_image' as FileId, + groupIds: ['1'], + y: 0, + x: 0, + } + ]) + elements.push(...newElements) + this.excalidrawApi.updateScene({ elements }) + } } /** - * adds drop eventlistener to excalidraw - * uploads file to nextcloud server, to be shared with all users - * if filetype not supported by excalidraw inserts link to file - * @param {ExcalidrawImperativeAPI} excalidrawApi excalidrawApi -*/ -export function registerFilesHandler(excalidrawApi: ExcalidrawImperativeAPI) { - const containerRef = document.getElementsByClassName('excalidraw-container')[0] - if (containerRef) { - containerRef.addEventListener('drop', (ev) => filesEventListener(ev, excalidrawApi)) - } + * adds drop eventlistener to excalidraw + * uploads file to nextcloud server, to be shared with all users + * if filetype not supported by excalidraw inserts link to file + * @param {ExcalidrawImperativeAPI} excalidrawApi excalidrawApi + */ +export function registerFilesHandler( + excalidrawApi: ExcalidrawImperativeAPI, + collab: Collab, +): FileHandle { + const types = [ + 'application/vnd.excalidraw+json', + 'application/vnd.excalidrawlib+json', + 'application/json', + 'image/svg+xml', + 'image/svg+xml', + 'image/png', + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'image/bmp', + 'image/x-icon', + 'application/octet-stream', + ] + return new FileHandle(excalidrawApi, collab, types) } From 2aea3057f5a54b9cc8ff7a457d647f701dc18ea9 Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Mon, 2 Dec 2024 21:50:27 +0100 Subject: [PATCH 3/9] feat(fileuploads): download files when element clicked Signed-off-by: grnd-alt --- src/App.tsx | 1 - src/files/files.ts | 70 ++++++++++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index b779178..55b6b0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,6 @@ import type { ResolvablePromise } from '@excalidraw/excalidraw/types/utils' import type { NonDeletedExcalidrawElement } from '@excalidraw/excalidraw/types/element/types' import { getLinkWithPicker } from '@nextcloud/vue/dist/Components/NcRichText.js' import { useExcalidrawLang } from './hooks/useExcalidrawLang' -import { registerFilesHandler } from './files/files' interface WhiteboardAppProps { fileId: number diff --git a/src/files/files.ts b/src/files/files.ts index e2f670a..22199cb 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -1,3 +1,7 @@ +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ import { convertToExcalidrawElements } from '@excalidraw/excalidraw' import type { BinaryFileData, @@ -5,9 +9,10 @@ import type { ExcalidrawImperativeAPI, } from '@excalidraw/excalidraw/types/types' import { Collab } from '../collaboration/collab' -import type { FileId } from '@excalidraw/excalidraw/types/element/types' +import type { ExcalidrawElement, FileId } from '@excalidraw/excalidraw/types/element/types' export class FileHandle { + private collab: Collab private excalidrawApi: ExcalidrawImperativeAPI private types: string[] @@ -22,7 +27,7 @@ export class FileHandle { const containerRef = document.getElementsByClassName( 'excalidraw-container', )[0] - let constructedFile: BinaryFileData = { + const constructedFile: BinaryFileData = { mimeType: 'image/png', created: 0o0, id: 'placeholder_image' as FileId, @@ -31,16 +36,43 @@ export class FileHandle { this.collab.addFile(constructedFile) if (containerRef) { containerRef.addEventListener('drop', (ev) => - this.filesDragEventListener(ev, excalidrawApi), + this.filesDragEventListener(ev), ) } - this.excalidrawApi.onPointerDown((tool, state, event) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + this.excalidrawApi.onPointerDown((activeTool, state, event) => { + const clickedElement = this.getElementAt(state.lastCoords.x, state.lastCoords.y) + if (!clickedElement) { + return + } + this.downloadFile(clickedElement.customData?.meta) }) } - private filesDragEventListener(ev: Event, excalidrawApi: ExcalidrawImperativeAPI) { + private downloadFile(meta) { + const blob = new Blob([meta.dataurl], { type: meta.type }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = meta.name + a.click() + URL.revokeObjectURL(url) + } + + private getElementAt(px: number, py: number): ExcalidrawElement | undefined { + const elements = this.excalidrawApi.getSceneElements() + return elements.find((element) => { + const { x, y, width, height } = element + return ( + px >= x && px <= x + width + && py >= y && py <= y + height + ) + }) + } + + private filesDragEventListener(ev: Event) { if (ev instanceof DragEvent) { - for (let file of Array.from(ev.dataTransfer?.files || [])) { + for (const file of Array.from(ev.dataTransfer?.files || [])) { this.handleFileInsert(file, ev) } } @@ -56,17 +88,20 @@ export class FileHandle { const fr = new FileReader() fr.readAsDataURL(file) fr.onload = () => { - let constructedFile: BinaryFileData = { + const constructedFile: BinaryFileData = { mimeType: 'image/png', created: 0o0, id: (Math.random() + 1).toString(36).substring(7) as FileId, dataURL: fr.result as DataURL, } - this.addCustomFileElement(constructedFile, file.name) + const meta = { + name: file.name, type: file.type, lastModified: file.lastModified, dataurl: fr.result, + } + this.addCustomFileElement(constructedFile, meta) } } - private addCustomFileElement(constructedFile: BinaryFileData, filename: string) { + private addCustomFileElement(constructedFile: BinaryFileData, meta) { this.collab.addFile(constructedFile) const elements = this.excalidrawApi .getSceneElementsIncludingDeleted() @@ -74,23 +109,27 @@ export class FileHandle { const newElements = convertToExcalidrawElements([ { type: 'text', - text: filename, - customData: { filedata: { constructedFile } }, + text: meta.name, + customData: { meta }, groupIds: ['1'], y: 0, - x: 0, + x: 50, }, { type: 'image', fileId: 'placeholder_image' as FileId, + customData: { meta }, groupIds: ['1'], - y: 0, - x: 0, - } + y: -10, + x: -10, + width: 50, + height: 50, + }, ]) elements.push(...newElements) this.excalidrawApi.updateScene({ elements }) } + } /** @@ -98,6 +137,7 @@ export class FileHandle { * uploads file to nextcloud server, to be shared with all users * if filetype not supported by excalidraw inserts link to file * @param {ExcalidrawImperativeAPI} excalidrawApi excalidrawApi + * @param collab {Collab} collab */ export function registerFilesHandler( excalidrawApi: ExcalidrawImperativeAPI, From c48bc27c2efa9637b50e9b30d94a8eb25e4f4288 Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Thu, 5 Dec 2024 12:18:24 +0100 Subject: [PATCH 4/9] feat(fileuploads): download files on double click add designed download button Signed-off-by: grnd-alt --- src/files/files.ts | 156 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 122 insertions(+), 34 deletions(-) diff --git a/src/files/files.ts b/src/files/files.ts index 22199cb..2ff9d45 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -9,7 +9,15 @@ import type { ExcalidrawImperativeAPI, } from '@excalidraw/excalidraw/types/types' import { Collab } from '../collaboration/collab' -import type { ExcalidrawElement, FileId } from '@excalidraw/excalidraw/types/element/types' +import type { FileId } from '@excalidraw/excalidraw/types/element/types' + +type Meta = { + name: string, + type: string, + lastModified: number, + dataurl: string, + fileId: string, +} export class FileHandle { @@ -39,18 +47,27 @@ export class FileHandle { this.filesDragEventListener(ev), ) } + let lastPointerDown = 0 // eslint-disable-next-line @typescript-eslint/no-unused-vars this.excalidrawApi.onPointerDown((activeTool, state, event) => { - const clickedElement = this.getElementAt(state.lastCoords.x, state.lastCoords.y) - if (!clickedElement) { + const clickedElement = state.hit.element + if (!clickedElement || !clickedElement.customData) { return } - this.downloadFile(clickedElement.customData?.meta) + event.stopPropagation() + if (Date.now() - lastPointerDown > 200) { + lastPointerDown = Date.now() + return + } else { + lastPointerDown = Date.now() + } + this.downloadFile(clickedElement.customData.meta) }) } - private downloadFile(meta) { - const blob = new Blob([meta.dataurl], { type: meta.type }) + private downloadFile(meta: Meta) { + const file = this.excalidrawApi.getFiles()[meta.fileId] + const blob = new Blob([file.dataURL], { type: meta.type }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url @@ -59,17 +76,6 @@ export class FileHandle { URL.revokeObjectURL(url) } - private getElementAt(px: number, py: number): ExcalidrawElement | undefined { - const elements = this.excalidrawApi.getSceneElements() - return elements.find((element) => { - const { x, y, width, height } = element - return ( - px >= x && px <= x + width - && py >= y && py <= y + height - ) - }) - } - private filesDragEventListener(ev: Event) { if (ev instanceof DragEvent) { for (const file of Array.from(ev.dataTransfer?.files || [])) { @@ -89,41 +95,123 @@ export class FileHandle { fr.readAsDataURL(file) fr.onload = () => { const constructedFile: BinaryFileData = { - mimeType: 'image/png', - created: 0o0, + mimeType: file.type, + created: Date.now(), id: (Math.random() + 1).toString(36).substring(7) as FileId, dataURL: fr.result as DataURL, } - const meta = { - name: file.name, type: file.type, lastModified: file.lastModified, dataurl: fr.result, + if (typeof fr.result === 'string') { + const meta: Meta = { + name: file.name, + type: file.type, + lastModified: file.lastModified, + dataurl: fr.result, + fileId: constructedFile.id, + } + this.addCustomFileElement(constructedFile, meta) } - this.addCustomFileElement(constructedFile, meta) } } - private addCustomFileElement(constructedFile: BinaryFileData, meta) { - this.collab.addFile(constructedFile) + private addCustomFileElement(constructedFile: BinaryFileData, meta: Meta) { + this.collab.portal.sendImageFiles({ [constructedFile.id]: constructedFile }) const elements = this.excalidrawApi .getSceneElementsIncludingDeleted() .slice() const newElements = convertToExcalidrawElements([ { - type: 'text', - text: meta.name, + type: 'rectangle', + fillStyle: 'hachure', customData: { meta }, - groupIds: ['1'], + strokeWidth: 1, + strokeStyle: 'solid', + roughness: 0, + opacity: 30, + angle: 0, + x: 0, y: 0, - x: 50, + strokeColor: '#1e1e1e', + backgroundColor: '#a5d8ff', + width: 252.62770075583379, + height: 81.57857850076135, + seed: 1641118746, + groupIds: [meta.fileId], + frameId: null, + roundness: { + type: 3, + }, + boundElements: [], }, + // image to prevent excalidraw from removing file { type: 'image', - fileId: 'placeholder_image' as FileId, + fileId: meta.fileId as FileId, + x: 0, + y: 0, + height: 0, + width: 0, + opacity: 0, + locked: true, + groupIds: [meta.fileId], + }, + { + type: 'text', + customData: { meta }, + version: 248, + versionNonce: 94933274, + isDeleted: false, + id: 'sdDa83JaYdFr_Aja2q_z7', + fillStyle: 'solid', + strokeWidth: 1, + strokeStyle: 'solid', + roughness: 0, + opacity: 100, + angle: 0, + x: 85.2856430662, + y: 28.8678679811, + strokeColor: '#1e1e1e', + backgroundColor: 'transparent', + width: 140.625, + height: 24, + seed: 2067517530, + groupIds: [meta.fileId], + frameId: null, + roundness: null, + boundElements: [], + updated: 1733306011391, + link: null, + locked: false, + fontSize: 20, + fontFamily: 3, + text: meta.name.length > 14 ? meta.name.slice(0, 11) + '...' : meta.name, + textAlign: 'left', + verticalAlign: 'top', + containerId: null, + baseline: 20, + }, + { + type: 'ellipse', customData: { meta }, - groupIds: ['1'], - y: -10, - x: -10, - width: 50, - height: 50, + id: 'AaRO1KGioMv4hDDaJcmaI', + fillStyle: 'solid', + strokeWidth: 1, + strokeStyle: 'dotted', + roughness: 0, + opacity: 100, + angle: 0, + x: 28.8678679811, + y: 16.3505845419, + strokeColor: '#1e1e1e', + backgroundColor: '#a5d8ff', + width: 48.880073102719564, + height: 48.880073102719564, + seed: 1847675994, + groupIds: [meta.fileId], + frameId: null, + roundness: { + type: 2, + }, + boundElements: [], }, ]) elements.push(...newElements) From c4c72128752b96b02ca37dfaf84e98e1cb207fbb Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Tue, 10 Dec 2024 15:02:09 +0100 Subject: [PATCH 5/9] feat(fileuploads): add filetype icon Signed-off-by: grnd-alt --- src/files/files.ts | 100 ++++++++++++++++++++------------------------- 1 file changed, 45 insertions(+), 55 deletions(-) diff --git a/src/files/files.ts b/src/files/files.ts index 2ff9d45..5452396 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -10,12 +10,12 @@ import type { } from '@excalidraw/excalidraw/types/types' import { Collab } from '../collaboration/collab' import type { FileId } from '@excalidraw/excalidraw/types/element/types' +import axios from '@nextcloud/axios' type Meta = { name: string, type: string, lastModified: number, - dataurl: string, fileId: string, } @@ -35,13 +35,6 @@ export class FileHandle { const containerRef = document.getElementsByClassName( 'excalidraw-container', )[0] - const constructedFile: BinaryFileData = { - mimeType: 'image/png', - created: 0o0, - id: 'placeholder_image' as FileId, - dataURL: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTE0LDJMMjAsOFYyMEEyLDIgMCAwLDEgMTgsMjJINkEyLDIgMCAwLDEgNCwyMFY0QTIsMiAwIDAsMSA2LDJIMTRNMTgsMjBWOUgxM1Y0SDZWMjBIMThNMTIsMTlMOCwxNUgxMC41VjEySDEzLjVWMTVIMTZMMTIsMTlaIiAvPjwvc3ZnPg==' as DataURL, - } - this.collab.addFile(constructedFile) if (containerRef) { containerRef.addEventListener('drop', (ev) => this.filesDragEventListener(ev), @@ -105,7 +98,6 @@ export class FileHandle { name: file.name, type: file.type, lastModified: file.lastModified, - dataurl: fr.result, fileId: constructedFile.id, } this.addCustomFileElement(constructedFile, meta) @@ -113,7 +105,35 @@ export class FileHandle { } } - private addCustomFileElement(constructedFile: BinaryFileData, meta: Meta) { + private async getMimeIcon(mimeType: string): Promise { + let file = this.excalidrawApi.getFiles()[`filetype-icon-${mimeType}`] + if (!file) { + const iconUrl = window.OC.MimeType.getIconUrl(mimeType); + let response = await axios.get(iconUrl, { responseType: 'arraybuffer' }) + const blob = new Blob([response.data], { type:'image/svg+xml' }) + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + if (typeof reader.result === "string") { + file = { + mimeType: blob.type, + id: `filetype-icon-${mimeType}` as FileId, + dataURL: reader.result as DataURL + } + this.collab.portal.sendImageFiles({[file.id]: file}) + resolve(file.id) + } + console.log(`doing it ${reader.result} ${typeof reader.result}`) + } + reader.readAsDataURL(blob); + }) + } + return file.id + } + + private async addCustomFileElement(constructedFile: BinaryFileData, meta: Meta) { + const iconId = await this.getMimeIcon(meta.type) this.collab.portal.sendImageFiles({ [constructedFile.id]: constructedFile }) const elements = this.excalidrawApi .getSceneElementsIncludingDeleted() @@ -125,48 +145,47 @@ export class FileHandle { customData: { meta }, strokeWidth: 1, strokeStyle: 'solid', - roughness: 0, opacity: 30, - angle: 0, x: 0, y: 0, strokeColor: '#1e1e1e', backgroundColor: '#a5d8ff', - width: 252.62770075583379, + width: 260.62770075583379, height: 81.57857850076135, seed: 1641118746, groupIds: [meta.fileId], - frameId: null, roundness: { type: 3, }, - boundElements: [], }, - // image to prevent excalidraw from removing file { type: 'image', fileId: meta.fileId as FileId, - x: 0, - y: 0, - height: 0, + x: 28.8678679811, + y: 16.3505845419, width: 0, - opacity: 0, + height: 0, + locked: true, + groupIds: [meta.fileId], + }, + { + type: 'image', + fileId: iconId, + x: 28.8678679811, + y: 16.3505845419, + width: 48.880073102719564, + height: 48.880073102719564, locked: true, groupIds: [meta.fileId], }, { type: 'text', customData: { meta }, - version: 248, - versionNonce: 94933274, isDeleted: false, - id: 'sdDa83JaYdFr_Aja2q_z7', fillStyle: 'solid', strokeWidth: 1, strokeStyle: 'solid', - roughness: 0, opacity: 100, - angle: 0, x: 85.2856430662, y: 28.8678679811, strokeColor: '#1e1e1e', @@ -175,44 +194,15 @@ export class FileHandle { height: 24, seed: 2067517530, groupIds: [meta.fileId], - frameId: null, - roundness: null, - boundElements: [], updated: 1733306011391, - link: null, - locked: false, + locked: true, fontSize: 20, fontFamily: 3, text: meta.name.length > 14 ? meta.name.slice(0, 11) + '...' : meta.name, textAlign: 'left', verticalAlign: 'top', - containerId: null, baseline: 20, }, - { - type: 'ellipse', - customData: { meta }, - id: 'AaRO1KGioMv4hDDaJcmaI', - fillStyle: 'solid', - strokeWidth: 1, - strokeStyle: 'dotted', - roughness: 0, - opacity: 100, - angle: 0, - x: 28.8678679811, - y: 16.3505845419, - strokeColor: '#1e1e1e', - backgroundColor: '#a5d8ff', - width: 48.880073102719564, - height: 48.880073102719564, - seed: 1847675994, - groupIds: [meta.fileId], - frameId: null, - roundness: { - type: 2, - }, - boundElements: [], - }, ]) elements.push(...newElements) this.excalidrawApi.updateScene({ elements }) From 091839d73eb4ed342129edce9e2271dad30137cf Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Thu, 12 Dec 2024 14:37:44 +0100 Subject: [PATCH 6/9] feat(fileuploads): add toast message to download files Signed-off-by: grnd-alt --- src/collaboration/collab.ts | 5 +- src/files/{files.ts => files.tsx} | 76 ++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 29 deletions(-) rename src/files/{files.ts => files.tsx} (75%) diff --git a/src/collaboration/collab.ts b/src/collaboration/collab.ts index 91268ad..a7e9595 100644 --- a/src/collaboration/collab.ts +++ b/src/collaboration/collab.ts @@ -9,7 +9,7 @@ import { Portal } from './Portal' import { restoreElements } from '@excalidraw/excalidraw' import { throttle } from 'lodash' import { hashElementsVersion, reconcileElements } from './util' -import { registerFilesHandler, type FileHandle } from '../files/files' +import { registerFilesHandler } from '../files/files.tsx' export class Collab { @@ -21,7 +21,6 @@ export class Collab { lastBroadcastedOrReceivedSceneVersion: number = -1 private collaborators = new Map() private files = new Map() - private fileHandle: FileHandle constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch>) { this.excalidrawAPI = excalidrawAPI @@ -30,7 +29,7 @@ export class Collab { this.setViewModeEnabled = setViewModeEnabled this.portal = new Portal(`${fileId}`, this, publicSharingToken) - this.fileHandle = registerFilesHandler(this.excalidrawAPI, this) + registerFilesHandler(this.excalidrawAPI, this) } async startCollab() { diff --git a/src/files/files.ts b/src/files/files.tsx similarity index 75% rename from src/files/files.ts rename to src/files/files.tsx index 5452396..b810e8c 100644 --- a/src/files/files.ts +++ b/src/files/files.tsx @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { convertToExcalidrawElements } from '@excalidraw/excalidraw' +import { showMessage, ToastType } from '@nextcloud/dialogs' import type { BinaryFileData, DataURL, @@ -11,6 +12,9 @@ import type { import { Collab } from '../collaboration/collab' import type { FileId } from '@excalidraw/excalidraw/types/element/types' import axios from '@nextcloud/axios' +import { createRoot } from 'react-dom' +import { mdiDownloadBox } from '@mdi/js' +import { Icon } from '@mdi/react' type Meta = { name: string, @@ -24,11 +28,13 @@ export class FileHandle { private collab: Collab private excalidrawApi: ExcalidrawImperativeAPI private types: string[] + private openDownloadToasts: string[] constructor( excalidrawApi: ExcalidrawImperativeAPI, collab: Collab, types: string[], ) { + this.openDownloadToasts = [] this.collab = collab this.excalidrawApi = excalidrawApi this.types = types @@ -40,33 +46,51 @@ export class FileHandle { this.filesDragEventListener(ev), ) } - let lastPointerDown = 0 // eslint-disable-next-line @typescript-eslint/no-unused-vars this.excalidrawApi.onPointerDown((activeTool, state, event) => { const clickedElement = state.hit.element if (!clickedElement || !clickedElement.customData) { return } - event.stopPropagation() - if (Date.now() - lastPointerDown > 200) { - lastPointerDown = Date.now() - return - } else { - lastPointerDown = Date.now() - } - this.downloadFile(clickedElement.customData.meta) + this.downloadDialog(clickedElement.customData.meta) + }) + } + + private renderDownloadBox(meta: Meta) { + return ( +
+ + {meta.name} + + +
+ ) + } + + private downloadDialog(meta: Meta) { + if (this.openDownloadToasts.indexOf(meta.fileId) !== -1) return + + const undoContent = document.createElement('div') + const root = createRoot(undoContent) + root.render(this.renderDownloadBox(meta)) + this.openDownloadToasts.push(meta.fileId) + showMessage(undoContent, { + type: ToastType.INFO, + close: true, + onRemove: () => { + this.openDownloadToasts = this.openDownloadToasts.filter((val) => val !== meta.fileId) + }, + onClick: () => { this.downloadFile(meta) }, }) } private downloadFile(meta: Meta) { const file = this.excalidrawApi.getFiles()[meta.fileId] - const blob = new Blob([file.dataURL], { type: meta.type }) - const url = URL.createObjectURL(blob) + const url = file.dataURL const a = document.createElement('a') a.href = url a.download = meta.name a.click() - URL.revokeObjectURL(url) } private filesDragEventListener(ev: Event) { @@ -108,25 +132,24 @@ export class FileHandle { private async getMimeIcon(mimeType: string): Promise { let file = this.excalidrawApi.getFiles()[`filetype-icon-${mimeType}`] if (!file) { - const iconUrl = window.OC.MimeType.getIconUrl(mimeType); - let response = await axios.get(iconUrl, { responseType: 'arraybuffer' }) - const blob = new Blob([response.data], { type:'image/svg+xml' }) + const iconUrl = window.OC.MimeType.getIconUrl(mimeType) + const response = await axios.get(iconUrl, { responseType: 'arraybuffer' }) + const blob = new Blob([response.data], { type: 'image/svg+xml' }) - return new Promise((resolve, reject) => { - const reader = new FileReader(); + return new Promise((resolve) => { + const reader = new FileReader() reader.onloadend = () => { - if (typeof reader.result === "string") { + if (typeof reader.result === 'string') { file = { mimeType: blob.type, id: `filetype-icon-${mimeType}` as FileId, - dataURL: reader.result as DataURL + dataURL: reader.result as DataURL, } - this.collab.portal.sendImageFiles({[file.id]: file}) + this.collab.portal.sendImageFiles({ [file.id]: file }) resolve(file.id) } - console.log(`doing it ${reader.result} ${typeof reader.result}`) } - reader.readAsDataURL(blob); + reader.readAsDataURL(blob) }) } return file.id @@ -150,8 +173,8 @@ export class FileHandle { y: 0, strokeColor: '#1e1e1e', backgroundColor: '#a5d8ff', - width: 260.62770075583379, - height: 81.57857850076135, + width: 260.62, + height: 81.57, seed: 1641118746, groupIds: [meta.fileId], roundness: { @@ -163,8 +186,9 @@ export class FileHandle { fileId: meta.fileId as FileId, x: 28.8678679811, y: 16.3505845419, - width: 0, - height: 0, + width: 1, + height: 1, + opacity: 0, locked: true, groupIds: [meta.fileId], }, From b4865cd7bd32a34618f3ea8a086c6ba39f2862e3 Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Tue, 17 Dec 2024 12:55:04 +0100 Subject: [PATCH 7/9] feat(fileupload): add SideBar Download Signed-off-by: grnd-alt --- src/collaboration/collab.ts | 2 +- src/files/SideBarDownload.tsx | 43 ++++++++++++++++++ src/files/{files.tsx => files.ts} | 72 ++++++++++++------------------- src/files/toastDownloadDialog.tsx | 37 ++++++++++++++++ 4 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 src/files/SideBarDownload.tsx rename src/files/{files.tsx => files.ts} (76%) create mode 100644 src/files/toastDownloadDialog.tsx diff --git a/src/collaboration/collab.ts b/src/collaboration/collab.ts index a7e9595..cb545ff 100644 --- a/src/collaboration/collab.ts +++ b/src/collaboration/collab.ts @@ -9,7 +9,7 @@ import { Portal } from './Portal' import { restoreElements } from '@excalidraw/excalidraw' import { throttle } from 'lodash' import { hashElementsVersion, reconcileElements } from './util' -import { registerFilesHandler } from '../files/files.tsx' +import { registerFilesHandler } from '../files/files.ts' export class Collab { diff --git a/src/files/SideBarDownload.tsx b/src/files/SideBarDownload.tsx new file mode 100644 index 0000000..444e656 --- /dev/null +++ b/src/files/SideBarDownload.tsx @@ -0,0 +1,43 @@ +import type { Meta } from './files' +import { createRoot } from 'react-dom' +import { mdiDownloadBox } from '@mdi/js' +import { Icon } from '@mdi/react' + +function renderDownloadBox(meta: Meta) { + return ( +
+ + {meta.name} + + +
+ ) +} + +export async function downloadDialog(meta: Meta, onClick: () => void) { + const observer = new MutationObserver(() => { + const sideBar = document.getElementsByClassName('App-menu__left')[0] + if (sideBar !== undefined) { + observer.disconnect() + } else { + return + } + const newElement = document.createElement('div') + const root = createRoot(newElement) + root.render(renderDownloadBox(meta)) + newElement.addEventListener('click', onClick) + + const panelColumn = sideBar.querySelector('.panelColumn') + if (panelColumn) { + panelColumn.insertBefore(newElement, panelColumn.firstChild) + } else { + sideBar.appendChild(newElement) + } + }) + observer.observe(document.body, { childList: true, subtree: true }) +} diff --git a/src/files/files.tsx b/src/files/files.ts similarity index 76% rename from src/files/files.tsx rename to src/files/files.ts index b810e8c..0ff5a32 100644 --- a/src/files/files.tsx +++ b/src/files/files.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ import { convertToExcalidrawElements } from '@excalidraw/excalidraw' -import { showMessage, ToastType } from '@nextcloud/dialogs' import type { BinaryFileData, DataURL, @@ -12,15 +11,14 @@ import type { import { Collab } from '../collaboration/collab' import type { FileId } from '@excalidraw/excalidraw/types/element/types' import axios from '@nextcloud/axios' -import { createRoot } from 'react-dom' -import { mdiDownloadBox } from '@mdi/js' -import { Icon } from '@mdi/react' +import { downloadDialog } from './SideBarDownload' -type Meta = { - name: string, - type: string, - lastModified: number, - fileId: string, +export type Meta = { + name: string + type: string + lastModified: number + fileId: string + dataURL: string } export class FileHandle { @@ -47,46 +45,19 @@ export class FileHandle { ) } // eslint-disable-next-line @typescript-eslint/no-unused-vars - this.excalidrawApi.onPointerDown((activeTool, state, event) => { + this.excalidrawApi.onPointerDown(async (activeTool, state, event) => { const clickedElement = state.hit.element if (!clickedElement || !clickedElement.customData) { return } - this.downloadDialog(clickedElement.customData.meta) - }) - } - - private renderDownloadBox(meta: Meta) { - return ( -
- - {meta.name} - - -
- ) - } - - private downloadDialog(meta: Meta) { - if (this.openDownloadToasts.indexOf(meta.fileId) !== -1) return - - const undoContent = document.createElement('div') - const root = createRoot(undoContent) - root.render(this.renderDownloadBox(meta)) - this.openDownloadToasts.push(meta.fileId) - showMessage(undoContent, { - type: ToastType.INFO, - close: true, - onRemove: () => { - this.openDownloadToasts = this.openDownloadToasts.filter((val) => val !== meta.fileId) - }, - onClick: () => { this.downloadFile(meta) }, + downloadDialog(clickedElement.customData.meta, () => + this.downloadFile(clickedElement.customData!.meta), + ) }) } private downloadFile(meta: Meta) { - const file = this.excalidrawApi.getFiles()[meta.fileId] - const url = file.dataURL + const url = meta.dataURL const a = document.createElement('a') a.href = url a.download = meta.name @@ -123,6 +94,7 @@ export class FileHandle { type: file.type, lastModified: file.lastModified, fileId: constructedFile.id, + dataURL: fr.result, } this.addCustomFileElement(constructedFile, meta) } @@ -133,7 +105,9 @@ export class FileHandle { let file = this.excalidrawApi.getFiles()[`filetype-icon-${mimeType}`] if (!file) { const iconUrl = window.OC.MimeType.getIconUrl(mimeType) - const response = await axios.get(iconUrl, { responseType: 'arraybuffer' }) + const response = await axios.get(iconUrl, { + responseType: 'arraybuffer', + }) const blob = new Blob([response.data], { type: 'image/svg+xml' }) return new Promise((resolve) => { @@ -155,9 +129,14 @@ export class FileHandle { return file.id } - private async addCustomFileElement(constructedFile: BinaryFileData, meta: Meta) { + private async addCustomFileElement( + constructedFile: BinaryFileData, + meta: Meta, + ) { const iconId = await this.getMimeIcon(meta.type) - this.collab.portal.sendImageFiles({ [constructedFile.id]: constructedFile }) + this.collab.portal.sendImageFiles({ + [constructedFile.id]: constructedFile, + }) const elements = this.excalidrawApi .getSceneElementsIncludingDeleted() .slice() @@ -222,7 +201,10 @@ export class FileHandle { locked: true, fontSize: 20, fontFamily: 3, - text: meta.name.length > 14 ? meta.name.slice(0, 11) + '...' : meta.name, + text: + meta.name.length > 14 + ? meta.name.slice(0, 11) + '...' + : meta.name, textAlign: 'left', verticalAlign: 'top', baseline: 20, diff --git a/src/files/toastDownloadDialog.tsx b/src/files/toastDownloadDialog.tsx new file mode 100644 index 0000000..912e409 --- /dev/null +++ b/src/files/toastDownloadDialog.tsx @@ -0,0 +1,37 @@ +import { showMessage, ToastType } from '@nextcloud/dialogs' +import { createRoot } from 'react-dom' +import { mdiDownloadBox } from '@mdi/js' +import { Icon } from '@mdi/react' +import { type Meta } from './files' + +function renderDownloadBox(meta: Meta) { + return ( +
+ + {meta.name} + + +
+ ) +} + +export function downloadDialog( + meta: Meta, + onClick: () => void, + onRemove: () => void, +) { + const undoContent = document.createElement('div') + const root = createRoot(undoContent) + root.render(renderDownloadBox(meta)) + showMessage(undoContent, { + type: ToastType.INFO, + close: true, + onRemove, + onClick, + }) +} From 5823b4ff0809ff44f337456a29b32d59fb13bb47 Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Wed, 18 Dec 2024 14:41:55 +0100 Subject: [PATCH 8/9] only show download button in sidebar Signed-off-by: grnd-alt --- src/files/SideBarDownload.tsx | 100 ++++++++++++++++++++++++++---- src/files/files.ts | 5 +- src/files/toastDownloadDialog.tsx | 37 ----------- 3 files changed, 92 insertions(+), 50 deletions(-) delete mode 100644 src/files/toastDownloadDialog.tsx diff --git a/src/files/SideBarDownload.tsx b/src/files/SideBarDownload.tsx index 444e656..f004006 100644 --- a/src/files/SideBarDownload.tsx +++ b/src/files/SideBarDownload.tsx @@ -1,43 +1,121 @@ -import type { Meta } from './files' +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ import { createRoot } from 'react-dom' +import { type JSX } from 'react' +import { type Meta } from './files' import { mdiDownloadBox } from '@mdi/js' import { Icon } from '@mdi/react' -function renderDownloadBox(meta: Meta) { +/** + * renders the html button for file downloads + * @param meta file data + * @param onClick onClick callback + * @return {JSX.Element} rendered Button JSX + */ +function renderDownloadButton(meta: Meta, onClick: () => void): JSX.Element { + const iconUrl = window.OC.MimeType.getIconUrl(meta.type) return (
- + + {meta.name} - +
) } -export async function downloadDialog(meta: Meta, onClick: () => void) { +/** + * removes the download button from the sidebar + * makes all default excalidraw settings visible again + * @return {void} + */ +export function ResetDownloadButton() { + const sideBar = document.getElementsByClassName('App-menu__left')[0] + if (sideBar === undefined) { + return + } + const panelColumn = sideBar.querySelector('.panelColumn') + if (panelColumn) { + panelColumn.childNodes.forEach((node) => { + if (!node.ELEMENT_NODE) { + return + } + const element = node as HTMLElement + if (element.style.display === 'none') { + element.style.display = '' + } + if (element.classList.contains('nc-download')) { + panelColumn.removeChild(element) + } + }) + } +} + +/** + * clears the excalidraw sidebar as soon as it appears + * inserts a download button with the file name instead + * @param meta file data + * @param onClick onClick callback + */ +export function InsertDownloadButton(meta: Meta, onClick: () => void) { const observer = new MutationObserver(() => { const sideBar = document.getElementsByClassName('App-menu__left')[0] - if (sideBar !== undefined) { - observer.disconnect() - } else { + if (sideBar === undefined) { return } + observer.disconnect() const newElement = document.createElement('div') + newElement.classList.add('nc-download') const root = createRoot(newElement) - root.render(renderDownloadBox(meta)) + root.render(renderDownloadButton(meta, onClick)) newElement.addEventListener('click', onClick) const panelColumn = sideBar.querySelector('.panelColumn') if (panelColumn) { - panelColumn.insertBefore(newElement, panelColumn.firstChild) + panelColumn.childNodes.forEach((node) => { + // hide all defautl excalidraw setting elements + if (!node.ELEMENT_NODE) { + return + } + const element = node as HTMLElement + element.style.display = 'none' + }) + panelColumn.appendChild(newElement) } else { sideBar.appendChild(newElement) } }) + + // wait until sidebar rendered by excalidraw observer.observe(document.body, { childList: true, subtree: true }) } diff --git a/src/files/files.ts b/src/files/files.ts index 0ff5a32..0145633 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -11,7 +11,7 @@ import type { import { Collab } from '../collaboration/collab' import type { FileId } from '@excalidraw/excalidraw/types/element/types' import axios from '@nextcloud/axios' -import { downloadDialog } from './SideBarDownload' +import { InsertDownloadButton, ResetDownloadButton } from './SideBarDownload' export type Meta = { name: string @@ -48,9 +48,10 @@ export class FileHandle { this.excalidrawApi.onPointerDown(async (activeTool, state, event) => { const clickedElement = state.hit.element if (!clickedElement || !clickedElement.customData) { + ResetDownloadButton() return } - downloadDialog(clickedElement.customData.meta, () => + InsertDownloadButton(clickedElement.customData.meta, () => this.downloadFile(clickedElement.customData!.meta), ) }) diff --git a/src/files/toastDownloadDialog.tsx b/src/files/toastDownloadDialog.tsx deleted file mode 100644 index 912e409..0000000 --- a/src/files/toastDownloadDialog.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { showMessage, ToastType } from '@nextcloud/dialogs' -import { createRoot } from 'react-dom' -import { mdiDownloadBox } from '@mdi/js' -import { Icon } from '@mdi/react' -import { type Meta } from './files' - -function renderDownloadBox(meta: Meta) { - return ( -
- - {meta.name} - - -
- ) -} - -export function downloadDialog( - meta: Meta, - onClick: () => void, - onRemove: () => void, -) { - const undoContent = document.createElement('div') - const root = createRoot(undoContent) - root.render(renderDownloadBox(meta)) - showMessage(undoContent, { - type: ToastType.INFO, - close: true, - onRemove, - onClick, - }) -} From 98e6d683c5bbb9dc9cd3d8996680aa20b05c99e9 Mon Sep 17 00:00:00 2001 From: grnd-alt Date: Wed, 18 Dec 2024 15:23:41 +0100 Subject: [PATCH 9/9] insert file element at cursor position Signed-off-by: grnd-alt --- src/files/SideBarDownload.tsx | 49 ++++++++++++++--------------------- src/files/files.ts | 33 ++++++++++++++--------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/src/files/SideBarDownload.tsx b/src/files/SideBarDownload.tsx index f004006..2a015a7 100644 --- a/src/files/SideBarDownload.tsx +++ b/src/files/SideBarDownload.tsx @@ -64,21 +64,12 @@ export function ResetDownloadButton() { if (sideBar === undefined) { return } - const panelColumn = sideBar.querySelector('.panelColumn') + const panelColumn = sideBar.querySelector('.panelColumn') as HTMLElement if (panelColumn) { - panelColumn.childNodes.forEach((node) => { - if (!node.ELEMENT_NODE) { - return - } - const element = node as HTMLElement - if (element.style.display === 'none') { - element.style.display = '' - } - if (element.classList.contains('nc-download')) { - panelColumn.removeChild(element) - } - }) + panelColumn.style.display = '' } + const downloadButton = document.getElementsByClassName('nc-download')[0] + sideBar.removeChild(downloadButton) } /** @@ -88,7 +79,7 @@ export function ResetDownloadButton() { * @param onClick onClick callback */ export function InsertDownloadButton(meta: Meta, onClick: () => void) { - const observer = new MutationObserver(() => { + const callback = () => { const sideBar = document.getElementsByClassName('App-menu__left')[0] if (sideBar === undefined) { return @@ -98,24 +89,22 @@ export function InsertDownloadButton(meta: Meta, onClick: () => void) { newElement.classList.add('nc-download') const root = createRoot(newElement) root.render(renderDownloadButton(meta, onClick)) - newElement.addEventListener('click', onClick) - const panelColumn = sideBar.querySelector('.panelColumn') + // hide all excalidraw settings + const panelColumn = sideBar.querySelector('.panelColumn') as HTMLElement if (panelColumn) { - panelColumn.childNodes.forEach((node) => { - // hide all defautl excalidraw setting elements - if (!node.ELEMENT_NODE) { - return - } - const element = node as HTMLElement - element.style.display = 'none' - }) - panelColumn.appendChild(newElement) - } else { - sideBar.appendChild(newElement) + panelColumn.style.display = 'none' } - }) - // wait until sidebar rendered by excalidraw - observer.observe(document.body, { childList: true, subtree: true }) + sideBar.appendChild(newElement) + } + + const observer = new MutationObserver(callback) + + const sideBar = document.getElementsByClassName('App-menu__left')[0] + if (sideBar !== undefined) { + callback() + } else { + observer.observe(document.body, { childList: true, subtree: true }) + } } diff --git a/src/files/files.ts b/src/files/files.ts index 0145633..6cd37ab 100644 --- a/src/files/files.ts +++ b/src/files/files.ts @@ -2,7 +2,10 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { convertToExcalidrawElements } from '@excalidraw/excalidraw' +import { + convertToExcalidrawElements, + viewportCoordsToSceneCoords, +} from '@excalidraw/excalidraw' import type { BinaryFileData, DataURL, @@ -48,7 +51,7 @@ export class FileHandle { this.excalidrawApi.onPointerDown(async (activeTool, state, event) => { const clickedElement = state.hit.element if (!clickedElement || !clickedElement.customData) { - ResetDownloadButton() + ResetDownloadButton() return } InsertDownloadButton(clickedElement.customData.meta, () => @@ -65,7 +68,7 @@ export class FileHandle { a.click() } - private filesDragEventListener(ev: Event) { + private filesDragEventListener(ev: DragEvent) { if (ev instanceof DragEvent) { for (const file of Array.from(ev.dataTransfer?.files || [])) { this.handleFileInsert(file, ev) @@ -97,7 +100,7 @@ export class FileHandle { fileId: constructedFile.id, dataURL: fr.result, } - this.addCustomFileElement(constructedFile, meta) + this.addCustomFileElement(constructedFile, meta, ev.x, ev.y) } } } @@ -133,7 +136,13 @@ export class FileHandle { private async addCustomFileElement( constructedFile: BinaryFileData, meta: Meta, + clientX: number, + clientY: number, ) { + const { x, y } = viewportCoordsToSceneCoords( + { clientX, clientY }, + this.excalidrawApi.getAppState(), + ) const iconId = await this.getMimeIcon(meta.type) this.collab.portal.sendImageFiles({ [constructedFile.id]: constructedFile, @@ -149,8 +158,8 @@ export class FileHandle { strokeWidth: 1, strokeStyle: 'solid', opacity: 30, - x: 0, - y: 0, + x, + y, strokeColor: '#1e1e1e', backgroundColor: '#a5d8ff', width: 260.62, @@ -164,8 +173,8 @@ export class FileHandle { { type: 'image', fileId: meta.fileId as FileId, - x: 28.8678679811, - y: 16.3505845419, + x: x + 28.8678679811, + y: y + 16.3505845419, width: 1, height: 1, opacity: 0, @@ -175,8 +184,8 @@ export class FileHandle { { type: 'image', fileId: iconId, - x: 28.8678679811, - y: 16.3505845419, + x: x + 28.8678679811, + y: y + 16.3505845419, width: 48.880073102719564, height: 48.880073102719564, locked: true, @@ -190,8 +199,8 @@ export class FileHandle { strokeWidth: 1, strokeStyle: 'solid', opacity: 100, - x: 85.2856430662, - y: 28.8678679811, + x: x + 85.2856430662, + y: y + 28.8678679811, strokeColor: '#1e1e1e', backgroundColor: 'transparent', width: 140.625,