Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: uploading files to nextcloud #278

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
\.idea/

/build/
/backup/
/js/
/dist/
/css/
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The official whiteboard app for Nextcloud. It allows users to create and share w
<screenshot>https://raw.githubusercontent.com/nextcloud/whiteboard/main/screenshots/screenshot1.png</screenshot>

<dependencies>
<nextcloud min-version="28" max-version="30"/>
<nextcloud min-version="28" max-version="31"/>
</dependencies>

<settings>
Expand Down
3 changes: 3 additions & 0 deletions src/collaboration/collab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -20,6 +21,7 @@ export class Collab {
lastBroadcastedOrReceivedSceneVersion: number = -1
private collaborators = new Map<string, Collaborator>()
private files = new Map<string, BinaryFileData>()
private fileHandle: FileHandle

constructor(excalidrawAPI: ExcalidrawImperativeAPI, fileId: number, publicSharingToken: string | null, setViewModeEnabled: React.Dispatch<React.SetStateAction<boolean>>) {
this.excalidrawAPI = excalidrawAPI
Expand All @@ -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() {
Expand Down
162 changes: 162 additions & 0 deletions src/files/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { convertToExcalidrawElements } from '@excalidraw/excalidraw'
import type {
BinaryFileData,
DataURL,
ExcalidrawImperativeAPI,
} from '@excalidraw/excalidraw/types/types'
import { Collab } from '../collaboration/collab'
import type { ExcalidrawElement, FileId } from '@excalidraw/excalidraw/types/element/types'

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]
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),
)
}
// 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 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 (const 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()

const fr = new FileReader()
fr.readAsDataURL(file)
fr.onload = () => {
const constructedFile: BinaryFileData = {
mimeType: 'image/png',
created: 0o0,
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,
}
this.addCustomFileElement(constructedFile, meta)
}
}

private addCustomFileElement(constructedFile: BinaryFileData, meta) {
this.collab.addFile(constructedFile)
const elements = this.excalidrawApi
.getSceneElementsIncludingDeleted()
.slice()
const newElements = convertToExcalidrawElements([
{
type: 'text',
text: meta.name,
customData: { meta },
groupIds: ['1'],
y: 0,
x: 50,
},
{
type: 'image',
fileId: 'placeholder_image' as FileId,
customData: { meta },
groupIds: ['1'],
y: -10,
x: -10,
width: 50,
height: 50,
},
])
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
* @param collab {Collab} collab
*/
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)
}
Loading