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

View File

@ -1,7 +1,8 @@
<template>
<div
<button
v-if="usePlaceholder"
:class="{ 'fullwidth': fullwidth }"
class="Attachment -placeholder button-unstyled"
:class="classNames"
@click="openModal"
>
<a
@ -15,16 +16,16 @@
<FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
</a>
</div>
</button>
<div
v-else
v-show="!isEmpty"
class="attachment"
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
class="Attachment"
:class="classNames"
>
<a
v-if="hidden"
class="image-attachment"
class="image-container"
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
@ -34,7 +35,6 @@
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"
:class="{'small': isSmall}"
>
<FAIcon
v-if="type === 'video'"
@ -42,21 +42,40 @@
icon="play-circle"
/>
</a>
<div
class="attachment-buttons"
v-if="!hidden"
>
<button
v-if="nsfw && hideNsfwLocal && !hidden"
class="button-unstyled hider"
v-if="type === 'flash' && flashLoaded"
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"
>
<FAIcon icon="times" />
</button>
</div>
<a
v-if="type === 'image' && (!hidden || preloadImage)"
class="image-attachment"
:class="{'hidden': hidden && preloadImage }"
class="image-container"
:class="{'-hidden': hidden && preloadImage }"
:href="attachment.url"
target="_blank"
@click="openModal"
@click.stop.prevent="openModal"
>
<StillImage
class="image"
@ -71,24 +90,29 @@
<a
v-if="type === 'video' && !hidden"
class="video-container"
:class="{'small': isSmall}"
:href="allowPlay ? undefined : attachment.url"
@click="openModal"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<VideoAttachment
class="video"
:attachment="attachment"
:controls="allowPlay"
:controls="!useModal"
@play="$emit('play')"
@pause="$emit('pause')"
/>
<FAIcon
v-if="!allowPlay"
v-if="useModal"
class="play-icon"
icon="play-circle"
/>
</a>
<a
v-if="type === 'audio' && !hidden"
class="audio-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<audio
v-if="type === 'audio'"
:src="attachment.url"
@ -98,10 +122,11 @@
@play="$emit('play')"
@pause="$emit('pause')"
/>
</a>
<div
v-if="type === 'html' && attachment.oembed"
class="oembed"
class="oembed-container"
@click.prevent="linkClicked"
>
<div
@ -118,211 +143,23 @@
</div>
</div>
<a
v-if="type === 'flash' && !hidden"
class="flash-container"
:href="attachment.url"
@click.stop.prevent="openModal"
>
<Flash
v-if="type === 'flash'"
class="flash"
ref="flash"
:src="attachment.large_thumb_url || attachment.url"
@playerOpened="setFlashLoaded(true)"
@playerClosed="setFlashLoaded(false)"
/>
</a>
</div>
</template>
<script src="./attachment.js"></script>
<style lang="scss">
@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>
<style src="./attachment.scss" lang="scss"></style>

View File

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

View File

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

View File

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

View File

@ -1,15 +1,17 @@
import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight, sumBy } from 'lodash'
import { sumBy } from 'lodash'
const Gallery = {
props: [
'attachments',
'nsfw',
'setMedia'
'setMedia',
'size'
],
data () {
return {
sizes: {}
sizes: {},
hidingLong: true
}
},
components: { Attachment },
@ -18,26 +20,54 @@ const Gallery = {
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
console.log(this.size)
if (this.size === 'hide') {
return this.attachments.map(item => ({ minimal: true, items: [item] }))
}
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
},
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
attachmentsDimensionalScore () {
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: {
onNaturalSizeLoad (id, size) {
this.$set(this.sizes, id, size)
},
rowStyle (itemsPerRow) {
return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
rowStyle (row) {
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) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
@ -46,6 +76,13 @@ const Gallery = {
getAspectRatio (id) {
const size = this.sizes[id]
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>
<div
class="Gallery"
ref="galleryContainer"
style="width: 100%;"
:class="{ '-long': tooManyAttachments && hidingLong }"
>
<div class="gallery-rows">
<div
v-for="(row, index) in rows"
:key="index"
class="gallery-row"
:style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
:style="rowStyle(row)"
:class="{ '-audio': row.audio, '-minimal': row.minimal }"
>
<div class="gallery-row-inner">
<attachment
v-for="attachment in row"
v-for="attachment in row.items"
class="gallery-item"
:key="attachment.id"
:set-media="setMedia"
:nsfw="nsfw"
:attachment="attachment"
:allow-play="false"
:size="size"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
:style="itemStyle(attachment.id, row.items)"
/>
</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>
<script src='./gallery.js'></script>
@ -31,12 +79,64 @@
<style lang="scss">
@import '../../_variables.scss';
.gallery-row {
.Gallery {
.gallery-rows {
display: flex;
flex-direction: column;
}
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
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 {
position: absolute;
@ -50,7 +150,7 @@
align-content: stretch;
}
.gallery-row-inner .attachment {
.gallery-item {
margin: 0 0.5em 0 0;
flex-grow: 1;
height: 100%;
@ -61,32 +161,5 @@
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>

View File

@ -58,24 +58,6 @@ const StatusContent = {
}
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 () {
return this.mergedConfig.maxThumbnails
},
@ -93,7 +75,7 @@ const StatusContent = {
},
methods: {
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
const attachments = this.status.attachments
return () => this.$store.dispatch('setMedia', attachments)
}
}

View File

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

View File

@ -717,7 +717,11 @@
"nsfw": "NSFW",
"expand": "Expand",
"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": {
"approve": "Approve",