feat: Allow Media to be shown in a grid (as an option) (#747)

* Allow Media to be shown in a grid

* Bring back the sidebar for ungrouped images
This commit is contained in:
sgenoud 2018-12-08 18:42:38 +01:00 committed by Nolan Lawson
parent ab548a0a5d
commit 530ad6b35c
11 changed files with 219 additions and 97 deletions

View file

@ -1,7 +1,10 @@
<video
class="autoplay-video {className || ''}"
<div class="autoplay-wrapper {$groupedImages ? 'fixed-size': ''}"
style="width: {width}px; height: {height}px;"
>
<video
class="autoplay-video {$groupedImages ? 'fixed-size': ''}"
aria-label={ariaLabel || ''}
style="background-image: url({poster});"
style="{focusStyle} background-image: url({poster}); "
{poster}
{width}
{height}
@ -11,11 +14,54 @@
loop
webkit-playsinline
playsinline
/>
/>
</div>
<style>
.autoplay-wrapper {
position: relative;
margin: 0;
padding: 0;
}
.autoplay-video {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
background-size: cover;
display: flex;
}
.fixed-size {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
.fixed-size {
overflow: hidden;
width: 100%;
height: 100%;
}
</style>
<script>
import { store } from '../_store/store'
const coordsToPercent = coord => (1 + coord) / 2 * 100
export default {
data: () => ({
focus: void 0
}),
store: () => store,
computed: {
focusStyle: ({ focus }) => {
// Here we do a pure css version instead of using
// https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
if (!focus) return 'background-position: center;'
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
}
}
}
</script>

View file

@ -6,6 +6,7 @@
<LazyImage
className={computedClass}
ariaHidden="true"
forceSize=true
alt=""
src={account.avatar}
{width}

View file

@ -1,24 +1,40 @@
<div class="lazy-image" style={computedStyle} >
<div class="lazy-image {fillFixSize ? 'fixed-size': ''}" style={computedStyle} >
<img
class={className}
aria-hidden={ariaHidden}
{alt}
{title}
{width}
{height}
src={displaySrc}
style={focusStyle}
ref:node
/>
</div>
<style>
.lazy-image {
margin: 0;
padding: 0;
overflow: hidden;
display: flex;
}
.fixed-size img {
width: 100%;
height: 100%;
}
.fixed-size {
position: absolute;
width: 100%;
height: 100%;
}
</style>
<script>
import { decodeImage } from '../_utils/decodeImage'
const coordsToPercent = coord => (1 + coord) / 2 * 100
export default {
export default {
async oncreate () {
try {
await decodeImage(this.refs.node)
@ -28,23 +44,30 @@
},
data: () => ({
error: false,
forceSize: false,
fallback: void 0,
focus: void 0,
background: '',
width: void 0,
height: void 0,
className: '',
ariaHidden: false,
alt: '',
title: ''
}),
computed: {
computedStyle: ({ width, height, background }) => {
computedStyle: ({ background }) => {
return [
width && `width: ${width}px;`,
height && `height: ${height}px;`,
background && `background: ${background};`
].filter(Boolean).join('')
},
focusStyle: ({ focus }) => {
// Here we do a pure css version instead of using
// https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point
if (!focus) return 'background-position: center;'
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
},
fillFixSize: ({ forceSize, $groupedImages }) => $groupedImages && !forceSize,
displaySrc: ({ error, src, fallback }) => ((error && fallback) || src)
}
}

View file

@ -1,15 +1,16 @@
<div class="non-autoplay-gifv" style="width: {width}px; height: {height}px;"
<div class="non-autoplay-gifv {$groupedImages ? 'fixed-size': ''}"
style="width: {width}px; height: {height}px;"
on:mouseover="onMouseOver(event)"
ref:node
>
{#if playing}
<AutoplayVideo
className={class}
ariaLabel={label}
{poster}
{src}
{width}
{height}
{focus}
/>
{:else}
<LazyImage
@ -20,7 +21,7 @@
{width}
{height}
background="var(--loading-bg)"
className={class}
{focus}
/>
{/if}
<PlayVideoIcon className={playing ? 'hidden' : ''}/>
@ -28,8 +29,20 @@
<style>
.non-autoplay-gifv {
cursor: zoom-in;
display: flex;
position: relative;
margin: 0;
padding: 0;
}
.fixed-size {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
:global(.non-autoplay-gifv .play-video-icon) {
transition: opacity 0.2s linear;
}
@ -51,7 +64,8 @@
mouseover
},
data: () => ({
oneTransparentPixel: ONE_TRANSPARENT_PIXEL
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
focus: void 0
}),
components: {
PlayVideoIcon,

View file

@ -6,12 +6,18 @@
className="image-modal-dialog"
>
{#if type === 'gifv'}
<AutoplayVideo
ariaLabel="Animated GIF: {description || ''}"
{poster}
{src}
<video
class="autoplay-video"
aria-label="Animated GIF: {description || ''}"
style="background-image: url({poster}); "
{width}
{height}
{src}
autoplay
muted
loop
webkit-playsinline
playsinline
/>
{:else}
<img
@ -30,18 +36,23 @@
max-height: calc(100% - 20px);
overflow: hidden;
}
.autoplay-video {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
</style>
<script>
import ModalDialog from './ModalDialog.html'
import AutoplayVideo from '../../AutoplayVideo.html'
import { show } from '../helpers/showDialog'
import { oncreate } from '../helpers/onCreateDialog'
export default {
oncreate,
components: {
ModalDialog,
AutoplayVideo
ModalDialog
},
methods: {
show

View file

@ -1,6 +1,6 @@
{#if type === 'video'}
<button type="button"
class="play-video-button"
class="play-video-button {$groupedImages ? 'fixed-size': ''}"
aria-label="Play video: {description}"
delegate-key={delegateKey}
style="width: {inlineWidth}px; height: {inlineHeight}px;">
@ -13,26 +13,25 @@
width={inlineWidth}
height={inlineHeight}
background="var(--loading-bg)"
className={noNativeWidthHeight ? 'no-native-width-height' : ''}
{focus}
/>
</button>
{:else}
<button type="button"
class="show-image-button"
class="show-image-button {$groupedImages ? 'fixed-size': ''}"
aria-label="Show image: {description}"
title={description}
delegate-key={delegateKey}
on:mouseover="set({mouseover: event})"
style="width: {inlineWidth}px; height: {inlineHeight}px;"
>
style="width: {inlineWidth}px; height: {inlineHeight}px;">
{#if type === 'gifv' && $autoplayGifs}
<AutoplayVideo
className={noNativeWidthHeight ? 'no-native-width-height' : ''}
ariaLabel="Animated GIF: {description}"
poster={previewUrl}
src={url}
width={inlineWidth}
height={inlineHeight}
{focus}
/>
{:elseif type === 'gifv' && !$autoplayGifs}
<NonAutoplayGifv
@ -44,6 +43,7 @@
width={inlineWidth}
height={inlineHeight}
playing={mouseover}
{focus}
/>
{:else}
<LazyImage
@ -54,46 +54,27 @@
width={inlineWidth}
height={inlineHeight}
background="var(--loading-bg)"
className={noNativeWidthHeight ? 'no-native-width-height' : ''}
{focus}
/>
{/if}
</button>
{/if}
<style>
:global(.status-media video, .status-media img) {
object-fit: cover;
}
:global(.no-native-width-height) {
background-color: var(--mask-bg);
}
.play-video-button {
margin: 0;
.play-video-button, .show-image-button {
margin: auto;
padding: 0;
border-radius: 0;
border: none;
background: none;
position: relative;
}
.show-image-button {
margin: 0;
padding: 0;
border-radius: 0;
border: none;
background: none;
cursor: zoom-in;
}
:global(.status-media video, .status-media img, .status-media .lazy-image,
.status-media .show-image-button, .status-media .non-autoplay-gifv,
.status-media .play-video-button) {
max-width: calc(100vw - 40px);
}
@media (max-width: 767px) {
:global(.status-media video, .status-media img, .status-media .lazy-image,
.status-media .show-image-button, .status-media .non-autoplay-gifv,
.status-media .play-video-button) {
max-width: calc(100vw - 20px);
}
.show-image-button {
cursor: zoom-in;
}
</style>
<script>
@ -120,9 +101,16 @@
})
},
computed: {
focus: ({ meta }) => meta && meta.focus,
// width/height to show inline
inlineWidth: ({ smallWidth }) => smallWidth || DEFAULT_MEDIA_WIDTH,
inlineHeight: ({ smallHeight }) => smallHeight || DEFAULT_MEDIA_HEIGHT,
inlineWidth: ({ smallWidth, $groupedImages }) => {
if ($groupedImages) return '100%'
return smallWidth || DEFAULT_MEDIA_WIDTH
},
inlineHeight: ({ smallHeight, $groupedImages }) => {
if ($groupedImages) return 'auto'
return smallHeight || DEFAULT_MEDIA_HEIGHT
},
// width/height to show in a modal
modalWidth: ({ originalWidth, inlineWidth }) => originalWidth || inlineWidth,
modalHeight: ({ originalHeight, inlineHeight }) => originalHeight || inlineHeight,

View file

@ -1,5 +1,10 @@
<div class="status-media {sensitive ? 'status-media-is-sensitive' : ''}"
style="grid-template-columns: repeat(auto-fit, minmax({maxMediaWidth}px, 1fr));" >
<div class="status-media
{sensitive ? 'status-media-is-sensitive' : ''}
{oddCols ? 'odd-cols' : ''}
{twoCols ? 'two-cols' : ''}
{$groupedImages ? 'grouped-images' : ''}
"
style="grid-template-columns: repeat({nCols}, 1fr); " >
{#each mediaAttachments as media}
<Media {media} {uuid} />
{/each}
@ -11,35 +16,55 @@
align-items: center;
justify-content: center;
justify-items: center;
grid-column-gap: 10px;
grid-row-gap: 10px;
margin: 10px 0;
grid-column-gap: 5px;
grid-row-gap: 5px;
overflow: hidden;
max-width: calc(100vw - 40px);
margin: 10px 10px 10px 5px;
}
.status-media.grouped-images {
grid-area: media-grp;
border-radius: 6px;
}
.status-media.grouped-images > :global(*) {
padding-bottom: 56.25%;
width: 100%;
}
.status-media.grouped-images.two-cols > :global(*) {
padding-bottom: calc(112.5% + 5px);
}
.status-media.grouped-images.odd-cols > :global(:first-child) {
grid-row-end: span 2;
padding-bottom: calc(112.5% + 5px);
background-color: blue;
}
.status-media.status-media-is-sensitive {
margin: 0;
}
.status-media {
overflow: hidden;
}
.status-media {
max-width: calc(100vw - 40px);
}
@media (max-width: 767px) {
.status-media {
max-width: calc(100vw - 20px);
}
}
</style>
<script>
import Media from './Media.html'
import { DEFAULT_MEDIA_WIDTH } from '../../_static/media'
export default {
computed: {
maxMediaWidth: ({ mediaAttachments }) => {
return Math.max.apply(Math, mediaAttachments.map(media => {
return media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH
}))
nCols:
({ mediaAttachments, $groupedImages }) => {
return ($groupedImages && mediaAttachments.length > 1) ? 2 : 1
},
oddCols:
({ mediaAttachments }) => {
return (mediaAttachments.length > 1 && (mediaAttachments.length % 2))
},
twoCols:
({ mediaAttachments }) => {
return (mediaAttachments.length === 2)
}
},
components: {

View file

@ -49,6 +49,7 @@
"sidebar spoiler-btn spoiler-btn spoiler-btn"
"sidebar mentions mentions mentions"
"sidebar content content content"
"sidebar media-grp media-grp media-grp"
"media media media media"
"....... toolbar toolbar toolbar"
"compose compose compose compose";

View file

@ -36,6 +36,7 @@
<style>
.status-sensitive-media-container {
grid-area: media;
width: 100%;
margin: 10px 0;
position: relative;
border-radius: 0;

View file

@ -27,6 +27,11 @@
bind:checked="$autoplayGifs" on:change="onChange(event)">
<label for="choice-autoplay-gif">Autoplay GIFs</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-grouped-images"
bind:checked="$groupedImages" on:change="onChange(event)">
<label for="choice-groupes-images">Group images</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-reduce-motion"
bind:checked="$reduceMotion" on:change="onChange(event)">

View file

@ -13,6 +13,7 @@ const persistedState = {
disableCustomScrollbars: false,
disableLongAriaLabels: false,
disableTapOnStatus: false,
groupedImages: false,
instanceNameInSearch: '',
instanceThemes: {},
loggedInInstances: {},
@ -22,7 +23,9 @@ const persistedState = {
omitEmojiInDisplayNames: undefined,
pinnedPages: {},
pushSubscription: null,
reduceMotion: !process.browser || window.matchMedia('(prefers-reduced-motion: reduce)').matches
reduceMotion:
!process.browser ||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
}
const nonPersistedState = {
@ -31,7 +34,11 @@ const nonPersistedState = {
instanceLists: {},
online: !process.browser || navigator.onLine,
pinnedStatuses: {},
pushNotificationsSupport: process.browser && ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in window.PushSubscription.prototype),
pushNotificationsSupport:
process.browser &&
('serviceWorker' in navigator &&
'PushManager' in window &&
'getKey' in window.PushSubscription.prototype),
queryInSearch: '',
repliesShown: {},
sensitivesShown: {},