forked from AkkomaGang/akkoma-fe
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 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: () => ({
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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'
|
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: {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
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