Add media viewer module and media module component, modify attachment behavior

This commit is contained in:
shpuld 2019-01-14 19:23:13 +02:00
parent a51167fa72
commit 17735943d5
10 changed files with 197 additions and 26 deletions

View file

@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_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 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 SideDrawer from './components/side_drawer/side_drawer.vue'
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils' import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
@ -20,6 +21,7 @@ export default {
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
ChatPanel, ChatPanel,
MediaModal,
SideDrawer SideDrawer
}, },
data: () => ({ data: () => ({

View file

@ -41,6 +41,7 @@
<router-view></router-view> <router-view></router-view>
</transition> </transition>
</div> </div>
<media-modal></media-modal>
</div> </div>
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
</div> </div>

View file

@ -7,7 +7,8 @@ const Attachment = {
'attachment', 'attachment',
'nsfw', 'nsfw',
'statusId', 'statusId',
'size' 'size',
'setMedia'
], ],
data () { data () {
return { return {
@ -17,13 +18,17 @@ const Attachment = {
loopVideo: this.$store.state.config.loopVideo, loopVideo: this.$store.state.config.loopVideo,
showHidden: false, showHidden: false,
loading: 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: { components: {
StillImage StillImage
}, },
computed: { computed: {
usePlaceHolder () {
return this.size === 'hide' || this.type === 'unknown'
},
type () { type () {
return fileTypeService.fileType(this.attachment.mimetype) return fileTypeService.fileType(this.attachment.mimetype)
}, },
@ -37,7 +42,7 @@ const Attachment = {
return this.size === 'small' return this.size === 'small'
}, },
fullwidth () { fullwidth () {
return fileTypeService.fileType(this.attachment.mimetype) === 'html' return this.type === 'html' || this.type === 'audio'
} }
}, },
methods: { methods: {
@ -62,6 +67,14 @@ const Attachment = {
this.showHidden = !this.showHidden 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) { onVideoDataLoad (e) {
if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') { if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track // non-zero if video has audio track

View file

@ -1,19 +1,29 @@
<template> <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> <a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a>
</div> </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()"> <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
<img :key="nsfwImage" :src="nsfwImage"/> <img :key="nsfwImage" :src="nsfwImage"/>
</a> </a>
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden"> <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
<a href="#" @click.prevent="toggleHidden()">Hide</a> <a href="#" @click.prevent="toggleHidden()">Hide</a>
</div> </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"/> <StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a> </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> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@ -40,12 +50,13 @@
.attachment.media-upload-container { .attachment.media-upload-container {
flex: 0 0 auto; flex: 0 0 auto;
max-height: 300px; max-height: 160px;
max-width: 100%; max-width: 100%;
} }
.placeholder { .placeholder {
margin-right: 0.5em; margin-right: 8px;
margin-bottom: 4px;
} }
.nsfw-placeholder { .nsfw-placeholder {
@ -57,16 +68,12 @@
} }
.small-attachment { .small-attachment {
&.image, &.video {
max-width: 35%;
}
max-height: 100px; max-height: 100px;
} }
.attachment { .attachment {
position: relative; position: relative;
flex: 1 0 30%; margin: 0.5em 0.5em 0em 0em;
margin: 0.5em 0.7em 0.6em 0.0em;
align-self: flex-start; align-self: flex-start;
line-height: 0; line-height: 0;
@ -86,6 +93,10 @@
line-height: 0; line-height: 0;
} }
.video {
object-fit: cover;
}
&.html { &.html {
flex-basis: 90%; flex-basis: 90%;
width: 100%; width: 100%;
@ -107,10 +118,10 @@
.small { .small {
max-height: 100px; max-height: 100px;
} }
video { video {
max-height: 500px; max-height: 160px;
height: 100%; height: 100%;
width: 100%;
z-index: 0; z-index: 0;
} }
@ -120,7 +131,7 @@
img.media-upload { img.media-upload {
line-height: 0; line-height: 0;
max-height: 300px; max-height: 160px;
max-width: 100%; max-width: 100%;
} }
@ -165,21 +176,19 @@
} }
.still-image { .still-image {
width: 100%;
height: 100%; height: 100%;
} }
.small { .small {
img { img {
max-height: 100px; max-height: 80px;
} }
} }
img { img {
object-fit: contain; object-fit: cover;
width: 100%;
height: 100%; /* If this isn't here, chrome will stretch the images */ height: 100%; /* If this isn't here, chrome will stretch the images */
max-height: 500px; height: 160px;
image-orientation: from-image; image-orientation: from-image;
} }
} }

View file

@ -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

View file

@ -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>

View file

@ -35,7 +35,8 @@ const Status = {
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject ? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject, : !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter,
maxAttachments: 9
} }
}, },
computed: { computed: {
@ -201,7 +202,8 @@ const Status = {
}, },
attachmentSize () { attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) || 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' return 'hide'
} else if (this.compact) { } else if (this.compact) {
return 'small' return 'small'
@ -291,6 +293,10 @@ const Status = {
}, },
userProfileLink (id, name) { userProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
setMedia () {
const attachments = this.status.attachments
return () => this.$store.dispatch('setMedia', attachments)
} }
}, },
watch: { watch: {

View file

@ -94,7 +94,14 @@
</div> </div>
<div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'> <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> </attachment>
</div> </div>

View file

@ -10,6 +10,7 @@ import apiModule from './modules/api.js'
import configModule from './modules/config.js' import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -62,7 +63,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
api: apiModule, api: apiModule,
config: configModule, config: configModule,
chat: chatModule, chat: chatModule,
oauth: oauthModule oauth: oauthModule,
mediaViewer: mediaViewerModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View file

@ -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