feat: add carousel for media modal (#928)
This commit is contained in:
parent
2ef4743b3c
commit
9d594f0bac
|
@ -24,7 +24,7 @@ const builders = [
|
|||
rebuild: buildInlineScript
|
||||
},
|
||||
{
|
||||
watch: 'src/bin/svgs.js',
|
||||
watch: 'bin/svgs.js',
|
||||
comment: '<!-- inline SVG -->',
|
||||
rebuild: buildSvg
|
||||
}
|
||||
|
|
|
@ -36,5 +36,10 @@ module.exports = [
|
|||
{ id: 'fa-times', src: 'src/thirdparty/font-awesome-svg-png/white/svg/times.svg' },
|
||||
{ id: 'fa-volume-off', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-off.svg' },
|
||||
{ id: 'fa-volume-up', src: 'src/thirdparty/font-awesome-svg-png/white/svg/volume-up.svg' },
|
||||
{ id: 'fa-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/link.svg' }
|
||||
{ id: 'fa-link', src: 'src/thirdparty/font-awesome-svg-png/white/svg/link.svg' },
|
||||
{ id: 'fa-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle.svg' },
|
||||
{ id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
|
||||
{ id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
|
||||
{ id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' }
|
||||
|
||||
]
|
||||
|
|
|
@ -99,6 +99,22 @@
|
|||
fill: var(--action-button-deemphasized-fill-color-pressed-active);
|
||||
}
|
||||
|
||||
/*
|
||||
* disable the separate press color (noPressColor)
|
||||
*/
|
||||
.icon-button.pressed.no-press-color .icon-button-svg {
|
||||
fill: var(--action-button-fill-color);
|
||||
}
|
||||
|
||||
.icon-button.pressed.no-press-color:hover .icon-button-svg {
|
||||
fill: var(--action-button-fill-color-hover);
|
||||
}
|
||||
|
||||
.icon-button.pressed.no-press-color:active .icon-button-svg {
|
||||
fill: var(--action-button-fill-color-active);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import { classname } from '../_utils/classname'
|
||||
|
@ -114,17 +130,19 @@
|
|||
pressable: false,
|
||||
pressed: false,
|
||||
className: void 0,
|
||||
delegateKey: void 0
|
||||
delegateKey: void 0,
|
||||
noPressColor: false
|
||||
}),
|
||||
store: () => store,
|
||||
computed: {
|
||||
computedClass: ({ pressable, pressed, big, muted, className }) => {
|
||||
computedClass: ({ pressable, pressed, big, muted, noPressColor, className }) => {
|
||||
return classname(
|
||||
'icon-button',
|
||||
!pressable && 'not-pressable',
|
||||
pressed && 'pressed',
|
||||
big && 'big-icon',
|
||||
muted && 'muted-style',
|
||||
noPressColor && 'no-press-color',
|
||||
className
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,39 +1,37 @@
|
|||
const getDefault = mod => mod.default
|
||||
|
||||
export const importShowAccountProfileOptionsDialog = () => import(
|
||||
/* webpackChunkName: 'showAccountProfileOptionsDialog' */ './creators/showAccountProfileOptionsDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowComposeDialog = () => import(
|
||||
/* webpackChunkName: 'showComposeDialog' */ './creators/showComposeDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowConfirmationDialog = () => import(
|
||||
/* webpackChunkName: 'showConfirmationDialog' */ './creators/showConfirmationDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowEmojiDialog = () => import(
|
||||
/* webpackChunkName: 'showEmojiDialog' */ './creators/showEmojiDialog'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importShowImageDialog = () => import(
|
||||
/* webpackChunkName: 'showImageDialog' */ './creators/showImageDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowPostPrivacyDialog = () => import(
|
||||
/* webpackChunkName: 'showPostPrivacyDialog' */ './creators/showPostPrivacyDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowStatusOptionsDialog = () => import(
|
||||
/* webpackChunkName: 'showStatusOptionsDialog' */ './creators/showStatusOptionsDialog'
|
||||
).then(mod => mod.default)
|
||||
|
||||
export const importShowVideoDialog = () => import(
|
||||
/* webpackChunkName: 'showVideoDialog' */ './creators/showVideoDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowCopyDialog = () => import(
|
||||
/* webpackChunkName: 'showCopyDialog' */ './creators/showCopyDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowShortcutHelpDialog = () => import(
|
||||
/* webpackChunkName: 'showShortcutHelpDialog' */ './creators/showShortcutHelpDialog'
|
||||
).then(mod => mod.default)
|
||||
).then(getDefault)
|
||||
|
||||
export const importShowMediaDialog = () => import(
|
||||
/* webpackChunkName: 'showMediaDialog' */ './creators/showMediaDialog'
|
||||
).then(getDefault)
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
background="var(--muted-modal-bg)"
|
||||
muted="true"
|
||||
className="image-modal-dialog"
|
||||
>
|
||||
{#if type === 'gifv'}
|
||||
<video
|
||||
class="image-modal-dialog-autoplay-video"
|
||||
aria-label="Animated GIF: {description || ''}"
|
||||
style="{videoStyle}"
|
||||
{src}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
webkit-playsinline
|
||||
playsinline
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
{src}
|
||||
{style}
|
||||
alt={description || ''}
|
||||
title={description || ''}
|
||||
/>
|
||||
{/if}
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.image-modal-dialog img, .image-modal-dialog video) {
|
||||
object-fit: contain;
|
||||
max-width: calc(100vw - 20px);
|
||||
max-height: calc(100% - 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-modal-dialog-autoplay-video {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
components: {
|
||||
ModalDialog
|
||||
},
|
||||
computed: {
|
||||
style: ({ width, height }) => `
|
||||
width: ${width ? width + 'px' : 'auto'};
|
||||
height: ${height ? height + 'px' : 'auto'};`,
|
||||
videoStyle: ({ style, poster }) => `
|
||||
${style}
|
||||
background-image: url(${poster});`
|
||||
},
|
||||
methods: {
|
||||
show
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,214 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
background="var(--muted-modal-bg)"
|
||||
muted="true"
|
||||
className="media-modal-dialog"
|
||||
>
|
||||
<div class="media-container">
|
||||
<div class="media-scroll" ref:scroller>
|
||||
{#each mediaItems as media}
|
||||
<div class="media-scroll-item">
|
||||
<div class="media-scroll-item-inner">
|
||||
<div class="media-scroll-item-inner-inner">
|
||||
<MediaInDialog {media} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if dots.length > 1}
|
||||
<div class="media-controls">
|
||||
<IconButton
|
||||
className="media-control-button"
|
||||
disabled={scrolledItem === 0}
|
||||
label="Show previous media"
|
||||
href="#fa-angle-left"
|
||||
on:click="onClick(scrolledItem - 1)"
|
||||
/>
|
||||
{#each dots as dot, i (dot.i)}
|
||||
<IconButton
|
||||
className="media-control-button"
|
||||
pressable={true}
|
||||
label="Show {nth(i)} media"
|
||||
pressed={i === scrolledItem}
|
||||
href={i === scrolledItem ? '#fa-circle' : '#fa-circle-o'}
|
||||
noPressColor={true}
|
||||
on:click="onClick(i)"
|
||||
/>
|
||||
{/each}
|
||||
<IconButton
|
||||
className="media-control-button"
|
||||
disabled={scrolledItem === length - 1}
|
||||
label="Show next media"
|
||||
href="#fa-angle-right"
|
||||
on:click="onClick(scrolledItem + 1)"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.media-modal-dialog) {
|
||||
max-width: calc(100vw);
|
||||
}
|
||||
.media-container {
|
||||
height: calc(100% - 64px); /* 44px X button height + 20px padding */
|
||||
width: calc(100vw);
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.media-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.media-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.media-scroll-item {
|
||||
height: 100%;
|
||||
}
|
||||
.media-scroll-item-inner {
|
||||
width: 100vw;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.media-scroll-item-inner-inner {
|
||||
height: calc(100% - 10px);
|
||||
width: calc(100% - 10px);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.media-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
:global(.media-control-button) {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
@supports (scroll-snap-align: start) {
|
||||
/* modern scroll snap points */
|
||||
.media-scroll {
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
.media-scroll-item {
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
}
|
||||
@supports not (scroll-snap-align: start) {
|
||||
/* old scroll snap points spec */
|
||||
.media-scroll {
|
||||
-webkit-scroll-snap-type: mandatory;
|
||||
scroll-snap-type: mandatory;
|
||||
-webkit-scroll-snap-destination: 0% center;
|
||||
scroll-snap-destination: 0% center;
|
||||
-webkit-scroll-snap-points-x: repeat(100%);
|
||||
scroll-snap-points-x: repeat(100%);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import MediaInDialog from './MediaInDialog.html'
|
||||
import IconButton from '../../IconButton.html'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { oncreate as onCreateDialog } from '../helpers/onCreateDialog'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
import times from 'lodash-es/times'
|
||||
import { smoothScroll } from '../../../_utils/smoothScroll'
|
||||
import { doubleRAF } from '../../../_utils/doubleRAF'
|
||||
import { store } from '../../../_store/store'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
onCreateDialog.call(this)
|
||||
|
||||
this.onScroll = debounce(this.onScroll.bind(this), 100, { leading: false, trailing: true })
|
||||
|
||||
let { scrolledItem } = this.get()
|
||||
if (scrolledItem) {
|
||||
doubleRAF(() => {
|
||||
this.scrollToItem(scrolledItem, false)
|
||||
this.setupScroll()
|
||||
})
|
||||
} else {
|
||||
this.setupScroll()
|
||||
}
|
||||
},
|
||||
ondestroy () {
|
||||
this.teardownScroll()
|
||||
},
|
||||
store: () => store,
|
||||
computed: {
|
||||
length: ({ mediaItems }) => mediaItems.length,
|
||||
originalWidths: ({ mediaItems }) => mediaItems.map(_ => _.meta.original.width),
|
||||
maxWidth: ({ originalWidths }) => Math.max.apply(Math, originalWidths),
|
||||
dots: ({ length }) => times(length, i => ({ i }))
|
||||
},
|
||||
components: {
|
||||
ModalDialog,
|
||||
MediaInDialog,
|
||||
IconButton
|
||||
},
|
||||
helpers: {
|
||||
nth (i) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
return 'first'
|
||||
case 1:
|
||||
return 'second'
|
||||
case 2:
|
||||
return 'third'
|
||||
case 3:
|
||||
return 'fourth'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show,
|
||||
setupScroll () {
|
||||
this.refs.scroller.addEventListener('scroll', this.onScroll)
|
||||
},
|
||||
teardownScroll () {
|
||||
this.refs.scroller.removeEventListener('scroll', this.onScroll)
|
||||
},
|
||||
onScroll () {
|
||||
let { length } = this.get()
|
||||
let { scrollWidth, scrollLeft } = this.refs.scroller
|
||||
let scrolledItem = Math.floor((scrollLeft / scrollWidth) * length)
|
||||
this.set({ scrolledItem })
|
||||
},
|
||||
onClick (i) {
|
||||
let { scrolledItem } = this.get()
|
||||
if (scrolledItem !== i) {
|
||||
this.scrollToItem(i, true)
|
||||
}
|
||||
},
|
||||
scrollToItem (i, smooth) {
|
||||
let { length } = this.get()
|
||||
let { scroller } = this.refs
|
||||
let { scrollWidth } = scroller
|
||||
let scrollLeft = Math.floor(scrollWidth * (i / length))
|
||||
if (smooth) {
|
||||
smoothScroll(scroller, scrollLeft, true)
|
||||
} else {
|
||||
scroller.scrollLeft = scrollLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,52 @@
|
|||
{#if type === 'video'}
|
||||
<video
|
||||
class="media-fit"
|
||||
aria-label={description}
|
||||
src={url}
|
||||
{poster}
|
||||
controls
|
||||
ref:video
|
||||
/>
|
||||
{:elseif type === 'gifv'}
|
||||
<video
|
||||
class="media-fit"
|
||||
style="background-image:url({static_url});"
|
||||
aria-label={description}
|
||||
src={url}
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
webkit-playsinline
|
||||
playsinline
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
class="media-fit"
|
||||
alt={description}
|
||||
title={description}
|
||||
src={url}
|
||||
/>
|
||||
{/if}
|
||||
<style>
|
||||
.media-fit {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
type: ({ media }) => media.type,
|
||||
url: ({ media }) => media.url,
|
||||
description: ({ media }) => media.description || '',
|
||||
poster: ({ media }) => media.poster,
|
||||
static_url: ({ media }) => media.static_url
|
||||
},
|
||||
ondestroy () {
|
||||
if (this.refs.video && !this.refs.video.paused) {
|
||||
this.refs.video.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,49 +0,0 @@
|
|||
<ModalDialog
|
||||
{id}
|
||||
{label}
|
||||
background="var(--muted-modal-bg)"
|
||||
muted="true"
|
||||
className="video-modal-dialog"
|
||||
on:close="onClose()"
|
||||
>
|
||||
<video {poster}
|
||||
{src}
|
||||
{style}
|
||||
aria-label="Video: {description || ''}"
|
||||
controls
|
||||
ref:video
|
||||
/>
|
||||
</ModalDialog>
|
||||
<style>
|
||||
:global(.video-modal-dialog video) {
|
||||
object-fit: contain;
|
||||
max-width: calc(100vw - 20px);
|
||||
max-height: calc(100% - 20px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import ModalDialog from './ModalDialog.html'
|
||||
import { show } from '../helpers/showDialog'
|
||||
import { oncreate } from '../helpers/onCreateDialog'
|
||||
|
||||
export default {
|
||||
oncreate,
|
||||
components: {
|
||||
ModalDialog
|
||||
},
|
||||
computed: {
|
||||
style: ({ width, height }) => `
|
||||
width: ${width ? width + 'px' : 'auto'};
|
||||
height: ${height ? height + 'px' : 'auto'};`
|
||||
},
|
||||
methods: {
|
||||
show,
|
||||
onClose () {
|
||||
if (this.refs.video && !this.refs.video.paused) {
|
||||
this.refs.video.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,20 +0,0 @@
|
|||
import ImageDialog from '../components/ImageDialog.html'
|
||||
import { createDialogElement } from '../helpers/createDialogElement'
|
||||
import { createDialogId } from '../helpers/createDialogId'
|
||||
|
||||
export default function showImageDialog (poster, src, type, width, height, description) {
|
||||
let imageDialog = new ImageDialog({
|
||||
target: createDialogElement(),
|
||||
data: {
|
||||
id: createDialogId(),
|
||||
label: 'Image dialog',
|
||||
poster,
|
||||
src,
|
||||
type,
|
||||
width,
|
||||
height,
|
||||
description
|
||||
}
|
||||
})
|
||||
imageDialog.show()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import MediaDialog from '../components/MediaDialog.html'
|
||||
import { createDialogElement } from '../helpers/createDialogElement'
|
||||
import { createDialogId } from '../helpers/createDialogId'
|
||||
|
||||
export default function showMediaDialog (mediaItems, scrolledItem) {
|
||||
let dialog = new MediaDialog({
|
||||
target: createDialogElement(),
|
||||
data: {
|
||||
id: createDialogId(),
|
||||
label: 'Media dialog',
|
||||
mediaItems,
|
||||
scrolledItem
|
||||
}
|
||||
})
|
||||
dialog.show()
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
import VideoDialog from '../components/VideoDialog.html'
|
||||
import { createDialogElement } from '../helpers/createDialogElement'
|
||||
import { createDialogId } from '../helpers/createDialogId'
|
||||
|
||||
export default function showVideoDialog (poster, src, width, height, description) {
|
||||
let videoDialog = new VideoDialog({
|
||||
target: createDialogElement(),
|
||||
data: {
|
||||
id: createDialogId(),
|
||||
label: 'Video dialog',
|
||||
poster,
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
description
|
||||
}
|
||||
})
|
||||
videoDialog.show()
|
||||
}
|
|
@ -79,7 +79,7 @@
|
|||
</style>
|
||||
<script>
|
||||
import { DEFAULT_MEDIA_WIDTH, DEFAULT_MEDIA_HEIGHT, ONE_TRANSPARENT_PIXEL } from '../../_static/media'
|
||||
import { importShowVideoDialog, importShowImageDialog } from '../dialog/asyncDialogs'
|
||||
import { importShowMediaDialog } from '../dialog/asyncDialogs'
|
||||
import { mouseover } from '../../_utils/events'
|
||||
import NonAutoplayGifv from '../NonAutoplayGifv.html'
|
||||
import PlayVideoIcon from '../PlayVideoIcon.html'
|
||||
|
@ -91,14 +91,7 @@
|
|||
export default {
|
||||
oncreate () {
|
||||
let { delegateKey } = this.get()
|
||||
registerClickDelegate(this, delegateKey, () => {
|
||||
let { type } = this.get()
|
||||
if (type === 'video') {
|
||||
this.onClickPlayVideoButton()
|
||||
} else {
|
||||
this.onClickShowImageButton()
|
||||
}
|
||||
})
|
||||
registerClickDelegate(this, delegateKey, () => this.onClick())
|
||||
},
|
||||
computed: {
|
||||
focus: ({ meta }) => meta && meta.focus,
|
||||
|
@ -133,17 +126,10 @@
|
|||
type: ({ media }) => media.type
|
||||
},
|
||||
methods: {
|
||||
async onClickPlayVideoButton () {
|
||||
let { previewUrl, url, modalWidth, modalHeight, description } = this.get()
|
||||
let showVideoDialog = await importShowVideoDialog()
|
||||
showVideoDialog(previewUrl, url,
|
||||
modalWidth, modalHeight, description)
|
||||
},
|
||||
async onClickShowImageButton () {
|
||||
let { previewUrl, url, modalWidth, modalHeight, description, type } = this.get()
|
||||
let showImageDialog = await importShowImageDialog()
|
||||
showImageDialog(previewUrl, url, type,
|
||||
modalWidth, modalHeight, description)
|
||||
async onClick () {
|
||||
let { mediaAttachments, index } = this.get()
|
||||
let showMediaDialog = await importShowMediaDialog()
|
||||
showMediaDialog(mediaAttachments, index)
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class={computedClass}
|
||||
style="grid-template-columns: repeat({nCols}, 1fr);" >
|
||||
{#each mediaAttachments as media}
|
||||
<Media {media} {uuid} />
|
||||
{#each mediaAttachments as media, index}
|
||||
<Media {media} {uuid} {mediaAttachments} {index} />
|
||||
{/each}
|
||||
</div>
|
||||
<style>
|
||||
|
|
|
@ -61,13 +61,13 @@ function testSupportsSmoothScroll () {
|
|||
|
||||
const smoothScrollSupported = process.browser && testSupportsSmoothScroll()
|
||||
|
||||
export function smoothScroll (node, top) {
|
||||
export function smoothScroll (node, topOrLeft, horizontal) {
|
||||
if (smoothScrollSupported) {
|
||||
return node.scrollTo({
|
||||
top: top,
|
||||
[horizontal ? 'left' : 'top']: topOrLeft,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
} else {
|
||||
return smoothScrollPolyfill(node, 'scrollTop', top)
|
||||
return smoothScrollPolyfill(node, horizontal ? 'scrollLeft' : 'scrollTop', topOrLeft)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue