refactored attachments and gallery. All attachments now are in gallery.

This commit is contained in:
Henry Jameson 2021-06-17 16:29:46 +03:00
parent 9c4957268d
commit e654fead23
10 changed files with 296 additions and 365 deletions

View file

@ -11,7 +11,9 @@ import {
faImage, faImage,
faVideo, faVideo,
faPlayCircle, faPlayCircle,
faTimes faTimes,
faStop,
faSearchPlus
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -20,7 +22,9 @@ library.add(
faImage, faImage,
faVideo, faVideo,
faPlayCircle, faPlayCircle,
faTimes faTimes,
faStop,
faSearchPlus
) )
const Attachment = { const Attachment = {
@ -28,7 +32,6 @@ const Attachment = {
'attachment', 'attachment',
'nsfw', 'nsfw',
'size', 'size',
'allowPlay',
'setMedia', 'setMedia',
'naturalSizeLoad' 'naturalSizeLoad'
], ],
@ -40,7 +43,8 @@ const Attachment = {
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, modalOpen: false,
showHidden: false showHidden: false,
flashLoaded: false
} }
}, },
components: { components: {
@ -49,9 +53,22 @@ const Attachment = {
VideoAttachment VideoAttachment
}, },
computed: { computed: {
classNames () {
return [
{
'-loading': this.loading,
'-nsfw-placeholder': this.hidden
},
'-' + this.type,
`-${this.useContainFit ? 'contain' : 'cover'}-fit`
]
},
usePlaceholder () { usePlaceholder () {
return this.size === 'hide' || this.type === 'unknown' return this.size === 'hide' || this.type === 'unknown'
}, },
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
},
placeholderName () { placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) { if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase() return this.type.toUpperCase()
@ -79,10 +96,6 @@ const Attachment = {
isSmall () { isSmall () {
return this.size === 'small' return this.size === 'small'
}, },
fullwidth () {
if (this.size === 'hide') return false
return this.type === 'html' || this.type === 'audio' || this.type === 'unknown'
},
useModal () { useModal () {
const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio'] const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio']
: this.mergedConfig.playVideosInModal : this.mergedConfig.playVideosInModal
@ -100,12 +113,20 @@ const Attachment = {
}, },
openModal (event) { openModal (event) {
if (this.useModal) { if (this.useModal) {
event.stopPropagation()
event.preventDefault()
this.setMedia() this.setMedia()
this.$store.dispatch('setCurrent', this.attachment) this.$store.dispatch('setCurrent', this.attachment)
} }
}, },
openModalForce (event) {
this.setMedia()
this.$store.dispatch('setCurrent', this.attachment)
},
stopFlash () {
this.$refs.flash.closePlayer()
},
setFlashLoaded (event) {
this.flashLoaded = event
},
toggleHidden (event) { toggleHidden (event) {
if ( if (
(this.mergedConfig.useOneClickNsfw && !this.showHidden) && (this.mergedConfig.useOneClickNsfw && !this.showHidden) &&

View file

@ -1,7 +1,8 @@
<template> <template>
<div <button
v-if="usePlaceholder" v-if="usePlaceholder"
:class="{ 'fullwidth': fullwidth }" class="Attachment -placeholder button-unstyled"
:class="classNames"
@click="openModal" @click="openModal"
> >
<a <a
@ -15,16 +16,16 @@
<FAIcon :icon="placeholderIconClass" /> <FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
</a> </a>
</div> </button>
<div <div
v-else v-else
v-show="!isEmpty" v-show="!isEmpty"
class="attachment" class="Attachment"
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" :class="classNames"
> >
<a <a
v-if="hidden" v-if="hidden"
class="image-attachment" class="image-container"
:href="attachment.url" :href="attachment.url"
:alt="attachment.description" :alt="attachment.description"
:title="attachment.description" :title="attachment.description"
@ -34,7 +35,6 @@
:key="nsfwImage" :key="nsfwImage"
class="nsfw" class="nsfw"
:src="nsfwImage" :src="nsfwImage"
:class="{'small': isSmall}"
> >
<FAIcon <FAIcon
v-if="type === 'video'" v-if="type === 'video'"
@ -42,21 +42,40 @@
icon="play-circle" icon="play-circle"
/> />
</a> </a>
<div
class="attachment-buttons"
v-if="!hidden"
>
<button <button
v-if="nsfw && hideNsfwLocal && !hidden" v-if="type === 'flash' && flashLoaded"
class="button-unstyled hider" class="button-unstyled attachment-button"
@click.prevent="stopFlash"
>
<FAIcon icon="stop" />
</button>
<button
v-if="!useModal"
class="button-unstyled attachment-button"
@click.prevent="openModalForce"
>
<FAIcon icon="search-plus" />
</button>
<button
v-if="nsfw && hideNsfwLocal"
class="button-unstyled attachment-button"
@click.prevent="toggleHidden" @click.prevent="toggleHidden"
> >
<FAIcon icon="times" /> <FAIcon icon="times" />
</button> </button>
</div>
<a <a
v-if="type === 'image' && (!hidden || preloadImage)" v-if="type === 'image' && (!hidden || preloadImage)"
class="image-attachment" class="image-container"
:class="{'hidden': hidden && preloadImage }" :class="{'-hidden': hidden && preloadImage }"
:href="attachment.url" :href="attachment.url"
target="_blank" target="_blank"
@click="openModal" @click.stop.prevent="openModal"
> >
<StillImage <StillImage
class="image" class="image"
@ -71,24 +90,29 @@
<a <a
v-if="type === 'video' && !hidden" v-if="type === 'video' && !hidden"
class="video-container" class="video-container"
:class="{'small': isSmall}" :href="attachment.url"
:href="allowPlay ? undefined : attachment.url" @click.stop.prevent="openModal"
@click="openModal"
> >
<VideoAttachment <VideoAttachment
class="video" class="video"
:attachment="attachment" :attachment="attachment"
:controls="allowPlay" :controls="!useModal"
@play="$emit('play')" @play="$emit('play')"
@pause="$emit('pause')" @pause="$emit('pause')"
/> />
<FAIcon <FAIcon
v-if="!allowPlay" v-if="useModal"
class="play-icon" class="play-icon"
icon="play-circle" icon="play-circle"
/> />
</a> </a>
<a
v-if="type === 'audio' && !hidden"
class="audio-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<audio <audio
v-if="type === 'audio'" v-if="type === 'audio'"
:src="attachment.url" :src="attachment.url"
@ -98,10 +122,11 @@
@play="$emit('play')" @play="$emit('play')"
@pause="$emit('pause')" @pause="$emit('pause')"
/> />
</a>
<div <div
v-if="type === 'html' && attachment.oembed" v-if="type === 'html' && attachment.oembed"
class="oembed" class="oembed-container"
@click.prevent="linkClicked" @click.prevent="linkClicked"
> >
<div <div
@ -118,211 +143,23 @@
</div> </div>
</div> </div>
<a
v-if="type === 'flash' && !hidden"
class="flash-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<Flash <Flash
v-if="type === 'flash'" class="flash"
ref="flash"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
@playerOpened="setFlashLoaded(true)"
@playerClosed="setFlashLoaded(false)"
/> />
</a>
</div> </div>
</template> </template>
<script src="./attachment.js"></script> <script src="./attachment.js"></script>
<style lang="scss"> <style src="./attachment.scss" lang="scss"></style>
@import '../../_variables.scss';
.attachments {
display: flex;
flex-wrap: wrap;
.non-gallery {
max-width: 100%;
}
.placeholder {
display: inline-block;
padding: 0.3em 1em 0.3em 0;
color: $fallback--link;
color: var(--postLink, $fallback--link);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
svg {
color: inherit;
}
}
.nsfw-placeholder {
cursor: pointer;
&.loading {
cursor: progress;
}
}
.attachment {
position: relative;
margin-top: 0.5em;
align-self: flex-start;
line-height: 0;
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden;
}
.non-gallery.attachment {
&.flash,
&.video {
flex: 1 0 40%;
}
.nsfw {
height: 260px;
}
.small {
height: 120px;
flex-grow: 0;
}
.video {
height: 260px;
display: flex;
}
video {
max-height: 100%;
object-fit: contain;
}
}
.fullwidth {
flex-basis: 100%;
}
// fixes small gap below video
&.video {
line-height: 0;
}
.video-container {
display: flex;
max-height: 100%;
}
.video {
width: 100%;
height: 100%;
}
.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%;
display: flex;
}
.hider {
position: absolute;
right: 0;
margin: 10px;
padding: 0;
z-index: 4;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
text-align: center;
width: 2em;
height: 2em;
font-size: 1.25em;
// TODO: theming? hard to theme with unknown background image color
background: rgba(230, 230, 230, 0.7);
.svg-inline--fa {
color: rgba(0, 0, 0, 0.6);
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
}
video {
z-index: 0;
}
audio {
width: 100%;
}
img.media-upload {
line-height: 0;
max-height: 200px;
max-width: 100%;
}
.oembed {
line-height: 1.2em;
flex: 1 0 100%;
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%;
}
.image {
flex: 1;
img {
border: 0px;
border-radius: 5px;
height: 100%;
object-fit: cover;
}
}
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
.image-attachment {
&,
& .image {
width: 100%;
height: 100%;
}
&.hidden {
display: none;
}
.nsfw {
object-fit: cover;
width: 100%;
height: 100%;
}
img {
image-orientation: from-image; // NOTE: only FF supports this
}
}
}
</style>

View file

@ -62,10 +62,6 @@
&.with-media { &.with-media {
width: 100%; width: 100%;
.gallery-row {
overflow: hidden;
}
.status { .status {
width: 100%; width: 100%;
} }

View file

@ -39,12 +39,13 @@ const Flash = {
this.player = 'error' this.player = 'error'
}) })
this.ruffleInstance = player this.ruffleInstance = player
this.$emit('playerOpened')
}) })
}, },
closePlayer () { closePlayer () {
console.log(this.ruffleInstance) this.ruffleInstance && this.ruffleInstance.remove()
this.ruffleInstance.remove()
this.player = false this.player = false
this.$emit('playerClosed')
} }
} }
} }

View file

@ -36,13 +36,6 @@
</p> </p>
</span> </span>
</button> </button>
<button
v-if="player"
class="button-unstyled hider"
@click="closePlayer"
>
<FAIcon icon="stop" />
</button>
</div> </div>
</template> </template>

View file

@ -1,15 +1,17 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight, sumBy } from 'lodash' import { sumBy } from 'lodash'
const Gallery = { const Gallery = {
props: [ props: [
'attachments', 'attachments',
'nsfw', 'nsfw',
'setMedia' 'setMedia',
'size'
], ],
data () { data () {
return { return {
sizes: {} sizes: {},
hidingLong: true
} }
}, },
components: { Attachment }, components: { Attachment },
@ -18,26 +20,54 @@ const Gallery = {
if (!this.attachments) { if (!this.attachments) {
return [] return []
} }
const rows = chunk(this.attachments, 3) console.log(this.size)
if (last(rows).length === 1 && rows.length > 1) { if (this.size === 'hide') {
// if 1 attachment on last row -> add it to the previous row instead return this.attachments.map(item => ({ minimal: true, items: [item] }))
const lastAttachment = last(rows)[0]
const allButLastRow = dropRight(rows)
last(allButLastRow).push(lastAttachment)
return allButLastRow
} }
const rows = this.attachments.reduce((acc, attachment, i) => {
if (attachment.mimetype.includes('audio')) {
return [...acc, { audio: true, items: [attachment] }, { items: [] }]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i - 1
const currentRow = acc[acc.length - 1].items
if (
currentRow.length <= maxPerRow ||
attachmentsRemaining === 1
) {
currentRow.push(attachment)
}
if (currentRow.length === maxPerRow && attachmentsRemaining > 1) {
return [...acc, { items: [] }]
} else {
return acc
}
}, [{ items: [] }]).filter(_ => _.items.length > 0)
return rows return rows
}, },
useContainFit () { attachmentsDimensionalScore () {
return this.$store.getters.mergedConfig.useContainFit return this.rows.reduce((acc, row) => {
return acc + (row.audio ? 0.25 : (1 / (row.items.length + 0.6)))
}, 0)
},
tooManyAttachments () {
if (this.size === 'hide') {
return this.attachments.length > 8
} else {
return this.attachmentsDimensionalScore > 1
}
} }
}, },
methods: { methods: {
onNaturalSizeLoad (id, size) { onNaturalSizeLoad (id, size) {
this.$set(this.sizes, id, size) this.$set(this.sizes, id, size)
}, },
rowStyle (itemsPerRow) { rowStyle (row) {
return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` } if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
}
}, },
itemStyle (id, row) { itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id)) const total = sumBy(row, item => this.getAspectRatio(item.id))
@ -46,6 +76,13 @@ const Gallery = {
getAspectRatio (id) { getAspectRatio (id) {
const size = this.sizes[id] const size = this.sizes[id]
return size ? size.width / size.height : 1 return size ? size.width / size.height : 1
},
toggleHidingLong (event) {
this.hidingLong = event
},
openGallery () {
this.setMedia()
this.$store.dispatch('setCurrent', this.attachments[0])
} }
} }
} }

View file

@ -1,29 +1,77 @@
<template> <template>
<div <div
class="Gallery"
ref="galleryContainer" ref="galleryContainer"
style="width: 100%;" :class="{ '-long': tooManyAttachments && hidingLong }"
> >
<div class="gallery-rows">
<div <div
v-for="(row, index) in rows" v-for="(row, index) in rows"
:key="index" :key="index"
class="gallery-row" class="gallery-row"
:style="rowStyle(row.length)" :style="rowStyle(row)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" :class="{ '-audio': row.audio, '-minimal': row.minimal }"
> >
<div class="gallery-row-inner"> <div class="gallery-row-inner">
<attachment <attachment
v-for="attachment in row" v-for="attachment in row.items"
class="gallery-item"
:key="attachment.id" :key="attachment.id"
:set-media="setMedia" :set-media="setMedia"
:nsfw="nsfw" :nsfw="nsfw"
:attachment="attachment" :attachment="attachment"
:allow-play="false" :allow-play="false"
:size="size"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)" :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)" :style="itemStyle(attachment.id, row.items)"
/> />
</div> </div>
</div> </div>
</div> </div>
<div
v-if="tooManyAttachments"
class="many-attachments"
>
<div class="many-attachments-text">
{{ $t("status.many_attachments", { number: attachments.length })}}
</div>
<div class="many-attachments-buttons">
<span
v-if="!hidingLong"
class="many-attachments-button"
>
<button
class="button-unstyled -link"
@click="toggleHidingLong(true)"
>
{{ $t("status.collapse_attachments") }}
</button>
</span>
<span
v-if="hidingLong"
class="many-attachments-button"
>
<button
class="button-unstyled -link"
@click="toggleHidingLong(false)"
>
{{ $t("status.show_all_attachments") }}
</button>
</span>
<span
class="many-attachments-button"
v-if="hidingLong"
>
<button
class="button-unstyled -link"
@click="openGallery"
>
{{ $t("status.open_gallery") }}
</button>
</span>
</div>
</div>
</div>
</template> </template>
<script src='./gallery.js'></script> <script src='./gallery.js'></script>
@ -31,12 +79,64 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.gallery-row { .Gallery {
.gallery-rows {
display: flex;
flex-direction: column;
}
.gallery-row {
position: relative; position: relative;
height: 0; height: 0;
width: 100%; width: 100%;
flex-grow: 1; flex-grow: 1;
margin-top: 0.5em; margin-top: 0.5em;
}
&.-long {
.gallery-rows {
max-height: 25em;
overflow: hidden;
mask:
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
}
.many-attachments-text {
text-align: center;
line-height: 2;
}
.many-attachments-buttons {
display: flex;
}
.many-attachments-button {
display: flex;
flex: 1;
justify-content: center;
line-height: 2;
button {
padding: 0 2em;
}
}
.gallery-row {
&.-minimal {
height: auto;
.gallery-row-inner {
position: relative;
}
}
}
.gallery-row-inner { .gallery-row-inner {
position: absolute; position: absolute;
@ -50,7 +150,7 @@
align-content: stretch; align-content: stretch;
} }
.gallery-row-inner .attachment { .gallery-item {
margin: 0 0.5em 0 0; margin: 0 0.5em 0 0;
flex-grow: 1; flex-grow: 1;
height: 100%; height: 100%;
@ -61,32 +161,5 @@
margin: 0; margin: 0;
} }
} }
.image-attachment {
width: 100%;
height: 100%;
}
.video-container {
height: 100%;
}
&.contain-fit {
img,
video,
canvas {
object-fit: contain;
height: 100%;
}
}
&.cover-fit {
img,
video,
canvas {
object-fit: cover;
}
}
} }
</style> </style>

View file

@ -58,24 +58,6 @@ const StatusContent = {
} }
return 'normal' return 'normal'
}, },
galleryTypes () {
if (this.attachmentSize === 'hide') {
return []
}
return this.mergedConfig.playVideosInModal
? ['image', 'video']
: ['image']
},
galleryAttachments () {
return this.status.attachments.filter(
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
nonGalleryAttachments () {
return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
maxThumbnails () { maxThumbnails () {
return this.mergedConfig.maxThumbnails return this.mergedConfig.maxThumbnails
}, },
@ -93,7 +75,7 @@ const StatusContent = {
}, },
methods: { methods: {
setMedia () { setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments const attachments = this.status.attachments
return () => this.$store.dispatch('setMedia', attachments) return () => this.$store.dispatch('setMedia', attachments)
} }
} }

View file

@ -11,29 +11,16 @@
<poll :base-poll="status.poll" /> <poll :base-poll="status.poll" />
</div> </div>
<div <gallery
v-if="status.attachments.length !== 0"
class="attachments media-body" class="attachments media-body"
> v-if="status.attachments.length !== 0"
<attachment
v-for="attachment in nonGalleryAttachments"
:key="attachment.id"
class="non-gallery"
:size="attachmentSize"
:nsfw="nsfwClickthrough" :nsfw="nsfwClickthrough"
:attachment="attachment" :attachments="status.attachments"
:allow-play="true"
:set-media="setMedia()" :set-media="setMedia()"
:size="attachmentSize"
@play="$emit('mediaplay', attachment.id)" @play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)" @pause="$emit('mediapause', attachment.id)"
/> />
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
:set-media="setMedia()"
/>
</div>
<div <div
v-if="status.card && !noHeading" v-if="status.card && !noHeading"

View file

@ -717,7 +717,11 @@
"nsfw": "NSFW", "nsfw": "NSFW",
"expand": "Expand", "expand": "Expand",
"you": "(You)", "you": "(You)",
"plus_more": "+{number} more" "plus_more": "+{number} more",
"many_attachments": "Post has {number} attachment(s)",
"collapse_attachments": "Collapse attachments",
"show_all_attachments": "Show all attachments",
"open_gallery": "Open gallery"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",