diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2024-11-30 18:07:37 -0400 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2024-11-30 18:07:37 -0400 |
commit | 611c82294aecf54dd365aebcd4d34d0eda9a5f0a (patch) | |
tree | ab7747b554c85e810d84d10b2ce4314dc9314a36 /src | |
parent | dda1966d87ffc88bea11f9efdca6bfe617839e57 (diff) |
client: image-overlay: be a client module
Diffstat (limited to 'src')
-rw-r--r-- | src/static/js/client/image-overlay.js | 306 | ||||
-rw-r--r-- | src/static/js/client/index.js | 3 | ||||
-rw-r--r-- | src/static/js/image-overlay.js | 256 |
3 files changed, 308 insertions, 257 deletions
diff --git a/src/static/js/client/image-overlay.js b/src/static/js/client/image-overlay.js new file mode 100644 index 00000000..bdfefeea --- /dev/null +++ b/src/static/js/client/image-overlay.js @@ -0,0 +1,306 @@ +/* eslint-env browser */ + +import {cssProp} from '../client-util.js'; +import {fetchWithProgress} from '../xhr-util.js'; + +export const info = { + id: 'imageOverlayInfo', + + container: null, + actionContainer: null, + + viewOriginalLinks: null, + mainImage: null, + thumbImage: null, + + actionContentWithoutSize: null, + actionContentWithSize: null, + + megabytesContainer: null, + kilobytesContainer: null, + megabytesContent: null, + kilobytesContent: null, + fileSizeWarning: null, + + links: null, +}; + +export function getPageReferences() { + info.container = + document.getElementById('image-overlay-container'); + + if (!info.container) return; + + info.actionContainer = + document.getElementById('image-overlay-action-container'); + + info.viewOriginalLinks = + document.getElementsByClassName('image-overlay-view-original'); + + info.mainImage = + document.getElementById('image-overlay-image'); + + info.thumbImage = + document.getElementById('image-overlay-image-thumb'); + + info.actionContentWithoutSize = + document.getElementById('image-overlay-action-content-without-size'); + + info.actionContentWithSize = + document.getElementById('image-overlay-action-content-with-size'); + + info.megabytesContainer = + document.getElementById('image-overlay-file-size-megabytes'); + + info.kilobytesContainer = + document.getElementById('image-overlay-file-size-kilobytes'); + + info.megabytesContent = + info.megabytesContainer.querySelector('.image-overlay-file-size-count'); + + info.kilobytesContent = + info.kilobytesContainer.querySelector('.image-overlay-file-size-count'); + + info.fileSizeWarning = + document.getElementById('image-overlay-file-size-warning'); + + info.links = + Array.from(document.querySelectorAll('.image-link')) + .filter(link => !link.closest('.no-image-preview')); +} + +export function addPageListeners() { + if (!info.container) return; + + for (const link of info.links) { + link.addEventListener('click', handleImageLinkClicked); + } + + info.container.addEventListener('click', handleContainerClicked); + document.body.addEventListener('keydown', handleKeyDown); +} + +function handleContainerClicked(evt) { + // Only hide the image overlay if actually clicking the background. + if (evt.target !== info.container) { + return; + } + + // If you clicked anything close to or beneath the action bar, don't hide + // the image overlay. + const rect = info.actionContainer.getBoundingClientRect(); + if (evt.clientY >= rect.top - 40) { + return; + } + + info.container.classList.remove('visible'); +} + +function handleKeyDown(evt) { + if (evt.key === 'Escape' || evt.key === 'Esc' || evt.keyCode === 27) { + info.container.classList.remove('visible'); + } +} + +async function handleImageLinkClicked(evt) { + if (evt.metaKey || evt.shiftKey || evt.altKey) { + return; + } + + evt.preventDefault(); + + // Don't show the overlay if the image still needs to be revealed. + if (evt.target.closest('.reveal:not(.revealed)')) { + return; + } + + info.container.classList.add('visible'); + info.container.classList.remove('loaded'); + info.container.classList.remove('errored'); + + const details = getImageLinkDetails(evt.target); + + updateFileSizeInformation(details.originalFileSize); + + let mainSrc = null; + let thumbSrc = null; + + if (details.availableThumbList) { + const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(details.availableThumbList); + const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(details.availableThumbList); + mainSrc = details.embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${mainThumb}.jpg`); + thumbSrc = details.embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${smallThumb}.jpg`); + // Show the thumbnail size on each <img> element's data attributes. + // Y'know, just for debugging convenience. + info.mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`; + info.thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`; + } else { + mainSrc = details.originalSrc; + thumbSrc = null; + info.mainImage.dataset.displayingThumb = ''; + info.thumbImage.dataset.displayingThumb = ''; + } + + for (const link of info.viewOriginalLinks) { + link.href = details.originalSrc; + } + + await loadOverlayImage(mainSrc, thumbSrc); +} + +function getImageLinkDetails(imageLink) { + const a = imageLink.closest('a'); + const img = a.querySelector('img'); + + return { + originalSrc: + a.href, + + embeddedSrc: + img.src, + + originalFileSize: + img.dataset.originalSize, + + availableThumbList: + img.dataset.thumbs, + }; +} + +async function loadOverlayImage(mainSrc, thumbSrc) { + if (thumbSrc) { + info.thumbImage.src = thumbSrc; + info.thumbImage.style.display = null; + } else { + info.thumbImage.src = ''; + info.thumbImage.style.display = 'none'; + } + + info.mainImage.addEventListener('load', handleMainImageLoaded); + info.mainImage.addEventListener('error', handleMainImageErrored); + + const showProgress = amount => { + cssProp(info.container, '--download-progress', `${amount * 100}%`); + }; + + showProgress(0.00); + + const response = + await fetchWithProgress(mainSrc, progress => { + if (progress === -1) { + // TODO: Indeterminate response progress cue + showProgress(0.00); + } else { + showProgress(0.20 + 0.80 * progress); + } + }); + + if (!response.status.toString().startsWith('2')) { + handleMainImageErrored(); + return; + } + + const blob = await response.blob(); + const blobSrc = URL.createObjectURL(blob); + + info.mainImage.src = blobSrc; + showProgress(1.00); + + function handleMainImageLoaded() { + info.container.classList.add('loaded'); + removeEventListeners(); + } + + function handleMainImageErrored() { + info.container.classList.add('errored'); + removeEventListeners(); + } + + function removeEventListeners() { + info.mainImage.removeEventListener('load', handleMainImageLoaded); + info.mainImage.removeEventListener('error', handleMainImageErrored); + } +} + +function parseThumbList(availableThumbList) { + // Parse all the available thumbnail sizes! These are provided by the actual + // content generation on each image. + const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250' + const availableSizes = + (availableThumbList || defaultThumbList) + .split(' ') + .map(part => part.split(':')) + .map(([thumb, length]) => ({thumb, length: parseInt(length)})) + .sort((a, b) => a.length - b.length); + + return availableSizes; +} + +function getPreferredThumbSize(availableThumbList) { + // Assuming a square, the image will be constrained to the lesser window + // dimension. Coefficient here matches CSS dimensions for image overlay. + const constrainedLength = Math.floor(Math.min( + 0.80 * window.innerWidth, + 0.80 * window.innerHeight)); + + // Match device pixel ratio, which is 2x for "retina" displays and certain + // device configurations. + const visualLength = window.devicePixelRatio * constrainedLength; + + const availableSizes = parseThumbList(availableThumbList); + + // Starting from the smallest dimensions, find (and return) the first + // available length which hits a "good enough" threshold - it's got to be + // at least that percent of the way to the actual displayed dimensions. + const goodEnoughThreshold = 0.90; + + // (The last item is skipped since we'd be falling back to it anyway.) + for (const {thumb, length} of availableSizes.slice(0, -1)) { + if (Math.floor(visualLength * goodEnoughThreshold) <= length) { + return {thumb, length}; + } + } + + // If none of the items in the list were big enough to hit the "good enough" + // threshold, just use the largest size available. + return availableSizes[availableSizes.length - 1]; +} + +function getSmallestThumbSize(availableThumbList) { + // Just snag the smallest size. This'll be used for displaying the "preview" + // as the bigger one is loading. + const availableSizes = parseThumbList(availableThumbList); + return availableSizes[0]; +} + +function updateFileSizeInformation(fileSize) { + const fileSizeWarningThreshold = 8 * 10 ** 6; + + if (!fileSize) { + info.actionContentWithSize.classList.remove('visible'); + info.actionContentWithoutSize.classList.add('visible'); + return; + } + + info.actionContentWithoutSize.classList.remove('visible'); + info.actionContentWithSize.classList.add('visible'); + + fileSize = parseInt(fileSize); + const round = (exp) => Math.round(fileSize / 10 ** (exp - 1)) / 10; + + if (fileSize > fileSizeWarningThreshold) { + info.fileSizeWarning.classList.add('visible'); + } else { + info.fileSizeWarning.classList.remove('visible'); + } + + if (fileSize > 10 ** 6) { + info.megabytesContainer.classList.add('visible'); + info.kilobytesContainer.classList.remove('visible'); + info.megabytesContent.innerText = round(6); + } else { + info.megabytesContainer.classList.remove('visible'); + info.kilobytesContainer.classList.add('visible'); + info.kilobytesContent.innerText = round(3); + } +} diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 1c291bfc..e2e5b75b 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -1,7 +1,6 @@ /* eslint-env browser */ import '../group-contributions-table.js'; -import '../image-overlay.js'; import * as additionalNamesBoxModule from './additional-names-box.js'; import * as albumCommentarySidebarModule from './album-commentary-sidebar.js'; @@ -10,6 +9,7 @@ import * as cssCompatibilityAssistantModule from './css-compatibility-assistant. import * as datetimestampTooltipModule from './datetimestamp-tooltip.js'; import * as hashLinkModule from './hash-link.js'; import * as hoverableTooltipModule from './hoverable-tooltip.js'; +import * as imageOverlayModule from './image-overlay.js'; import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js'; import * as liveMousePositionModule from './live-mouse-position.js'; import * as quickDescriptionModule from './quick-description.js'; @@ -28,6 +28,7 @@ export const modules = [ datetimestampTooltipModule, hashLinkModule, hoverableTooltipModule, + imageOverlayModule, intrapageDotSwitcherModule, liveMousePositionModule, quickDescriptionModule, diff --git a/src/static/js/image-overlay.js b/src/static/js/image-overlay.js deleted file mode 100644 index 1eee60e5..00000000 --- a/src/static/js/image-overlay.js +++ /dev/null @@ -1,256 +0,0 @@ -/* eslint-env browser */ - -import {cssProp} from './client-util.js'; -import {fetchWithProgress} from './xhr-util.js'; - -// TODO: Update to clientSteps style. - -function addImageOverlayClickHandlers() { - const container = document.getElementById('image-overlay-container'); - - if (!container) { - console.warn(`#image-overlay-container missing, image overlay module disabled.`); - return; - } - - for (const link of document.querySelectorAll('.image-link')) { - if (link.closest('.no-image-preview')) { - continue; - } - - link.addEventListener('click', handleImageLinkClicked); - } - - const actionContainer = document.getElementById('image-overlay-action-container'); - - container.addEventListener('click', handleContainerClicked); - document.body.addEventListener('keydown', handleKeyDown); - - function handleContainerClicked(evt) { - // Only hide the image overlay if actually clicking the background. - if (evt.target !== container) { - return; - } - - // If you clicked anything close to or beneath the action bar, don't hide - // the image overlay. - const rect = actionContainer.getBoundingClientRect(); - if (evt.clientY >= rect.top - 40) { - return; - } - - container.classList.remove('visible'); - } - - function handleKeyDown(evt) { - if (evt.key === 'Escape' || evt.key === 'Esc' || evt.keyCode === 27) { - container.classList.remove('visible'); - } - } -} - -async function handleImageLinkClicked(evt) { - if (evt.metaKey || evt.shiftKey || evt.altKey) { - return; - } - - evt.preventDefault(); - - // Don't show the overlay if the image still needs to be revealed. - if (evt.target.closest('.reveal:not(.revealed)')) { - return; - } - - const container = document.getElementById('image-overlay-container'); - container.classList.add('visible'); - container.classList.remove('loaded'); - container.classList.remove('errored'); - - const allViewOriginal = document.getElementsByClassName('image-overlay-view-original'); - const mainImage = document.getElementById('image-overlay-image'); - const thumbImage = document.getElementById('image-overlay-image-thumb'); - - const {href: originalSrc} = evt.target.closest('a'); - - const { - src: embeddedSrc, - dataset: { - originalSize: originalFileSize, - thumbs: availableThumbList, - }, - } = evt.target.closest('a').querySelector('img'); - - updateFileSizeInformation(originalFileSize); - - let mainSrc = null; - let thumbSrc = null; - - if (availableThumbList) { - const {thumb: mainThumb, length: mainLength} = getPreferredThumbSize(availableThumbList); - const {thumb: smallThumb, length: smallLength} = getSmallestThumbSize(availableThumbList); - mainSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${mainThumb}.jpg`); - thumbSrc = embeddedSrc.replace(/\.[a-z]+\.(jpg|png)$/, `.${smallThumb}.jpg`); - // Show the thumbnail size on each <img> element's data attributes. - // Y'know, just for debugging convenience. - mainImage.dataset.displayingThumb = `${mainThumb}:${mainLength}`; - thumbImage.dataset.displayingThumb = `${smallThumb}:${smallLength}`; - } else { - mainSrc = originalSrc; - thumbSrc = null; - mainImage.dataset.displayingThumb = ''; - thumbImage.dataset.displayingThumb = ''; - } - - if (thumbSrc) { - thumbImage.src = thumbSrc; - thumbImage.style.display = null; - } else { - thumbImage.src = ''; - thumbImage.style.display = 'none'; - } - - for (const viewOriginal of allViewOriginal) { - viewOriginal.href = originalSrc; - } - - mainImage.addEventListener('load', handleMainImageLoaded); - mainImage.addEventListener('error', handleMainImageErrored); - - const showProgress = amount => { - cssProp(container, '--download-progress', `${amount * 100}%`); - }; - - showProgress(0.00); - - const response = - await fetchWithProgress(mainSrc, progress => { - if (progress === -1) { - // TODO: Indeterminate response progress cue - showProgress(0.00); - } else { - showProgress(0.20 + 0.80 * progress); - } - }); - - if (!response.status.toString().startsWith('2')) { - handleMainImageErrored(); - return; - } - - const blob = await response.blob(); - const blobSrc = URL.createObjectURL(blob); - - mainImage.src = blobSrc; - showProgress(1.00); - - function handleMainImageLoaded() { - container.classList.add('loaded'); - removeEventListeners(); - } - - function handleMainImageErrored() { - container.classList.add('errored'); - removeEventListeners(); - } - - function removeEventListeners() { - mainImage.removeEventListener('load', handleMainImageLoaded); - mainImage.removeEventListener('error', handleMainImageErrored); - } -} - -function parseThumbList(availableThumbList) { - // Parse all the available thumbnail sizes! These are provided by the actual - // content generation on each image. - const defaultThumbList = 'huge:1400 semihuge:1200 large:800 medium:400 small:250' - const availableSizes = - (availableThumbList || defaultThumbList) - .split(' ') - .map(part => part.split(':')) - .map(([thumb, length]) => ({thumb, length: parseInt(length)})) - .sort((a, b) => a.length - b.length); - - return availableSizes; -} - -function getPreferredThumbSize(availableThumbList) { - // Assuming a square, the image will be constrained to the lesser window - // dimension. Coefficient here matches CSS dimensions for image overlay. - const constrainedLength = Math.floor(Math.min( - 0.80 * window.innerWidth, - 0.80 * window.innerHeight)); - - // Match device pixel ratio, which is 2x for "retina" displays and certain - // device configurations. - const visualLength = window.devicePixelRatio * constrainedLength; - - const availableSizes = parseThumbList(availableThumbList); - - // Starting from the smallest dimensions, find (and return) the first - // available length which hits a "good enough" threshold - it's got to be - // at least that percent of the way to the actual displayed dimensions. - const goodEnoughThreshold = 0.90; - - // (The last item is skipped since we'd be falling back to it anyway.) - for (const {thumb, length} of availableSizes.slice(0, -1)) { - if (Math.floor(visualLength * goodEnoughThreshold) <= length) { - return {thumb, length}; - } - } - - // If none of the items in the list were big enough to hit the "good enough" - // threshold, just use the largest size available. - return availableSizes[availableSizes.length - 1]; -} - -function getSmallestThumbSize(availableThumbList) { - // Just snag the smallest size. This'll be used for displaying the "preview" - // as the bigger one is loading. - const availableSizes = parseThumbList(availableThumbList); - return availableSizes[0]; -} - -function updateFileSizeInformation(fileSize) { - const fileSizeWarningThreshold = 8 * 10 ** 6; - - const actionContentWithoutSize = document.getElementById('image-overlay-action-content-without-size'); - const actionContentWithSize = document.getElementById('image-overlay-action-content-with-size'); - - if (!fileSize) { - actionContentWithSize.classList.remove('visible'); - actionContentWithoutSize.classList.add('visible'); - return; - } - - actionContentWithoutSize.classList.remove('visible'); - actionContentWithSize.classList.add('visible'); - - const megabytesContainer = document.getElementById('image-overlay-file-size-megabytes'); - const kilobytesContainer = document.getElementById('image-overlay-file-size-kilobytes'); - const megabytesContent = megabytesContainer.querySelector('.image-overlay-file-size-count'); - const kilobytesContent = kilobytesContainer.querySelector('.image-overlay-file-size-count'); - const fileSizeWarning = document.getElementById('image-overlay-file-size-warning'); - - fileSize = parseInt(fileSize); - const round = (exp) => Math.round(fileSize / 10 ** (exp - 1)) / 10; - - if (fileSize > fileSizeWarningThreshold) { - fileSizeWarning.classList.add('visible'); - } else { - fileSizeWarning.classList.remove('visible'); - } - - if (fileSize > 10 ** 6) { - megabytesContainer.classList.add('visible'); - kilobytesContainer.classList.remove('visible'); - megabytesContent.innerText = round(6); - } else { - megabytesContainer.classList.remove('visible'); - kilobytesContainer.classList.add('visible'); - kilobytesContent.innerText = round(3); - } - - void fileSizeWarning; -} - -addImageOverlayClickHandlers(); |