diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.js | 2 | ||||
| -rw-r--r-- | src/App.vue | 1 | ||||
| -rw-r--r-- | src/assets/nsfw.png | bin | 17071 -> 39603 bytes | |||
| -rw-r--r-- | src/components/attachment/attachment.js | 52 | ||||
| -rw-r--r-- | src/components/attachment/attachment.vue | 102 | ||||
| -rw-r--r-- | src/components/media_modal/media_modal.js | 39 | ||||
| -rw-r--r-- | src/components/media_modal/media_modal.vue | 39 | ||||
| -rw-r--r-- | src/components/settings/settings.js | 13 | ||||
| -rw-r--r-- | src/components/settings/settings.vue | 9 | ||||
| -rw-r--r-- | src/components/status/status.js | 10 | ||||
| -rw-r--r-- | src/components/status/status.vue | 9 | ||||
| -rw-r--r-- | src/main.js | 4 | ||||
| -rw-r--r-- | src/modules/media_viewer.js | 39 |
13 files changed, 223 insertions, 96 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: () => ({ diff --git a/src/App.vue b/src/App.vue index feadb009..833608ea 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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> diff --git a/src/assets/nsfw.png b/src/assets/nsfw.png Binary files differindex 42749033..972bcb4c 100644 --- a/src/assets/nsfw.png +++ b/src/assets/nsfw.png diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 18a03770..d16e5086 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -7,23 +7,25 @@ const Attachment = { 'attachment', 'nsfw', 'statusId', - 'size' + 'size', + 'setMedia' ], data () { return { nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage, hideNsfwLocal: this.$store.state.config.hideNsfw, preloadImage: this.$store.state.config.preloadImage, - loopVideo: this.$store.state.config.loopVideo, - showHidden: false, loading: false, - img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img') + modalOpen: false } }, components: { StillImage }, computed: { + usePlaceHolder () { + return this.size === 'hide' || this.type === 'unknown' + }, referrerpolicy () { return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' }, @@ -31,7 +33,7 @@ const Attachment = { return fileTypeService.fileType(this.attachment.mimetype) }, hidden () { - return this.nsfw && this.hideNsfwLocal && !this.showHidden + return this.nsfw && this.hideNsfwLocal }, isEmpty () { return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown' @@ -40,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: { @@ -49,38 +51,14 @@ const Attachment = { window.open(target.href, '_blank') } }, - toggleHidden () { - if (this.img && !this.preloadImage) { - if (this.img.onload) { - this.img.onload() - } else { - this.loading = true - this.img.src = this.attachment.url - this.img.onload = () => { - this.loading = false - this.showHidden = !this.showHidden - } - } - } else { - this.showHidden = !this.showHidden - } - }, - 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 - } + toggleModal (event) { + if (this.type !== 'image' && this.type !== 'video') { + return } + event.stopPropagation() + event.preventDefault() + this.setMedia() + this.$store.dispatch('setCurrent', this.attachment) } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index b80300b4..ad5120c0 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,19 +1,35 @@ <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"> - <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> - <img :key="nsfwImage" :src="nsfwImage"/> + <div + v-else class="attachment" + :class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" + v-show="!isEmpty" + @click="toggleModal" + > + <a class="image-attachment" v-if="hidden" :href="attachment.url"> + <img :key="nsfwImage" :src="nsfwImage" :class="{'small': isSmall}"/> + <i v-if="type === 'video'" class="play-icon icon-play-circled"></i> </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"> - <StillImage :class="{'small': isSmall}" :referrerpolicy="referrerPolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/> + + <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="referrerPolicy" :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> + <a class="video-container" + v-if="type === 'video' && !hidden" + :class="{'small': isSmall}" + :href="attachment.url" + > + <video class="video" :src="attachment.url"></video> + <i class="play-icon icon-play-circled"></i> + </a> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio> @@ -34,18 +50,23 @@ <style lang="scss"> @import '../../_variables.scss'; +$normalheight: 140px; +$normalwidth: 180px; +$smallheight: 80px; + .attachments { display: flex; flex-wrap: wrap; .attachment.media-upload-container { flex: 0 0 auto; - max-height: 300px; + max-height: $normalheight; max-width: 100%; } .placeholder { - margin-right: 0.5em; + margin-right: 8px; + margin-bottom: 4px; } .nsfw-placeholder { @@ -56,19 +77,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; + flex-grow: 0; border-style: solid; border-width: 1px; @@ -86,6 +100,30 @@ line-height: 0; } + .video-container { + width: auto; + height: $normalheight; + max-width: $normalwidth; + } + + .video { + width: auto; + object-fit: cover; + } + + .play-icon { + position: absolute; + font-size: 64px; + top: calc(50% - 32px); + left: calc(50% - 32px); + color: rgba(255, 255, 255, 0.75); + text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); + } + + .play-icon::before { + margin: 0; + } + &.html { flex-basis: 90%; width: 100%; @@ -105,12 +143,11 @@ } .small { - max-height: 100px; + max-height: $smallheight; } + video { - max-height: 500px; height: 100%; - width: 100%; z-index: 0; } @@ -120,7 +157,7 @@ img.media-upload { line-height: 0; - max-height: 300px; + max-height: $normalheight; max-width: 100%; } @@ -157,29 +194,26 @@ } .image-attachment { - display: flex; - flex: 1; + flex-grow: 0; &.hidden { display: none; } .still-image { - width: 100%; - height: 100%; } .small { img { - max-height: 100px; + height: $smallheight; } } img { - object-fit: contain; - width: 100%; - height: 100%; /* If this isn't here, chrome will stretch the images */ - max-height: 500px; + object-fit: cover; + height: $normalheight; + width: auto; + max-width: $normalwidth; image-orientation: from-image; } } diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js new file mode 100644 index 00000000..7f10589c --- /dev/null +++ b/src/components/media_modal/media_modal.js @@ -0,0 +1,39 @@ +import StillImage from '../still-image/still-image.vue' +import fileTypeService from '../../services/file_type/file_type.service.js' + +const MediaModal = { + 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 + }, + loopVideo () { + return this.$store.state.config.loopVideo + } + }, + created () { + document.addEventListener('keyup', e => { + if (e.keyCode === 27 && this.showing) { // escape + this.hide() + } + }) + }, + methods: { + hide () { + this.$store.dispatch('closeMediaViewer') + } + } +} + +export default MediaModal diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue new file mode 100644 index 00000000..eb8fca53 --- /dev/null +++ b/src/components/media_modal/media_modal.vue @@ -0,0 +1,39 @@ +<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"> + </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> diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index d45ec72d..76b42bab 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -29,7 +29,6 @@ const settings = { notificationVisibilityLocal: user.notificationVisibility, replyVisibilityLocal: user.replyVisibility, loopVideoLocal: user.loopVideo, - loopVideoSilentOnlyLocal: user.loopVideoSilentOnly, muteWordsString: user.muteWords.join('\n'), autoLoadLocal: user.autoLoad, streamingLocal: user.streaming, @@ -57,14 +56,7 @@ const settings = { scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy), stopGifs: user.stopGifs, - webPushNotificationsLocal: user.webPushNotifications, - loopSilentAvailable: - // Firefox - Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || - // Chrome-likes - Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || - // Future spec, still not supported in Nightly 63 as of 08/2018 - Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks') + webPushNotificationsLocal: user.webPushNotifications } }, components: { @@ -120,9 +112,6 @@ const settings = { loopVideoLocal (value) { this.$store.dispatch('setOption', { name: 'loopVideo', value }) }, - loopVideoSilentOnlyLocal (value) { - this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value }) - }, autoLoadLocal (value) { this.$store.dispatch('setOption', { name: 'autoLoad', value }) }, diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 39125009..e84bd3f6 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -131,15 +131,6 @@ <li> <input type="checkbox" id="loopVideo" v-model="loopVideoLocal"> <label for="loopVideo">{{$t('settings.loop_video')}}</label> - <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]"> - <li> - <input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal"> - <label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label> - <div v-if="!loopSilentAvailable" class="unavailable"> - <i class="icon-globe"/>! {{$t('settings.limited_availability')}} - </div> - </li> - </ul> </li> </ul> </div> diff --git a/src/components/status/status.js b/src/components/status/status.js index 7d6acbac..6e82307a 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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: { @@ -189,7 +190,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' @@ -279,6 +281,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: { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 4a1aef8f..d7cab15b 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -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> diff --git a/src/main.js b/src/main.js index f87ef9da..adeb0550 100644 --- a/src/main.js +++ b/src/main.js @@ -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. diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js new file mode 100644 index 00000000..a24b408d --- /dev/null +++ b/src/modules/media_viewer.js @@ -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 |
