mirror of
https://github.com/d0zingcat/BroadcastChannel.git
synced 2026-05-13 15:09:12 +00:00
Merge pull request #1 from d0zingcat/feat/image-gallery
This commit is contained in:
@@ -110,3 +110,77 @@ const commentsClass = 'ml-[3px] border-l-2 border-line pb-6 pl-[15px] pt-[6px] s
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showGalleryImage(modal: HTMLElement, index: number): void {
|
||||||
|
const images = modal.querySelectorAll<HTMLElement>('.gallery__img')
|
||||||
|
images.forEach((img, i) => {
|
||||||
|
img.toggleAttribute('hidden', i !== index)
|
||||||
|
})
|
||||||
|
modal.dataset.current = String(index)
|
||||||
|
const counter = modal.querySelector('.gallery__counter')
|
||||||
|
if (counter) {
|
||||||
|
counter.textContent = `${index + 1} / ${images.length}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateGallery(modal: HTMLElement, dir: number): void {
|
||||||
|
const images = modal.querySelectorAll('.gallery__img')
|
||||||
|
const current = Number.parseInt(modal.dataset.current ?? '0', 10)
|
||||||
|
const next = (current + dir + images.length) % images.length
|
||||||
|
showGalleryImage(modal, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', (e: MouseEvent) => {
|
||||||
|
const target = e.target as Element
|
||||||
|
|
||||||
|
const trigger = target.closest<HTMLElement>('.image-gallery__trigger')
|
||||||
|
if (trigger) {
|
||||||
|
const galleryId = trigger.dataset.gallery
|
||||||
|
const startIndex = Number.parseInt(trigger.dataset.index ?? '0', 10)
|
||||||
|
if (!galleryId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const modal = document.getElementById(galleryId) as HTMLElement | null
|
||||||
|
if (!modal) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Set active image before native popovertarget opens the modal
|
||||||
|
showGalleryImage(modal, startIndex)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevBtn = target.closest('.gallery__prev')
|
||||||
|
if (prevBtn) {
|
||||||
|
const modal = prevBtn.closest<HTMLElement>('.image-gallery__modal')
|
||||||
|
if (modal) {
|
||||||
|
navigateGallery(modal, -1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextBtn = target.closest('.gallery__next')
|
||||||
|
if (nextBtn) {
|
||||||
|
const modal = nextBtn.closest<HTMLElement>('.image-gallery__modal')
|
||||||
|
if (modal) {
|
||||||
|
navigateGallery(modal, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
const modal = document.querySelector<HTMLElement>('.image-gallery__modal:popover-open')
|
||||||
|
if (!modal) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowLeft') {
|
||||||
|
e.preventDefault()
|
||||||
|
navigateGallery(modal, -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowRight') {
|
||||||
|
e.preventDefault()
|
||||||
|
navigateGallery(modal, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -235,59 +235,90 @@ function getImageStickers($: CheerioAPI, message: MessageSelection, options: Ind
|
|||||||
|
|
||||||
function getImages($: CheerioAPI, message: MessageSelection, options: MessageAssetOptions): string {
|
function getImages($: CheerioAPI, message: MessageSelection, options: MessageAssetOptions): string {
|
||||||
const { staticProxy = '', id = '', index = 0, title = '' } = options
|
const { staticProxy = '', id = '', index = 0, title = '' } = options
|
||||||
const fragments: string[] = []
|
|
||||||
const loading = getImageLoading(index)
|
const loading = getImageLoading(index)
|
||||||
const safeTitle = escapeHtmlAttribute(title || 'Image from post')
|
const safeTitle = escapeHtmlAttribute(title || 'Image from post')
|
||||||
const safePreviewLabel = escapeHtmlAttribute(title ? `Open image preview: ${title}` : 'Open image preview')
|
|
||||||
const safeCloseLabel = 'Close image preview'
|
const safeCloseLabel = 'Close image preview'
|
||||||
|
|
||||||
for (const [photoIndex, photoNode] of message.find('.tgme_widget_message_photo_wrap').toArray().entries()) {
|
interface PhotoData { imageUrl: string, width: number, height: number }
|
||||||
|
const photos: PhotoData[] = []
|
||||||
|
|
||||||
|
for (const photoNode of message.find('.tgme_widget_message_photo_wrap').toArray()) {
|
||||||
const imageUrl = $(photoNode).attr('style')?.match(STYLE_URL_REGEX)?.[1]
|
const imageUrl = $(photoNode).attr('style')?.match(STYLE_URL_REGEX)?.[1]
|
||||||
|
if (!imageUrl)
|
||||||
if (!imageUrl) {
|
|
||||||
continue
|
continue
|
||||||
}
|
|
||||||
|
|
||||||
const popoverId = `modal-${id}-${photoIndex}`
|
|
||||||
const { width, height } = inferImageDimensions($, photoNode)
|
const { width, height } = inferImageDimensions($, photoNode)
|
||||||
fragments.push(`
|
photos.push({ imageUrl, width, height })
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="image-preview-button image-preview-wrap"
|
|
||||||
popovertarget="${popoverId}"
|
|
||||||
popovertargetaction="show"
|
|
||||||
aria-label="${safePreviewLabel}"
|
|
||||||
>
|
|
||||||
<img src="${staticProxy + imageUrl}" alt="${safeTitle}" width="${width}" height="${height}" loading="${loading}" />
|
|
||||||
</button>
|
|
||||||
<div class="modal" id="${popoverId}" popover aria-label="Image preview">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="modal__backdrop"
|
|
||||||
popovertarget="${popoverId}"
|
|
||||||
popovertargetaction="hide"
|
|
||||||
aria-label="${safeCloseLabel}"
|
|
||||||
></button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="modal__close"
|
|
||||||
popovertarget="${popoverId}"
|
|
||||||
popovertargetaction="hide"
|
|
||||||
aria-label="${safeCloseLabel}"
|
|
||||||
>×</button>
|
|
||||||
<div class="modal__surface">
|
|
||||||
<img class="modal-img" src="${staticProxy + imageUrl}" alt="${safeTitle}" width="${width}" height="${height}" loading="lazy" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fragments.length) {
|
if (!photos.length) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const layoutClass = fragments.length % 2 === 0 ? 'image-list-even' : 'image-list-odd'
|
// Single image: keep existing per-image popover behavior
|
||||||
return `<div class="image-list-container ${layoutClass}">${fragments.join('')}</div>`
|
if (photos.length === 1) {
|
||||||
|
const { imageUrl, width, height } = photos[0]
|
||||||
|
const popoverId = `modal-${id}-0`
|
||||||
|
const safePreviewLabel = escapeHtmlAttribute(title ? `Open image preview: ${title}` : 'Open image preview')
|
||||||
|
return `
|
||||||
|
<div class="image-list-container image-list-odd">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="image-preview-button image-preview-wrap"
|
||||||
|
popovertarget="${popoverId}"
|
||||||
|
popovertargetaction="show"
|
||||||
|
aria-label="${safePreviewLabel}"
|
||||||
|
>
|
||||||
|
<img src="${staticProxy + imageUrl}" alt="${safeTitle}" width="${width}" height="${height}" loading="${loading}" />
|
||||||
|
</button>
|
||||||
|
<div class="modal" id="${popoverId}" popover aria-label="Image preview">
|
||||||
|
<button type="button" class="modal__backdrop" popovertarget="${popoverId}" popovertargetaction="hide" aria-label="${safeCloseLabel}"></button>
|
||||||
|
<button type="button" class="modal__close" popovertarget="${popoverId}" popovertargetaction="hide" aria-label="${safeCloseLabel}">×</button>
|
||||||
|
<div class="modal__surface">
|
||||||
|
<img class="modal-img" src="${staticProxy + imageUrl}" alt="${safeTitle}" width="${width}" height="${height}" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple images: single shared gallery modal with thumbnail grid
|
||||||
|
const galleryId = `gallery-${id}`
|
||||||
|
const count = photos.length
|
||||||
|
// 2 or 4 photos → 2-column; everything else → 3-column (matches Telegram)
|
||||||
|
const colClass = count === 2 || count === 4 ? 'image-gallery-2col' : 'image-gallery-3col'
|
||||||
|
|
||||||
|
const thumbnailButtons = photos.map(({ imageUrl, width, height }, i) => {
|
||||||
|
const safeGalleryLabel = escapeHtmlAttribute(`Open image gallery, image ${i + 1} of ${count}`)
|
||||||
|
return `
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="image-preview-button image-preview-wrap image-gallery__trigger"
|
||||||
|
popovertarget="${galleryId}"
|
||||||
|
popovertargetaction="show"
|
||||||
|
data-gallery="${galleryId}"
|
||||||
|
data-index="${i}"
|
||||||
|
aria-label="${safeGalleryLabel}"
|
||||||
|
>
|
||||||
|
<img class="gallery__thumb" src="${staticProxy + imageUrl}" alt="${safeTitle}" width="${width}" height="${height}" loading="${loading}" />
|
||||||
|
</button>`
|
||||||
|
}).join('')
|
||||||
|
|
||||||
|
const galleryImages = photos.map(({ imageUrl, width, height }, i) =>
|
||||||
|
`<img class="modal-img gallery__img" src="${staticProxy + imageUrl}" alt="${safeTitle}" width="${width}" height="${height}" loading="lazy"${i > 0 ? ' hidden' : ''} />`,
|
||||||
|
).join('')
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="image-gallery ${colClass}">${thumbnailButtons}
|
||||||
|
</div>
|
||||||
|
<div class="modal image-gallery__modal" id="${galleryId}" data-current="0" popover aria-label="Image gallery">
|
||||||
|
<button type="button" class="modal__backdrop" popovertarget="${galleryId}" popovertargetaction="hide" aria-label="${safeCloseLabel}"></button>
|
||||||
|
<button type="button" class="modal__close" popovertarget="${galleryId}" popovertargetaction="hide" aria-label="${safeCloseLabel}">×</button>
|
||||||
|
<button type="button" class="gallery__nav gallery__prev" aria-label="Previous image">‹</button>
|
||||||
|
<button type="button" class="gallery__nav gallery__next" aria-label="Next image">›</button>
|
||||||
|
<div class="modal__surface">${galleryImages}</div>
|
||||||
|
<p class="gallery__counter" aria-live="polite">1 / ${count}</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideo($: CheerioAPI, message: MessageSelection, options: IndexedStaticProxyOptions): string {
|
function getVideo($: CheerioAPI, message: MessageSelection, options: IndexedStaticProxyOptions): string {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
line-break: anywhere;
|
line-break: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
img:not(.tg-emoji):not(.sticker):not(.link_preview_image):not(.modal-img) {
|
img:not(.tg-emoji):not(.sticker):not(.link_preview_image):not(.modal-img):not(.gallery__thumb) {
|
||||||
width: calc(100% - var(--box-margin));
|
width: calc(100% - var(--box-margin));
|
||||||
max-width: calc(100% - 1px);
|
max-width: calc(100% - 1px);
|
||||||
max-height: initial;
|
max-height: initial;
|
||||||
@@ -250,6 +250,114 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Compact Telegram-style photo grid */
|
||||||
|
.image-gallery {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
border-radius: var(--radius-media);
|
||||||
|
overflow: hidden;
|
||||||
|
width: calc(100% - var(--box-margin));
|
||||||
|
max-width: calc(100% - 1px);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery.image-gallery-2col {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery.image-gallery-3col {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery__trigger {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery__trigger img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.image-gallery__trigger img {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery__trigger:hover img,
|
||||||
|
.image-gallery__trigger:focus-visible img {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-gallery__trigger:focus-visible {
|
||||||
|
outline: 2px solid var(--color-heading);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__img[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 20;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__nav:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__prev {
|
||||||
|
left: max(16px, env(safe-area-inset-left, 16px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__next {
|
||||||
|
right: max(16px, env(safe-area-inset-right, 16px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery__counter {
|
||||||
|
position: absolute;
|
||||||
|
bottom: max(16px, env(safe-area-inset-bottom, 16px));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 20;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 3px 12px;
|
||||||
|
border-radius: 9999px;
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.tgme_widget_message_link_preview {
|
.tgme_widget_message_link_preview {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
display: none;
|
display: none;
|
||||||
@@ -548,6 +656,10 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-preview-button.image-gallery__trigger {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.image-preview-button:focus-visible {
|
.image-preview-button:focus-visible {
|
||||||
outline: 2px solid var(--color-heading);
|
outline: 2px solid var(--color-heading);
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user