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 @@
<div class="autoplay-wrapper {$groupedImages ? 'fixed-size': ''}"
style="width: {width}px; height: {height}px;"
>
<video <video
class="autoplay-video {className || ''}" class="autoplay-video {$groupedImages ? 'fixed-size': ''}"
aria-label={ariaLabel || ''} aria-label={ariaLabel || ''}
style="background-image: url({poster});" style="{focusStyle} background-image: url({poster}); "
{poster} {poster}
{width} {width}
{height} {height}
@ -12,10 +15,53 @@
webkit-playsinline webkit-playsinline
playsinline playsinline
/> />
</div>
<style> <style>
.autoplay-wrapper {
position: relative;
margin: 0;
padding: 0;
}
.autoplay-video { .autoplay-video {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; 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> </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 <LazyImage
className={computedClass} className={computedClass}
ariaHidden="true" ariaHidden="true"
forceSize=true
alt="" alt=""
src={account.avatar} src={account.avatar}
{width} {width}

View file

@ -1,22 +1,38 @@
<div class="lazy-image" style={computedStyle} > <div class="lazy-image {fillFixSize ? 'fixed-size': ''}" style={computedStyle} >
<img <img
class={className}
aria-hidden={ariaHidden} aria-hidden={ariaHidden}
{alt} {alt}
{title} {title}
{width} {width}
{height} {height}
src={displaySrc} src={displaySrc}
style={focusStyle}
ref:node ref:node
/> />
</div> </div>
<style> <style>
.lazy-image { .lazy-image {
margin: 0;
padding: 0;
overflow: hidden; overflow: hidden;
display: flex;
} }
.fixed-size img {
width: 100%;
height: 100%;
}
.fixed-size {
position: absolute;
width: 100%;
height: 100%;
}
</style> </style>
<script> <script>
import { decodeImage } from '../_utils/decodeImage' import { decodeImage } from '../_utils/decodeImage'
const coordsToPercent = coord => (1 + coord) / 2 * 100
export default { export default {
async oncreate () { async oncreate () {
@ -28,23 +44,30 @@
}, },
data: () => ({ data: () => ({
error: false, error: false,
forceSize: false,
fallback: void 0, fallback: void 0,
focus: void 0,
background: '', background: '',
width: void 0, width: void 0,
height: void 0, height: void 0,
className: '',
ariaHidden: false, ariaHidden: false,
alt: '', alt: '',
title: '' title: ''
}), }),
computed: { computed: {
computedStyle: ({ width, height, background }) => { computedStyle: ({ background }) => {
return [ return [
width && `width: ${width}px;`,
height && `height: ${height}px;`,
background && `background: ${background};` background && `background: ${background};`
].filter(Boolean).join('') ].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) 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)" on:mouseover="onMouseOver(event)"
ref:node ref:node
> >
{#if playing} {#if playing}
<AutoplayVideo <AutoplayVideo
className={class}
ariaLabel={label} ariaLabel={label}
{poster} {poster}
{src} {src}
{width} {width}
{height} {height}
{focus}
/> />
{:else} {:else}
<LazyImage <LazyImage
@ -20,7 +21,7 @@
{width} {width}
{height} {height}
background="var(--loading-bg)" background="var(--loading-bg)"
className={class} {focus}
/> />
{/if} {/if}
<PlayVideoIcon className={playing ? 'hidden' : ''}/> <PlayVideoIcon className={playing ? 'hidden' : ''}/>
@ -28,8 +29,20 @@
<style> <style>
.non-autoplay-gifv { .non-autoplay-gifv {
cursor: zoom-in; cursor: zoom-in;
display: flex;
position: relative; position: relative;
margin: 0;
padding: 0;
} }
.fixed-size {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
:global(.non-autoplay-gifv .play-video-icon) { :global(.non-autoplay-gifv .play-video-icon) {
transition: opacity 0.2s linear; transition: opacity 0.2s linear;
} }
@ -51,7 +64,8 @@
mouseover mouseover
}, },
data: () => ({ data: () => ({
oneTransparentPixel: ONE_TRANSPARENT_PIXEL oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
focus: void 0
}), }),
components: { components: {
PlayVideoIcon, PlayVideoIcon,

View file

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

View file

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

View file

@ -1,5 +1,10 @@
<div class="status-media {sensitive ? 'status-media-is-sensitive' : ''}" <div class="status-media
style="grid-template-columns: repeat(auto-fit, minmax({maxMediaWidth}px, 1fr));" > {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} {#each mediaAttachments as media}
<Media {media} {uuid} /> <Media {media} {uuid} />
{/each} {/each}
@ -11,35 +16,55 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
justify-items: center; justify-items: center;
grid-column-gap: 10px; grid-column-gap: 5px;
grid-row-gap: 10px; grid-row-gap: 5px;
margin: 10px 0;
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 { .status-media.status-media-is-sensitive {
margin: 0; 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> </style>
<script> <script>
import Media from './Media.html' import Media from './Media.html'
import { DEFAULT_MEDIA_WIDTH } from '../../_static/media'
export default { export default {
computed: { computed: {
maxMediaWidth: ({ mediaAttachments }) => { nCols:
return Math.max.apply(Math, mediaAttachments.map(media => { ({ mediaAttachments, $groupedImages }) => {
return media.meta && media.meta.small && typeof media.meta.small.width === 'number' ? media.meta.small.width : DEFAULT_MEDIA_WIDTH return ($groupedImages && mediaAttachments.length > 1) ? 2 : 1
})) },
oddCols:
({ mediaAttachments }) => {
return (mediaAttachments.length > 1 && (mediaAttachments.length % 2))
},
twoCols:
({ mediaAttachments }) => {
return (mediaAttachments.length === 2)
} }
}, },
components: { components: {

View file

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

View file

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

View file

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

View file

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