From 17735943d5cdde1eb852d36f1c3bb699d23f7eb6 Mon Sep 17 00:00:00 2001 From: shpuld <shp@cock.li> Date: Mon, 14 Jan 2019 19:23:13 +0200 Subject: [PATCH] Add media viewer module and media module component, modify attachment behavior --- src/App.js | 2 + src/App.vue | 1 + src/components/attachment/attachment.js | 19 ++++++-- src/components/attachment/attachment.vue | 47 ++++++++++++-------- src/components/media_modal/media_modal.js | 51 ++++++++++++++++++++++ src/components/media_modal/media_modal.vue | 40 +++++++++++++++++ src/components/status/status.js | 10 ++++- src/components/status/status.vue | 9 +++- src/main.js | 4 +- src/modules/media_viewer.js | 40 +++++++++++++++++ 10 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 src/components/media_modal/media_modal.js create mode 100644 src/components/media_modal/media_modal.vue create mode 100644 src/modules/media_viewer.js diff --git a/src/App.js b/src/App.js index 85df94169..83a61d392 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue' +import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils' @@ -20,6 +21,7 @@ export default { FeaturesPanel, WhoToFollowPanel, ChatPanel, + MediaModal, SideDrawer }, data: () => ({ diff --git a/src/App.vue b/src/App.vue index feadb009b..833608ea5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,6 +41,7 @@ <router-view></router-view> </transition> </div> + <media-modal></media-modal> </div> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> </div> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 97c4f283f..5e672ef28 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -7,7 +7,8 @@ const Attachment = { 'attachment', 'nsfw', 'statusId', - 'size' + 'size', + 'setMedia' ], data () { return { @@ -17,13 +18,17 @@ const Attachment = { loopVideo: this.$store.state.config.loopVideo, showHidden: false, loading: false, - img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img') + img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), + modalOpen: false } }, components: { StillImage }, computed: { + usePlaceHolder () { + return this.size === 'hide' || this.type === 'unknown' + }, type () { return fileTypeService.fileType(this.attachment.mimetype) }, @@ -37,7 +42,7 @@ const Attachment = { return this.size === 'small' }, fullwidth () { - return fileTypeService.fileType(this.attachment.mimetype) === 'html' + return this.type === 'html' || this.type === 'audio' } }, methods: { @@ -62,6 +67,14 @@ const Attachment = { this.showHidden = !this.showHidden } }, + toggleModal (event) { + if (this.type !== 'image' && this.type !== 'video') { + return + } + event.preventDefault() + this.setMedia() + this.$store.dispatch('setCurrent', this.attachment) + }, onVideoDataLoad (e) { if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') { // non-zero if video has audio track diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 5eaa0d1df..1c6b84df3 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,19 +1,29 @@ <template> - <div v-if="size==='hide'"> + <div v-if="usePlaceHolder" @click="toggleModal"> <a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a> </div> - <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty"> + <div + v-else class="attachment" + :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" + v-show="!isEmpty" + @click="toggleModal" + > <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> <img :key="nsfwImage" :src="nsfwImage"/> </a> <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden"> <a href="#" @click.prevent="toggleHidden()">Hide</a> </div> - <a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description"> + <a v-if="type === 'image' && (!hidden || preloadImage)" + class="image-attachment" + :class="{'hidden': hidden && preloadImage}" + :href="attachment.url" target="_blank" + :title="attachment.description" + > <StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/> </a> - <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video> + <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" :src="attachment.url"></video> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio> @@ -40,12 +50,13 @@ .attachment.media-upload-container { flex: 0 0 auto; - max-height: 300px; + max-height: 160px; max-width: 100%; } .placeholder { - margin-right: 0.5em; + margin-right: 8px; + margin-bottom: 4px; } .nsfw-placeholder { @@ -57,16 +68,12 @@ } .small-attachment { - &.image, &.video { - max-width: 35%; - } max-height: 100px; } .attachment { position: relative; - flex: 1 0 30%; - margin: 0.5em 0.7em 0.6em 0.0em; + margin: 0.5em 0.5em 0em 0em; align-self: flex-start; line-height: 0; @@ -86,6 +93,10 @@ line-height: 0; } + .video { + object-fit: cover; + } + &.html { flex-basis: 90%; width: 100%; @@ -107,10 +118,10 @@ .small { max-height: 100px; } + video { - max-height: 500px; + max-height: 160px; height: 100%; - width: 100%; z-index: 0; } @@ -120,7 +131,7 @@ img.media-upload { line-height: 0; - max-height: 300px; + max-height: 160px; max-width: 100%; } @@ -165,21 +176,19 @@ } .still-image { - width: 100%; height: 100%; } .small { img { - max-height: 100px; + max-height: 80px; } } img { - object-fit: contain; - width: 100%; + object-fit: cover; height: 100%; /* If this isn't here, chrome will stretch the images */ - max-height: 500px; + height: 160px; image-orientation: from-image; } } diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js new file mode 100644 index 000000000..916015154 --- /dev/null +++ b/src/components/media_modal/media_modal.js @@ -0,0 +1,51 @@ +import StillImage from '../still-image/still-image.vue' +import fileTypeService from '../../services/file_type/file_type.service.js' + +const MediaModal = { + data () { + return { + loopVideo: this.$store.state.config.loopVideo + } + }, + components: { + StillImage + }, + computed: { + showing () { + return this.$store.state.mediaViewer.activated + }, + currentIndex () { + return this.$store.state.mediaViewer.currentIndex + }, + currentMedia () { + return this.$store.state.mediaViewer.media[this.currentIndex] + }, + type () { + return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null + } + }, + methods: { + hide () { + this.$store.dispatch('closeMediaViewer') + }, + onVideoDataLoad (e) { + if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') { + // non-zero if video has audio track + if (e.srcElement.webkitAudioDecodedByteCount > 0) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } else if (typeof e.srcElement.mozHasAudio !== 'undefined') { + // true if video has audio track + if (e.srcElement.mozHasAudio) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } else if (typeof e.srcElement.audioTracks !== 'undefined') { + if (e.srcElement.audioTracks.length > 0) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } + } + } +} + +export default MediaModal diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue new file mode 100644 index 000000000..6e291ac57 --- /dev/null +++ b/src/components/media_modal/media_modal.vue @@ -0,0 +1,40 @@ +<template> + <div class="modal-view" v-if="showing" @click.prevent="hide"> + <img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img> + <video + class="modal-image" + v-if="type === 'video'" + :src="currentMedia.url" + @click.stop="" + controls autoplay + :loop="loopVideo" + @loadeddata="onVideoDataLoad"> + </video> + </div> +</template> + +<script src="./media_modal.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.modal-view { + z-index: 1005; + position: fixed; + width: 100vw; + height: 100vh; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +.modal-image { + max-width: 90%; + max-height: 90%; + box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); +} +</style> diff --git a/src/components/status/status.js b/src/components/status/status.js index 73d53694e..8058e1bb3 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -35,7 +35,8 @@ const Status = { expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' ? !this.$store.state.instance.collapseMessageWithSubject : !this.$store.state.config.collapseMessageWithSubject, - betterShadow: this.$store.state.interface.browserSupport.cssFilter + betterShadow: this.$store.state.interface.browserSupport.cssFilter, + maxAttachments: 9 } }, computed: { @@ -201,7 +202,8 @@ const Status = { }, attachmentSize () { if ((this.$store.state.config.hideAttachments && !this.inConversation) || - (this.$store.state.config.hideAttachmentsInConv && this.inConversation)) { + (this.$store.state.config.hideAttachmentsInConv && this.inConversation) || + (this.status.attachments.length > this.maxAttachments)) { return 'hide' } else if (this.compact) { return 'small' @@ -291,6 +293,10 @@ const Status = { }, userProfileLink (id, name) { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) + }, + setMedia () { + const attachments = this.status.attachments + return () => this.$store.dispatch('setMedia', attachments) } }, watch: { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index af7568013..f88b5afbd 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -94,7 +94,14 @@ </div> <div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'> - <attachment :size="attachmentSize" :status-id="status.id" :nsfw="nsfwClickthrough" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id"> + <attachment + :size="attachmentSize" + :status-id="status.id" + :nsfw="nsfwClickthrough" + :attachment="attachment" + :set-media="setMedia()" + v-for="attachment in status.attachments" + :key="attachment.id"> </attachment> </div> diff --git a/src/main.js b/src/main.js index f87ef9dac..adeb05504 100644 --- a/src/main.js +++ b/src/main.js @@ -10,6 +10,7 @@ import apiModule from './modules/api.js' import configModule from './modules/config.js' import chatModule from './modules/chat.js' import oauthModule from './modules/oauth.js' +import mediaViewerModule from './modules/media_viewer.js' import VueTimeago from 'vue-timeago' import VueI18n from 'vue-i18n' @@ -62,7 +63,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => { api: apiModule, config: configModule, chat: chatModule, - oauth: oauthModule + oauth: oauthModule, + mediaViewer: mediaViewerModule }, plugins: [persistedState, pushNotifications], strict: false // Socket modifies itself, let's ignore this for now. diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js new file mode 100644 index 000000000..27714bae1 --- /dev/null +++ b/src/modules/media_viewer.js @@ -0,0 +1,40 @@ +import fileTypeService from '../services/file_type/file_type.service.js' + +const mediaViewer = { + state: { + media: [], + currentIndex: 0, + activated: false + }, + mutations: { + setMedia (state, media) { + state.media = media + }, + setCurrent (state, index) { + state.activated = true + state.currentIndex = index + }, + close (state) { + state.activated = false + } + }, + actions: { + setMedia ({ commit }, attachments) { + const media = attachments.filter(attachment => { + const type = fileTypeService.fileType(attachment.mimetype) + return type === 'image' || type === 'video' + }) + commit('setMedia', media) + }, + setCurrent ({ commit, state }, current) { + const index = state.media.indexOf(current) + console.log(index, current) + commit('setCurrent', index || 0) + }, + closeMediaViewer ({ commit }) { + commit('close') + } + } +} + +export default mediaViewer -- GitLab