From 750f1930a70e37575f778f12cae1df2fbb6ae73a Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Sat, 12 Nov 2022 15:14:51 +0100 Subject: [PATCH] File drop support (#4247) --- static/multifile-service.ts | 60 +++++++++++---- static/panes/editor.ts | 1 - static/panes/tree.ts | 129 ++++++++++++++++++++++++++++++--- static/styles/explorer.scss | 7 ++ views/templates/panes/tree.pug | 2 + 5 files changed, 174 insertions(+), 25 deletions(-) diff --git a/static/multifile-service.ts b/static/multifile-service.ts index 5b6261b70e8..55b127a0836 100644 --- a/static/multifile-service.ts +++ b/static/multifile-service.ts @@ -114,7 +114,10 @@ export class MultifileService { }); if (possibleLang.length > 0) { - return possibleLang[0].id; + const sorted = _.sortBy(possibleLang, a => { + return a.extensions.indexOf(filenameExt); + }); + return sorted[0].id; } if (this.isCMakeFile(filename)) { @@ -244,7 +247,7 @@ export class MultifileService { } public isEditorPartOfProject(editorId: number) { - const found = _.find(this.files, (file: MultifileFile) => { + const found = this.files.find((file: MultifileFile) => { return file.isIncluded && file.isOpen && editorId === file.editorId; }); @@ -252,7 +255,7 @@ export class MultifileService { } public getFileByFileId(fileId: number): MultifileFile | undefined { - const file = _.find(this.files, (file: MultifileFile) => { + const file = this.files.find((file: MultifileFile) => { return file.fileId === fileId; }); @@ -277,17 +280,17 @@ export class MultifileService { } private filterOutNonsense() { - this.files = _.filter(this.files, (file: MultifileFile) => MultifileService.isValidFile(file)); + this.files = this.files.filter((file: MultifileFile) => MultifileService.isValidFile(file)); } public getFiles(): Array { this.filterOutNonsense(); - const filtered = _.filter(this.files, (file: MultifileFile) => { + const filtered = this.files.filter((file: MultifileFile) => { return !file.isMainSource && file.isIncluded; }); - return _.map(filtered, (file: MultifileFile) => { + return filtered.map((file: MultifileFile) => { return { filename: file.filename, contents: this.getFileContents(file), @@ -322,7 +325,7 @@ export class MultifileService { } public getMainSource(): string { - const mainFile = _.find(this.files, (file: MultifileFile) => { + const mainFile = this.files.find((file: MultifileFile) => { return file.isIncluded && this.isMainSourceFile(file); }); @@ -334,21 +337,27 @@ export class MultifileService { } public getFileByEditorId(editorId: number): MultifileFile | undefined { - return _.find(this.files, (file: MultifileFile) => { + return this.files.find((file: MultifileFile) => { return file.editorId === editorId; }); } public getEditorIdByFilename(filename: string): number | null { - const file = _.find(this.files, (file: MultifileFile) => { + const file = this.files.find((file: MultifileFile) => { return file.isIncluded && file.filename === filename; }); return file && file.editorId > 0 ? file.editorId : null; } + private getFileByFilename(filename: string): MultifileFile | undefined { + return this.files.find((file: MultifileFile) => { + return file.filename === filename; + }); + } + public getMainSourceEditorId(): number | null { - const file = _.find(this.files, (file: MultifileFile) => { + const file = this.files.find((file: MultifileFile) => { return file.isIncluded && this.isMainSourceFile(file); }); @@ -385,6 +394,14 @@ export class MultifileService { return file; } + public removeFileByFilename(filename: string): MultifileFile | undefined { + const file = this.getFileByFilename(filename); + if (file) { + this.files = this.files.filter((obj: MultifileFile) => obj.fileId !== file.fileId); + } + return file; + } + public async excludeByFileId(fileId: number): Promise { const file = this.getFileByFileId(fileId); if (file) { @@ -470,12 +487,29 @@ export class MultifileService { return suggestedFilename; } - private fileExists(filename: string, excludeFile: MultifileFile): boolean { - return !!_.find(this.files, (file: MultifileFile) => { - return file !== excludeFile && file.filename === filename; + public fileExists(filename: string, excludeFile?: MultifileFile): boolean { + return this.files.some((file: MultifileFile) => { + if (excludeFile && file === excludeFile) return false; + + return file.filename === filename; }); } + public addNewTextFile(filename: string, content: string) { + const file: MultifileFile = { + fileId: this.newFileId, + isIncluded: false, + isOpen: false, + isMainSource: false, + filename: filename, + content: content, + editorId: -1, + langId: this.getLanguageIdFromFilename(filename), + }; + + this.addFile(file); + } + public async renameFile(fileId: number): Promise { const file = this.getFileByFileId(fileId); if (!file) return Promise.reject('File could not be found'); diff --git a/static/panes/editor.ts b/static/panes/editor.ts index decacb688cb..18491a18acd 100644 --- a/static/panes/editor.ts +++ b/static/panes/editor.ts @@ -1726,7 +1726,6 @@ export class Editor extends MonacoPane { const files = e.target.files; if (files && files.length > 0) { - this.multifileService.forEachFile((file: MultifileFile) => { - this.removeFile(file.fileId); - }); - - await this.multifileService.loadProjectFromFile(files[0], (file: MultifileFile) => { - this.refresh(); - if (file.filename === 'CMakeLists.txt') { - // todo: find a way to toggle on CMake checkbox... - this.editFile(file.fileId); - } - }); + await this.openZipFile(files[0]); } }); @@ -518,6 +522,109 @@ export class Tree { this.domRoot.find('.options'), state as unknown as Record ); + + let drophereHideTimeout; + this.root.on('dragover', ev => { + ev.preventDefault(); + + if (drophereHideTimeout) clearTimeout(drophereHideTimeout); + + const drophere = this.root.find('.drophere'); + drophere.show(); + }); + + this.root.on('dragleave', () => { + const drophere = this.root.find('.drophere'); + drophereHideTimeout = setTimeout(() => { + drophere.hide(); + }, 1000); + }); + + this.root.on('drop', async (ev: any) => { + ev.preventDefault(); + + const drophere = this.root.find('.drophere'); + drophere.hide(); + + const dataTransfer = ev.originalEvent.dataTransfer; + if (dataTransfer.items) { + [...dataTransfer.items].forEach(async (item, i) => { + if (item.kind === 'file') { + const file = item.getAsFile(); + if (file.name.endsWith('.zip')) { + this.openZipFile(file); + } else { + await this.addSingleFile(file); + } + } + }); + } else { + [...dataTransfer.files].forEach(async (file, i) => { + if (file.name.endsWith('.zip')) { + this.openZipFile(file); + } else { + await this.addSingleFile(file); + } + }); + } + }); + } + + private async openZipFile(htmlfile) { + this.multifileService.forEachFile((file: MultifileFile) => { + this.removeFile(file.fileId); + }); + + await this.multifileService.loadProjectFromFile(htmlfile, (file: MultifileFile) => { + this.refresh(); + if (file.filename === 'CMakeLists.txt') { + // todo: find a way to toggle on CMake checkbox... + this.editFile(file.fileId); + } + }); + } + + private async askForOverwriteAndDo(filename): Promise { + return new Promise((resolve, reject) => { + if (this.multifileService.fileExists(filename)) { + this.alertSystem.ask('Overwrite file', `${_.escape(filename)} already exists, overwrite this file?`, { + yes: () => { + this.removeFileByFilename(filename); + resolve(); + }, + no: () => { + reject(); + }, + onClose: () => { + reject(); + }, + yesClass: 'btn-danger', + yesHtml: 'Yes', + noClass: 'btn-primary', + noHtml: 'No', + }); + } else { + resolve(); + } + }); + } + + private async addSingleFile(htmlfile): Promise { + try { + await this.askForOverwriteAndDo(htmlfile.name); + + return new Promise(resolve => { + const fr = new FileReader(); + fr.onload = () => { + this.multifileService.addNewTextFile(htmlfile.name, fr.result?.toString() || ''); + this.refresh(); + resolve(); + }; + fr.readAsText(htmlfile); + }); + } catch { + // expected when user says no + } } private numberUsedLines() { diff --git a/static/styles/explorer.scss b/static/styles/explorer.scss index 9a1567e0df4..d286c3ec935 100644 --- a/static/styles/explorer.scss +++ b/static/styles/explorer.scss @@ -866,6 +866,13 @@ html[data-theme='dark'] { float: right; padding: 4px 10px 4px 0; } +.tree .drophere { + margin: 5px; + border: dashed rgba(127, 127, 127, 0.2); + border-radius: 16px; + height: 150px; +} + .mainbar .cmake-project { margin-top: 2px; } diff --git a/views/templates/panes/tree.pug b/views/templates/panes/tree.pug index 7a010965ac3..7782ebd19d9 100644 --- a/views/templates/panes/tree.pug +++ b/views/templates/panes/tree.pug @@ -41,3 +41,5 @@ li.root.list-group-item.far-fa-folder .group-header Excluded files ul.list-group.unnamed-editors + li.drophere.row.align-items-center(style="display: none;") + span.col-lg.text-center Drop files here