Add media viewer module and media module component, modify attachment behavior
This commit is contained in:
parent
a51167fa72
commit
17735943d5
10 changed files with 197 additions and 26 deletions
|
@ -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: () => ({
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
51
src/components/media_modal/media_modal.js
Normal file
51
src/components/media_modal/media_modal.js
Normal 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
|
40
src/components/media_modal/media_modal.vue
Normal file
40
src/components/media_modal/media_modal.vue
Normal 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>
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
40
src/modules/media_viewer.js
Normal file
40
src/modules/media_viewer.js
Normal 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
|
Loading…
Reference in a new issue