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>
|
||||
|
||||
<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,22 +235,32 @@ function getImageStickers($: CheerioAPI, message: MessageSelection, options: Ind
|
||||
|
||||
function getImages($: CheerioAPI, message: MessageSelection, options: MessageAssetOptions): string {
|
||||
const { staticProxy = '', id = '', index = 0, title = '' } = options
|
||||
const fragments: string[] = []
|
||||
const loading = getImageLoading(index)
|
||||
const safeTitle = escapeHtmlAttribute(title || 'Image from post')
|
||||
const safePreviewLabel = escapeHtmlAttribute(title ? `Open image preview: ${title}` : 'Open image preview')
|
||||
const safeCloseLabel = 'Close image preview'
|
||||
|
||||
for (const [photoIndex, photoNode] of message.find('.tgme_widget_message_photo_wrap').toArray().entries()) {
|
||||
const imageUrl = $(photoNode).attr('style')?.match(STYLE_URL_REGEX)?.[1]
|
||||
interface PhotoData { imageUrl: string, width: number, height: number }
|
||||
const photos: PhotoData[] = []
|
||||
|
||||
if (!imageUrl) {
|
||||
for (const photoNode of message.find('.tgme_widget_message_photo_wrap').toArray()) {
|
||||
const imageUrl = $(photoNode).attr('style')?.match(STYLE_URL_REGEX)?.[1]
|
||||
if (!imageUrl)
|
||||
continue
|
||||
const { width, height } = inferImageDimensions($, photoNode)
|
||||
photos.push({ imageUrl, width, height })
|
||||
}
|
||||
|
||||
const popoverId = `modal-${id}-${photoIndex}`
|
||||
const { width, height } = inferImageDimensions($, photoNode)
|
||||
fragments.push(`
|
||||
if (!photos.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Single image: keep existing per-image popover behavior
|
||||
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"
|
||||
@@ -261,33 +271,54 @@ function getImages($: CheerioAPI, message: MessageSelection, options: MessageAss
|
||||
<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>
|
||||
<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>
|
||||
`
|
||||
}
|
||||
|
||||
if (!fragments.length) {
|
||||
return ''
|
||||
}
|
||||
// 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 layoutClass = fragments.length % 2 === 0 ? 'image-list-even' : 'image-list-odd'
|
||||
return `<div class="image-list-container ${layoutClass}">${fragments.join('')}</div>`
|
||||
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 {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
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));
|
||||
max-width: calc(100% - 1px);
|
||||
max-height: initial;
|
||||
@@ -250,6 +250,114 @@
|
||||
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 {
|
||||
margin-top: 16px;
|
||||
display: none;
|
||||
@@ -548,6 +656,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-preview-button.image-gallery__trigger {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.image-preview-button:focus-visible {
|
||||
outline: 2px solid var(--color-heading);
|
||||
outline-offset: 4px;
|
||||
|
||||
Reference in New Issue
Block a user