summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCarl Schwan <carl@carlschwan.eu>2022-07-11 18:27:04 +0200
committerGitHub <noreply@github.com>2022-07-11 18:27:04 +0200
commit1416d7cdfedfc1373f37b7e409d9276329f971f1 (patch)
tree7939573efc68895ee0bfc0b624eefd4740a9cc38
parent4ecb9e014254adac4210ede462e777fb7fbd14ae (diff)
parent5a36e2474e84be6983aba7d3efa1b29091b90311 (diff)
Merge pull request #1444 from nextcloud/improve/upload/frontend
Improve upload UI
-rw-r--r--lib/Controller/LocalController.php9
-rw-r--r--lib/Controller/MediaApiController.php44
-rw-r--r--package-lock.json11
-rw-r--r--package.json1
-rw-r--r--src/components/Composer/Composer.vue (renamed from src/components/Composer.vue)141
-rw-r--r--src/components/Composer/PreviewGrid.vue72
-rw-r--r--src/components/Composer/PreviewGridItem.vue139
-rw-r--r--src/main.js2
-rw-r--r--src/views/Timeline.vue2
-rw-r--r--src/views/TimelineSinglePost.vue2
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'