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) => {
|
||||
let type = 'unknown'
|
||||
|
||||
if (typeString.match(/text\/html/)) {
|
||||
type = 'html'
|
||||
// TODO this func might as well take the entire file and use its mimetype
|
||||
// or the entire service could be just mimetype service that only operates
|
||||
// on mimetypes and not files. Currently the naming is confusing.
|
||||
const fileType = mimetype => {
|
||||
if (mimetype.match(/text\/html/)) {
|
||||
return 'html'
|
||||
}
|
||||
|
||||
if (typeString.match(/image/)) {
|
||||
type = 'image'
|
||||
if (mimetype.match(/image/)) {
|
||||
return 'image'
|
||||
}
|
||||
|
||||
if (typeString.match(/video/)) {
|
||||
type = 'video'
|
||||
if (mimetype.match(/video/)) {
|
||||
return 'video'
|
||||
}
|
||||
|
||||
if (typeString.match(/audio/)) {
|
||||
type = 'audio'
|
||||
if (mimetype.match(/audio/)) {
|
||||
return 'audio'
|
||||
}
|
||||
|
||||
return type
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
const fileMatchesSomeType = (types, file) =>
|
||||
types.some(type => fileType(file.mimetype) === type)
|
||||
|
||||
const fileTypeService = {
|
||||
fileType
|
||||
fileType,
|
||||
fileMatchesSomeType
|
||||
}
|
||||
|
||||
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