modal for viewing attachments in-tab See merge request pleroma/pleroma-fe!468fix/add-option-to-not-render-background-tabs
commit
5b7b1dfebc
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 34 KiB |
@ -0,0 +1,55 @@
|
|||||||
|
import Attachment from '../attachment/attachment.vue'
|
||||||
|
import { chunk, last, dropRight } from 'lodash'
|
||||||
|
|
||||||
|
const Gallery = {
|
||||||
|
data: () => ({
|
||||||
|
width: 500
|
||||||
|
}),
|
||||||
|
props: [
|
||||||
|
'attachments',
|
||||||
|
'nsfw',
|
||||||
|
'setMedia'
|
||||||
|
],
|
||||||
|
components: { Attachment },
|
||||||
|
mounted () {
|
||||||
|
this.resize()
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
rows () {
|
||||||
|
if (!this.attachments) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const rows = chunk(this.attachments, 3)
|
||||||
|
if (last(rows).length === 1 && rows.length > 1) {
|
||||||
|
// if 1 attachment on last row -> add it to the previous row instead
|
||||||
|
const lastAttachment = last(rows)[0]
|
||||||
|
const allButLastRow = dropRight(rows)
|
||||||
|
last(allButLastRow).push(lastAttachment)
|
||||||
|
return allButLastRow
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
},
|
||||||
|
rowHeight () {
|
||||||
|
return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
|
||||||
|
},
|
||||||
|
useContainFit () {
|
||||||
|
return this.$store.state.config.useContainFit
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resize () {
|
||||||
|
// Quick optimization to make resizing not always trigger state change,
|
||||||
|
// only update attachment size in 10px steps
|
||||||
|
const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10
|
||||||
|
if (this.width !== width) {
|
||||||
|
this.width = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Gallery
|
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="galleryContainer" style="width: 100%;">
|
||||||
|
<div class="gallery-row" v-for="row in rows" :style="rowHeight(row.length)" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }">
|
||||||
|
<attachment
|
||||||
|
v-for="attachment in row"
|
||||||
|
:setMedia="setMedia"
|
||||||
|
:nsfw="nsfw"
|
||||||
|
:attachment="attachment"
|
||||||
|
:allowPlay="false"
|
||||||
|
:key="attachment.id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src='./gallery.js'></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.gallery-row {
|
||||||
|
height: 200px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-content: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
.attachments, .attachment {
|
||||||
|
margin: 0 0.5em 0 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-attachment {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.contain-fit {
|
||||||
|
img, video {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cover-fit {
|
||||||
|
img, video {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,38 @@
|
|||||||
|
import StillImage from '../still-image/still-image.vue'
|
||||||
|
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||||
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
|
|
||||||
|
const MediaModal = {
|
||||||
|
components: {
|
||||||
|
StillImage,
|
||||||
|
VideoAttachment
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
document.addEventListener('keyup', e => {
|
||||||
|
if (e.keyCode === 27 && this.showing) { // escape
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
hide () {
|
||||||
|
this.$store.dispatch('closeMediaViewer')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaModal
|
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal-view" v-if="showing" @click.prevent="hide">
|
||||||
|
<img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img>
|
||||||
|
<VideoAttachment
|
||||||
|
class="modal-image"
|
||||||
|
v-if="type === 'video'"
|
||||||
|
:attachment="currentMedia"
|
||||||
|
:controls="true"
|
||||||
|
@click.stop.native="">
|
||||||
|
</VideoAttachment>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./media_modal.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.modal-view {
|
||||||
|
z-index: 1000;
|
||||||
|
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>
|
@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
const VideoAttachment = {
|
||||||
|
props: ['attachment', 'controls'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
loopVideo: this.$store.state.config.loopVideo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onVideoDataLoad (e) {
|
||||||
|
const target = e.srcElement || e.target
|
||||||
|
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
|
||||||
|
// non-zero if video has audio track
|
||||||
|
if (target.webkitAudioDecodedByteCount > 0) {
|
||||||
|
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||||
|
}
|
||||||
|
} else if (typeof target.mozHasAudio !== 'undefined') {
|
||||||
|
// true if video has audio track
|
||||||
|
if (target.mozHasAudio) {
|
||||||
|
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||||
|
}
|
||||||
|
} else if (typeof target.audioTracks !== 'undefined') {
|
||||||
|
if (target.audioTracks.length > 0) {
|
||||||
|
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VideoAttachment
|
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<video class="video"
|
||||||
|
@loadeddata="onVideoDataLoad"
|
||||||
|
:src="attachment.url"
|
||||||
|
:loop="loopVideo"
|
||||||
|
:controls="controls"
|
||||||
|
playsinline
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./video_attachment.js"></script>
|
@ -0,0 +1,39 @@
|
|||||||
|
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)
|
||||||
|
commit('setCurrent', index || 0)
|
||||||
|
},
|
||||||
|
closeMediaViewer ({ commit }) {
|
||||||
|
commit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default mediaViewer
|
@ -1,27 +1,32 @@
|
|||||||
const fileType = (typeString) => {
|
// TODO this func might as well take the entire file and use its mimetype
|
||||||
let type = 'unknown'
|
// or the entire service could be just mimetype service that only operates
|
||||||
|
// on mimetypes and not files. Currently the naming is confusing.
|
||||||
if (typeString.match(/text\/html/)) {
|
const fileType = mimetype => {
|
||||||
type = 'html'
|
if (mimetype.match(/text\/html/)) {
|
||||||
|
return 'html'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeString.match(/image/)) {
|
if (mimetype.match(/image/)) {
|
||||||
type = 'image'
|
return 'image'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeString.match(/video/)) {
|
if (mimetype.match(/video/)) {
|
||||||
type = 'video'
|
return 'video'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeString.match(/audio/)) {
|
if (mimetype.match(/audio/)) {
|
||||||
type = 'audio'
|
return 'audio'
|
||||||
}
|
}
|
||||||
|
|
||||||
return type
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileMatchesSomeType = (types, file) =>
|
||||||
|
types.some(type => fileType(file.mimetype) === type)
|
||||||
|
|
||||||
const fileTypeService = {
|
const fileTypeService = {
|
||||||
fileType
|
fileType,
|
||||||
|
fileMatchesSomeType
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fileTypeService
|
export default fileTypeService
|
||||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,19 @@
|
|||||||
|
import fileType from 'src/services/file_type/file_type.service.js'
|
||||||
|
|
||||||
|
describe('fileType service', () => {
|
||||||
|
describe('fileMatchesSomeType', () => {
|
||||||
|
it('should be true when file type is one of the listed', () => {
|
||||||
|
const file = { mimetype: 'audio/mpeg' }
|
||||||
|
const types = ['video', 'audio']
|
||||||
|
|
||||||
|
expect(fileType.fileMatchesSomeType(types, file)).to.eql(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be false when files type is not included in type list', () => {
|
||||||
|
const file = { mimetype: 'audio/mpeg' }
|
||||||
|
const types = ['image', 'video']
|
||||||
|
|
||||||
|
expect(fileType.fileMatchesSomeType(types, file)).to.eql(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in new issue