mirror of
https://github.com/Luzifer/ots.git
synced 2024-12-18 12:04:35 -05:00
[#181] Add paste ability for files to textarea
closes #181 Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
6e2f20aa53
commit
b41db78745
@ -36,6 +36,7 @@
|
||||
v-model="secret"
|
||||
class="form-control"
|
||||
:rows="2"
|
||||
@pasteFile="handlePasteFile"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@ -50,7 +51,7 @@
|
||||
type="file"
|
||||
multiple
|
||||
:accept="$root.customize.acceptedFileTypes"
|
||||
@change="updateFileMeta"
|
||||
@change="handleSelectFiles"
|
||||
>
|
||||
<div class="form-text">
|
||||
{{ $t('text-max-filesize', { maxSize: bytesToHuman(maxFileSize) }) }}
|
||||
@ -67,6 +68,14 @@
|
||||
>
|
||||
{{ $t('text-max-filesize-exceeded', { curSize: bytesToHuman(fileSize), maxSize: bytesToHuman(maxFileSize) }) }}
|
||||
</div>
|
||||
<FilesDisplay
|
||||
v-if="attachedFiles.length > 0"
|
||||
class="mt-3"
|
||||
:can-delete="true"
|
||||
:track-download="false"
|
||||
:files="attachedFiles"
|
||||
@fileClicked="deleteFile"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6 col-12 order-2 order-md-1">
|
||||
<button
|
||||
@ -117,6 +126,7 @@
|
||||
|
||||
import appCrypto from '../crypto.js'
|
||||
import { bytesToHuman } from '../helpers'
|
||||
import FilesDisplay from './fileDisplay.vue'
|
||||
import GrowArea from './growarea.vue'
|
||||
import OTSMeta from '../ots-meta'
|
||||
|
||||
@ -148,7 +158,7 @@ const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS
|
||||
const passwordLength = 20
|
||||
|
||||
export default {
|
||||
components: { GrowArea },
|
||||
components: { FilesDisplay, GrowArea },
|
||||
|
||||
computed: {
|
||||
canCreate() {
|
||||
@ -222,6 +232,7 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
attachedFiles: [],
|
||||
canWrite: null,
|
||||
createRunning: false,
|
||||
fileSize: 0,
|
||||
@ -268,9 +279,9 @@ export default {
|
||||
const meta = new OTSMeta()
|
||||
meta.secret = this.secret
|
||||
|
||||
if (this.$refs.createSecretFiles) {
|
||||
for (const f of [...this.$refs.createSecretFiles.files]) {
|
||||
meta.files.push(f)
|
||||
if (this.attachedFiles.length > 0) {
|
||||
for (const f of this.attachedFiles) {
|
||||
meta.files.push(f.fileObj)
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,6 +328,37 @@ export default {
|
||||
return false
|
||||
},
|
||||
|
||||
deleteFile(fileId) {
|
||||
this.attachedFiles = [...this.attachedFiles].filter(file => file.id !== fileId)
|
||||
this.updateFileMeta()
|
||||
},
|
||||
|
||||
handlePasteFile(file) {
|
||||
this.attachedFiles.push({
|
||||
fileObj: file,
|
||||
id: window.crypto.randomUUID(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
})
|
||||
this.updateFileMeta()
|
||||
},
|
||||
|
||||
handleSelectFiles() {
|
||||
for (const file of this.$refs.createSecretFiles.files) {
|
||||
this.attachedFiles.push({
|
||||
fileObj: file,
|
||||
id: window.crypto.randomUUID(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
})
|
||||
}
|
||||
this.updateFileMeta()
|
||||
|
||||
this.$refs.createSecretFiles.value = ''
|
||||
},
|
||||
|
||||
isAcceptedBy(fileMeta, accept) {
|
||||
if (/^(?:[a-z]+|\*)\/(?:[a-zA-Z0-9.+_-]+|\*)$/.test(accept)) {
|
||||
// That's likely supposed to be a mime-type
|
||||
@ -332,12 +374,12 @@ export default {
|
||||
|
||||
updateFileMeta() {
|
||||
let cumSize = 0
|
||||
for (const f of [...this.$refs.createSecretFiles.files]) {
|
||||
for (const f of this.attachedFiles) {
|
||||
cumSize += f.size
|
||||
}
|
||||
|
||||
this.fileSize = cumSize
|
||||
this.selectedFileMeta = [...this.$refs.createSecretFiles.files].map(file => ({
|
||||
this.selectedFileMeta = this.attachedFiles.map(file => ({
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
}))
|
||||
|
90
src/components/fileDisplay.vue
Normal file
90
src/components/fileDisplay.vue
Normal file
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<div class="list-group mb-3">
|
||||
<a
|
||||
v-for="file in files"
|
||||
:key="file.id"
|
||||
class="cursor-pointer list-group-item list-group-item-action font-monospace d-flex align-items-center"
|
||||
:href="file.url"
|
||||
:download="file.name"
|
||||
@click="handleClick(file)"
|
||||
>
|
||||
<i :class="fasFileType(file.type)" />
|
||||
<span>{{ file.name }}</span>
|
||||
<span class="ms-auto">{{ bytesToHuman(file.size) }}</span>
|
||||
<template v-if="trackDownload">
|
||||
<i
|
||||
v-if="!hasDownloaded[file.id]"
|
||||
class="fas fa-fw fa-download ms-2 text-warning"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-fw fa-circle-check ms-2 text-success"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="canDelete">
|
||||
<i
|
||||
class="fas fa-fw fa-trash ms-2 text-danger"
|
||||
/>
|
||||
</template>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bytesToHuman } from '../helpers'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
hasDownloaded: {},
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
bytesToHuman,
|
||||
|
||||
fasFileType(type) {
|
||||
return [
|
||||
'fas',
|
||||
'fa-fw',
|
||||
'me-2',
|
||||
...[
|
||||
{ icon: ['fa-file-pdf'], match: /application\/pdf/ },
|
||||
{ icon: ['fa-file-audio'], match: /^audio\// },
|
||||
{ icon: ['fa-file-image'], match: /^image\// },
|
||||
{ icon: ['fa-file-lines'], match: /^text\// },
|
||||
{ icon: ['fa-file-video'], match: /^video\// },
|
||||
{ icon: ['fa-file-zipper'], match: /^application\/(gzip|x-tar|zip)$/ },
|
||||
{ icon: ['fa-file-circle-question'], match: /.*/ },
|
||||
].filter(el => el.match.test(type))[0].icon,
|
||||
].join(' ')
|
||||
},
|
||||
|
||||
handleClick(file) {
|
||||
this.$set(this.hasDownloaded, file.id, true)
|
||||
this.$emit('fileClicked', file.id)
|
||||
},
|
||||
},
|
||||
|
||||
name: 'AppFileDisplay',
|
||||
|
||||
props: {
|
||||
canDelete: {
|
||||
default: false,
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
|
||||
files: {
|
||||
required: true,
|
||||
type: Array,
|
||||
},
|
||||
|
||||
trackDownload: {
|
||||
default: true,
|
||||
required: false,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -3,6 +3,7 @@
|
||||
ref="area"
|
||||
v-model="data"
|
||||
style="resize: none;"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@ -33,8 +34,32 @@ export default {
|
||||
getStyle(name) {
|
||||
return parseInt(getComputedStyle(this.$refs.area, null)[name])
|
||||
},
|
||||
|
||||
handlePaste(evt) {
|
||||
if ([...evt.clipboardData.items]
|
||||
.filter(item => item.kind !== 'string')
|
||||
.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
* We have something else than text, prevent using clipboard and
|
||||
* pasting and emit an event containing the file data
|
||||
*/
|
||||
evt.stopPropagation()
|
||||
evt.preventDefault()
|
||||
|
||||
for (const item of evt.clipboardData.items) {
|
||||
if (item.kind === 'string') {
|
||||
continue
|
||||
}
|
||||
|
||||
this.$emit('pasteFile', item.getAsFile())
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
mounted() {
|
||||
this.changeSize()
|
||||
},
|
||||
|
@ -56,28 +56,7 @@
|
||||
</div>
|
||||
<template v-if="files.length > 0">
|
||||
<p v-html="$t('text-attached-files')" />
|
||||
<div class="list-group mb-3">
|
||||
<a
|
||||
v-for="file in files"
|
||||
:key="file.name"
|
||||
class="list-group-item list-group-item-action font-monospace d-flex align-items-center"
|
||||
:href="file.url"
|
||||
:download="file.name"
|
||||
@click="$set(hasDownloaded, file.name, true)"
|
||||
>
|
||||
<i :class="fasFileType(file.type)" />
|
||||
<span>{{ file.name }}</span>
|
||||
<span class="ms-auto">{{ bytesToHuman(file.size) }}</span>
|
||||
<i
|
||||
v-if="!hasDownloaded[file.name]"
|
||||
class="fas fa-fw fa-download ms-2 text-warning"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="fas fa-fw fa-circle-check ms-2 text-success"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<FilesDisplay :files="files" />
|
||||
</template>
|
||||
<p v-html="$t('text-hint-burned')" />
|
||||
</template>
|
||||
@ -88,17 +67,16 @@
|
||||
import appClipboardButton from './clipboard-button.vue'
|
||||
import appCrypto from '../crypto.js'
|
||||
import appQrButton from './qr-button.vue'
|
||||
import { bytesToHuman } from '../helpers'
|
||||
import FilesDisplay from './fileDisplay.vue'
|
||||
import GrowArea from './growarea.vue'
|
||||
import OTSMeta from '../ots-meta'
|
||||
|
||||
export default {
|
||||
components: { GrowArea, appClipboardButton, appQrButton },
|
||||
components: { FilesDisplay, GrowArea, appClipboardButton, appQrButton },
|
||||
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
hasDownloaded: {},
|
||||
popover: null,
|
||||
secret: null,
|
||||
secretContentBlobURL: null,
|
||||
@ -107,25 +85,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
bytesToHuman,
|
||||
|
||||
fasFileType(type) {
|
||||
return [
|
||||
'fas',
|
||||
'fa-fw',
|
||||
'me-2',
|
||||
...[
|
||||
{ icon: ['fa-file-pdf'], match: /application\/pdf/ },
|
||||
{ icon: ['fa-file-audio'], match: /^audio\// },
|
||||
{ icon: ['fa-file-image'], match: /^image\// },
|
||||
{ icon: ['fa-file-lines'], match: /^text\// },
|
||||
{ icon: ['fa-file-video'], match: /^video\// },
|
||||
{ icon: ['fa-file-zipper'], match: /^application\/(gzip|x-tar|zip)$/ },
|
||||
{ icon: ['fa-file-circle-question'], match: /.*/ },
|
||||
].filter(el => el.match.test(type))[0].icon,
|
||||
].join(' ')
|
||||
},
|
||||
|
||||
// requestSecret requests the encrypted secret from the backend
|
||||
requestSecret() {
|
||||
this.secretLoading = true
|
||||
@ -161,7 +120,13 @@ export default {
|
||||
file.arrayBuffer()
|
||||
.then(ab => {
|
||||
const blobURL = window.URL.createObjectURL(new Blob([ab], { type: file.type }))
|
||||
this.files.push({ name: file.name, size: ab.byteLength, type: file.type, url: blobURL })
|
||||
this.files.push({
|
||||
id: window.crypto.randomUUID(),
|
||||
name: file.name,
|
||||
size: ab.byteLength,
|
||||
type: file.type,
|
||||
url: blobURL,
|
||||
})
|
||||
})
|
||||
})
|
||||
this.secretLoading = false
|
||||
|
@ -8,4 +8,8 @@ $web-font-path: '';
|
||||
textarea {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user