diff --git a/src/components/item.astro b/src/components/item.astro index d09f30f..068f61b 100644 --- a/src/components/item.astro +++ b/src/components/item.astro @@ -110,3 +110,77 @@ const commentsClass = 'ml-[3px] border-l-2 border-line pb-6 pl-[15px] pt-[6px] s ) } + + diff --git a/src/lib/telegram/index.ts b/src/lib/telegram/index.ts index ad14220..b6cab27 100644 --- a/src/lib/telegram/index.ts +++ b/src/lib/telegram/index.ts @@ -235,59 +235,90 @@ 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()) { + 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] - - if (!imageUrl) { + if (!imageUrl) continue - } - - const popoverId = `modal-${id}-${photoIndex}` const { width, height } = inferImageDimensions($, photoNode) - fragments.push(` - - - `) + photos.push({ imageUrl, width, height }) } - if (!fragments.length) { + if (!photos.length) { return '' } - const layoutClass = fragments.length % 2 === 0 ? 'image-list-even' : 'image-list-odd' - return `
${fragments.join('')}
` + // 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 ` +
+ + +
+ ` + } + + // 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 ` + ` + }).join('') + + const galleryImages = photos.map(({ imageUrl, width, height }, i) => + `${safeTitle} 0 ? ' hidden' : ''} />`, + ).join('') + + return ` + + + ` } function getVideo($: CheerioAPI, message: MessageSelection, options: IndexedStaticProxyOptions): string { diff --git a/src/styles/content.css b/src/styles/content.css index c94c7cf..258f18f 100644 --- a/src/styles/content.css +++ b/src/styles/content.css @@ -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;