diff options
author | Carl Schwan <carl@carlschwan.eu> | 2022-07-11 18:27:04 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-11 18:27:04 +0200 |
commit | 1416d7cdfedfc1373f37b7e409d9276329f971f1 (patch) | |
tree | 7939573efc68895ee0bfc0b624eefd4740a9cc38 | |
parent | 4ecb9e014254adac4210ede462e777fb7fbd14ae (diff) | |
parent | 5a36e2474e84be6983aba7d3efa1b29091b90311 (diff) |
Merge pull request #1444 from nextcloud/improve/upload/frontend
Improve upload UI
-rw-r--r-- | lib/Controller/LocalController.php | 9 | ||||
-rw-r--r-- | lib/Controller/MediaApiController.php | 44 | ||||
-rw-r--r-- | package-lock.json | 11 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/components/Composer/Composer.vue (renamed from src/components/Composer.vue) | 141 | ||||
-rw-r--r-- | src/components/Composer/PreviewGrid.vue | 72 | ||||
-rw-r--r-- | src/components/Composer/PreviewGridItem.vue | 139 | ||||
-rw-r--r-- | src/main.js | 2 | ||||
-rw-r--r-- | src/views/Timeline.vue | 2 | ||||
-rw-r--r-- | src/views/TimelineSinglePost.vue | 2 |
10 files changed, 295 insertions, 128 deletions
diff --git a/lib/Controller/LocalController.php b/lib/Controller/LocalController.php index 20c64b79..12098d37 100644 --- a/lib/Controller/LocalController.php +++ b/lib/Controller/LocalController.php @@ -106,6 +106,15 @@ class LocalController extends Controller { $this->miscService = $miscService; } + /** + * Upload file + * + * @NoAdminRequired + */ + public function uploadAttachement(): DataResponse { + + } + /** * Create a new post. diff --git a/lib/Controller/MediaApiController.php b/lib/Controller/MediaApiController.php new file mode 100644 index 00000000..82cd0532 --- /dev/null +++ b/lib/Controller/MediaApiController.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +// SPDX-FileCopyrightText: Carl Schwan <carl@carlschwan.eu> +// SPDX-License-Identifier: AGPL-3.0-or-later + +namespace OCA\Social\Controller; + +use OCP\AppFramework\Controller; +use OCP\Files\IMimeTypeDetector; + +class MediaApiController extends Controller { + public const IMAGE_MIME_TYPES = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'image/gif', + 'image/x-xbitmap', + 'image/x-ms-bmp', + 'image/bmp', + 'image/svg+xml', + 'image/webp', + ]; + + private IMimeTypeDetector $mimeTypeDetector; + + /** + * Creates an attachment to be used with a new status. + * + * @NoAdminRequired + */ + public function uploadMedia(): DataResponse { + // TODO + return DataResponse([ + 'id' => 1, + 'url' => '', + 'preview_url' => '', + 'remote_url' => null, + 'text_url' => '', + 'description' => '', + ]); + } +} diff --git a/package-lock.json b/package-lock.json index a50efd7b..b3be8b25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "vue-click-outside": "^1.0.7", "vue-contenteditable-directive": "^1.2.0", "vue-infinite-loading": "^2.4.4", - "vue-masonry-css": "^1.0.3", "vue-material-design-icons": "^5.0.0", "vue-router": "^3.5.3", "vue-tribute": "^1.0.6", @@ -27541,11 +27540,6 @@ } } }, - "node_modules/vue-masonry-css": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vue-masonry-css/-/vue-masonry-css-1.0.3.tgz", - "integrity": "sha512-viecHQiHVLez7HlYUQsv1wJb2MT/RDSzkDp6m3In41vPrk6OsBmT2qRE8LZqYIA4daIwrnx/Xm8h4fjOpuE3hw==" - }, "node_modules/vue-material-design-icons": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.0.0.tgz", @@ -49827,11 +49821,6 @@ "vue-style-loader": "^4.1.0" } }, - "vue-masonry-css": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vue-masonry-css/-/vue-masonry-css-1.0.3.tgz", - "integrity": "sha512-viecHQiHVLez7HlYUQsv1wJb2MT/RDSzkDp6m3In41vPrk6OsBmT2qRE8LZqYIA4daIwrnx/Xm8h4fjOpuE3hw==" - }, "vue-material-design-icons": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/vue-material-design-icons/-/vue-material-design-icons-5.0.0.tgz", diff --git a/package.json b/package.json index 8cc6e702..34050246 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "vue-click-outside": "^1.0.7", "vue-contenteditable-directive": "^1.2.0", "vue-infinite-loading": "^2.4.4", - "vue-masonry-css": "^1.0.3", "vue-material-design-icons": "^5.0.0", "vue-router": "^3.5.3", "vue-tribute": "^1.0.6", diff --git a/src/components/Composer.vue b/src/components/Composer/Composer.vue index d10df3b8..7d5685b3 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer/Composer.vue @@ -1,5 +1,6 @@ <!-- - @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> + - @copyright Copyright (c) 2022 Carl Schwan <carl@carlschwan.eu> - - @author Julius Härtl <jus@bitgrid.net> - @@ -24,12 +25,12 @@ <div class="new-post" data-id=""> <input id="file-upload" ref="fileUploadInput" + @change="handleFileChange($event)" multiple type="file" tabindex="-1" aria-hidden="true" - class="hidden-visually" - @change="handleFileInput"> + class="hidden-visually"> <div class="new-post-author"> <avatar :user="currentUser.uid" :display-name="currentUser.displayName" :disable-tooltip="true" :size="32" /> @@ -61,14 +62,7 @@ @tribute-replaced="updatePostFromTribute" /> </vue-tribute> - <masonry> - <div v-for="(item, index) in miniatures" :key="index" ref="miniatures"> - <img alt="" :src="item.img" :usemap="'#map' + index"> - <map :name="'map' + index"> - <area shape="circle" :coords="getImageMapCoords(index)" @click="removeAttachment(index)"> - </map> - </div> - </masonry> + <PreviewGrid :uploading="false" :uploadProgress="0.4" :miniatures="previewUrls" /> <div class="options"> <Button type="tertiary" @@ -82,7 +76,7 @@ <div class="new-post-form__emoji-picker"> <EmojiPicker ref="emojiPicker" :search="search" :close-on-select="false" - :container="containerElement" + :container="container" @select="insert"> <Button type="tertiary" :aria-haspopup="true" @@ -106,7 +100,7 @@ </div> <div class="emptySpace" /> - <Button :value="currentVisibilityPostLabel" :disabled="post.length < 1 || post==='<br>'" type="primary" + <Button :value="currentVisibilityPostLabel" :disabled="!canPost" type="primary" @click.prevent="createPost"> <template #icon> <Send title="" :size="22" decorative /> @@ -129,11 +123,12 @@ import PopoverMenu from '@nextcloud/vue/dist/Components/PopoverMenu' import EmojiPicker from '@nextcloud/vue/dist/Components/EmojiPicker' import VueTribute from 'vue-tribute' import he from 'he' -import CurrentUserMixin from './../mixins/currentUserMixin' -import FocusOnCreate from '../directives/focusOnCreate' +import CurrentUserMixin from '../../mixins/currentUserMixin' +import FocusOnCreate from '../../directives/focusOnCreate' import axios from '@nextcloud/axios' -import ActorAvatar from './ActorAvatar.vue' +import ActorAvatar from '../ActorAvatar.vue' import { generateUrl } from '@nextcloud/router' +import PreviewGrid from './PreviewGrid' export default { name: 'Composer', @@ -147,6 +142,7 @@ export default { EmoticonOutline, Button, Send, + PreviewGrid, }, directives: { FocusOnCreate, @@ -160,6 +156,7 @@ export default { post: '', miniatures: [], // miniatures of images stored in postAttachments postAttachments: [], // The toot's attachments + previewUrls: [], canType: true, search: '', replyTo: null, @@ -268,9 +265,6 @@ export default { return t('social', 'Post to mentioned users') } }, - containerElement() { - return document.querySelector('#content-vue') - }, currentVisibilityIconClass() { return this.visibilityIconClass(this.type) }, @@ -366,6 +360,12 @@ export default { containerElement() { return document.querySelector(this.container) }, + canPost() { + if (this.previewUrls.length > 0) { + return true; + } + return this.post.length !== 0 && this.post !== '<br>' + } }, mounted() { this.$root.$on('composer-reply', (data) => { @@ -377,98 +377,16 @@ export default { clickImportInput() { this.$refs.fileUploadInput.click() }, - handleFileInput() { - // TODO: handle (or prevent) mulitples/ files - let self = this - let file = this.$refs.fileUploadInput.files[0] - let reader = new FileReader() - - // Called when selected file is completly loaded to draw a miniature - reader.onload = function(e) { - let canvas = document.createElement('canvas') - let ctx = canvas.getContext('2d') - let width = 265 - let height = 180 - let img = new Image() - - // Called when img.src is set below - img.onload = function() { - - // scale image for miniature - let imgWidth = this.width - let imgHeight = this.height - imgHeight = Math.floor(imgHeight * (width / imgWidth)) - imgWidth = width - if (imgHeight > height) { - imgWidth = Math.floor(imgWidth * (height / imgHeight)) - imgHeight = height - } - canvas.width = imgWidth - canvas.height = imgHeight - ctx.drawImage(this, 0, 0, imgWidth, imgHeight) - - // Draw a border - ctx.beginPath() - ctx.fillStyle = 'black' - ctx.lineWidth = 1 - ctx.moveTo(0, 0) - ctx.lineTo(imgWidth, 0) - ctx.lineTo(imgWidth, imgHeight) - ctx.lineTo(0, imgHeight) - ctx.lineTo(0, 0) - ctx.stroke() - - // Create a close badge in the upper-right corner - ctx.beginPath() - ctx.arc(imgWidth - 20, 20, 10, 0, 2 * Math.PI) - ctx.fillStyle = 'white' - ctx.fill() - ctx.lineWidth = 2 - ctx.StrokeStyle = 'darkgray' - ctx.stroke() - ctx.beginPath() - ctx.moveTo(imgWidth - (20 + 5), 20 - 5) - ctx.lineTo(imgWidth - (20 - 5), 20 + 5) - ctx.stroke() - ctx.moveTo(imgWidth - (20 - 5), 20 - 5) - ctx.lineTo(imgWidth - (20 + 5), 20 + 5) - ctx.stroke() - - // Add filename to generic icon for non image document - if (!e.target.result.startsWith('data:image')) { - ctx.fillStyle = 'black' - ctx.font = '12px Arial' - ctx.fillText(file.name, 30, imgHeight - 20) - } - - // Save miniature - self.miniatures.push({ - 'img': canvas.toDataURL(), - 'coords': String(imgWidth - 20) + ',20,10' - }) - - } - - // Save document - self.postAttachments.push(e.target.result) - - // Draw a generic icon when document is not an image - if (e.target.result.startsWith('data:image')) { - img.src = e.target.result - } else { - img.src = generateUrl('svg/core/filetypes/x-office-document?color=d8d8d8') - } - } - - // Start reading selected file - reader.readAsDataURL(file) + handleFileChange(event) { + const previewUrl = URL.createObjectURL(event.target.files[0]) + this.previewUrls.push({ + description: '', + url: previewUrl, + result: event.target.files[0], + }) }, removeAttachment(idx) { - this.postAttachments.splice(idx, 1) - this.miniatures.splice(idx, 1) - }, - getImageMapCoords(idx) { - return this.miniatures[idx].coords + this.previewUrls.splice(idx, 1) }, insert(emoji) { if (typeof emoji === 'object') { @@ -537,7 +455,7 @@ export default { to: to, hashtags: hashtags, type: this.type, - attachments: this.postAttachments + attachments: this.previewUrls.map(preview => preview.result), // TODO send the summary and other props too } if (this.replyTo) { @@ -587,8 +505,7 @@ export default { this.replyTo = null this.post = '' this.$refs.composerInput.innerText = this.post - this.postAttachments = [] - this.miniatures = [] + this.previewUrls = [] this.$store.dispatch('refreshTimeline') }) @@ -648,7 +565,7 @@ export default { } .reply-to { - background-image: url(../../img/reply.svg); + background-image: url(../../../img/reply.svg); background-position: 5px 5px; background-repeat: no-repeat; margin-left: 39px; diff --git a/src/components/Composer/PreviewGrid.vue b/src/components/Composer/PreviewGrid.vue new file mode 100644 index 00000000..77e134c9 --- /dev/null +++ b/src/components/Composer/PreviewGrid.vue @@ -0,0 +1,72 @@ +<!-- +SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu> +SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="upload-form"> + <div class="upload-progress" v-if="false"> + <div class="upload-progress__icon"> + <FileUpload :size="32" /> + </div> + + <div class="upload-progress__message"> + {{ t('social', 'Uploading...') }} + + <div class="upload-progress__backdrop"> + <div class="upload-progress__tracker" :style="`width: ${uploadProgress * 100}%`" /> + </div> + </div> + </div> + <div class="preview-grid"> + <PreviewGridItem v-for="(item, index) in miniatures" :key="index" :preview="item" :index="index" @delete="deletePreview" /> + </div> + </div> +</template> + +<script> +import PreviewGridItem from './PreviewGridItem' +import FileUpload from 'vue-material-design-icons/FileUpload' + +export default { + name: 'PreviewGrid', + components: { + PreviewGridItem, + FileUpload, + }, + props: { + uploadProgress: { + type: Number, + required: true, + }, + uploading: { + type: Boolean, + required: true, + }, + miniatures: { + type: Array, + required: true, + }, + }, + methods: { + deletePreview(index) { + console.debug("rjeoijreo") + this.miniatures.splice(index, 1) + } + }, +} +</script> + +<style scoped lang="scss"> +.upload-progress { + display: flex; +} + +.preview-grid { + display: flex; + flex-wrap: wrap; + flex-direction: row; + margin-left: -5px; + margin-right: -5px; +} +</style> diff --git a/src/components/Composer/PreviewGridItem.vue b/src/components/Composer/PreviewGridItem.vue new file mode 100644 index 00000000..94233059 --- /dev/null +++ b/src/components/Composer/PreviewGridItem.vue @@ -0,0 +1,139 @@ +<template> + <div class="preview-item-wrapper"> + <div class="preview-item" :style="backgroundStyle"> + <div class="preview-item__actions"> + <Button type="tertiary-no-background" @click="$emit('delete', index)"> + <template #icon> + <Close :size="16" fillColor="white" /> + </template> + <span>{{ t('social', 'Delete') }}</span> + </Button> + <Button type="tertiary-no-background" @click="showModal"> + <template #icon> + <Edit :size="16" fillColor="white" /> + </template> + <span>{{ t('social', 'Edit') }}</span> + </Button> + </div> + + <div class="description-warning" v-if="preview.description.length === 0"> + {{ t('social', 'No description added') }} + </div> + + <Modal v-if="modal" @close="closeModal" size="small"> + <div class="modal__content"> + <label :for="`image-description-${index}`"> + {{ t('social', 'Describe for the visually impaired') }} + </label> + <textarea :id="`image-description-${index}`" v-model="preview.description"> + </textarea> + <Button type="primary" @click="closeModal">{{ t('social', 'Close') }}</Button> + </div> + </Modal> + </div> + </div> +</template> + +<script> +import Close from 'vue-material-design-icons/Close' +import Edit from 'vue-material-design-icons/Pencil' +import Button from '@nextcloud/vue/dist/Components/Button' +import Modal from '@nextcloud/vue/dist/Components/Modal' + +export default { + name: 'PreviewGridItem', + components: { + Close, + Edit, + Button, + Modal, + }, + data() { + return { + modal: false, + } + }, + methods: { + showModal() { + this.modal = true + }, + closeModal() { + this.modal = false + } + }, + props: { + preview: { + type: Object, + required: true, + }, + index: { + type: Number, + required: true, + }, + }, + computed: { + backgroundStyle() { + return { + backgroundImage: `url("${this.preview.url}")`, + } + }, + }, +} +</script> + +<style scoped lang="scss"> +.preview-item-wrapper { + flex: 1 1 0; + min-width: 40%; + margin: 5px; +} + +.preview-item { + border-radius: 4px; + background-color: #000; + background-position: 50%; + background-size: cover; + background-repeat: no-repeat; + height: 140px; + width: 100%; + overflow: hidden; + position: relative; + + .button-vue--vue-tertiary-no-background { + color: white !important; + } + + &__actions { + background: linear-gradient(180deg,rgba(0,0,0,.8),rgba(0,0,0,.35) 80%,transparent); + display: flex; + align-items: flex-start; + justify-content: space-between; + + .button-vue__text { + color: white !important; + } + } + + .description-warning { + position: absolute; + z-index: 2; + bottom: 0; + left: 0; + right: 0; + box-sizing: border-box; + background: linear-gradient(0deg,rgba(0,0,0,.8),rgba(0,0,0,.35) 80%,transparent); + color: white; + padding: 10px; + } +} + +.modal__content { + padding: 20px; +} + +textarea { + width: 100%; + height: 100px; + margin-bottom: 20px; +} +</style> diff --git a/src/main.js b/src/main.js index da4d28d5..f850dd3a 100644 --- a/src/main.js +++ b/src/main.js @@ -30,7 +30,6 @@ import vuetwemoji from 'vue-twemoji' import contenteditableDirective from 'vue-contenteditable-directive' import ClickOutside from 'vue-click-outside' import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip' -import VueMasonry from 'vue-masonry-css' sync(store, router) @@ -57,7 +56,6 @@ Vue.use(vuetwemoji, { className: 'emoji', // custom className for image output size: 'twemoji' // image size }) -Vue.use(VueMasonry) /* eslint-disable-next-line no-new */ new Vue({ diff --git a/src/views/Timeline.vue b/src/views/Timeline.vue index 08fc3928..d06599fb 100644 --- a/src/views/Timeline.vue +++ b/src/views/Timeline.vue @@ -92,7 +92,7 @@ </style> <script> -import Composer from './../components/Composer.vue' +import Composer from './../components/Composer/Composer.vue' import CurrentUserMixin from './../mixins/currentUserMixin' import follow from './../mixins/follow' import TimelineList from './../components/TimelineList.vue' diff --git a/src/views/TimelineSinglePost.vue b/src/views/TimelineSinglePost.vue index cc9ed9d8..c62fad09 100644 --- a/src/views/TimelineSinglePost.vue +++ b/src/views/TimelineSinglePost.vue @@ -21,7 +21,7 @@ </style> <script> -import Composer from '../components/Composer.vue' +import Composer from '../components/Composer/Composer.vue' import ProfileInfo from '../components/ProfileInfo.vue' import TimelineEntry from '../components/TimelineEntry.vue' import TimelineList from '../components/TimelineList.vue' |