From 62cd6e574b89a5bfa75dc52ef2383fddf5cbc87a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Wed, 1 Mar 2023 21:11:36 -0400 Subject: display original file size in image overlay --- src/gen-thumbs.js | 56 +++++++++++++------------------- src/misc-templates.js | 9 +++++ src/static/client.js | 53 ++++++++++++++++++++++++++++-- src/static/site3.css | 16 ++++++++- src/strings-default.json | 4 ++- src/upd8.js | 40 +++++++++++++++++++---- src/util/html.js | 8 +++-- src/util/node-utils.js | 22 +++++++++++++ src/write/bind-utilities.js | 4 +++ src/write/build-modes/live-dev-server.js | 2 ++ src/write/build-modes/static-build.js | 2 ++ src/write/page-template.js | 35 ++++++++++++++++---- 12 files changed, 197 insertions(+), 54 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 89223778..21402cb6 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -77,11 +77,17 @@ const CACHE_FILE = 'thumbnail-cache.json'; const WARNING_DELAY_TIME = 10000; +const thumbnailSpec = { + 'huge': {size: 1600, quality: 95}, + 'medium': {size: 400, quality: 95}, + 'small': {size: 250, quality: 85}, +}; + import {spawn} from 'child_process'; import {createHash} from 'crypto'; import * as path from 'path'; -import {readdir, readFile, writeFile} from 'fs/promises'; // Whatcha know! Nice. +import {readFile, writeFile} from 'fs/promises'; // Whatcha know! Nice. import {createReadStream} from 'fs'; // Still gotta import from 8oth tho, for createReadStream. @@ -93,30 +99,15 @@ import { progressPromiseAll, } from './util/cli.js'; -import {commandExists, isMain, promisifyProcess} from './util/node-utils.js'; +import { + commandExists, + isMain, + promisifyProcess, + traverse, +} from './util/node-utils.js'; import {delay, queue} from './util/sugar.js'; -function traverse(startDirPath, { - filterFile = () => true, - filterDir = () => true -} = {}) { - const recursive = (names, subDirPath) => - Promise.all( - names.map((name) => - readdir(path.join(startDirPath, subDirPath, name)).then( - (names) => - filterDir(name) - ? recursive(names, path.join(subDirPath, name)) - : [], - () => (filterFile(name) ? [path.join(subDirPath, name)] : []) - ) - ) - ).then((pathArrays) => pathArrays.flatMap((x) => x)); - - return readdir(startDirPath).then((names) => recursive(names, '')); -} - function readFileMD5(filePath) { return new Promise((resolve, reject) => { const md5 = createHash('md5'); @@ -190,11 +181,10 @@ function generateImageThumbnails(filePath, {spawnConvert}) { output(name), ]); - return Promise.all([ - promisifyProcess(convert('.huge', {size: 1800, quality: 96}), false), - promisifyProcess(convert('.medium', {size: 400, quality: 95}), false), - promisifyProcess(convert('.small', {size: 250, quality: 85}), false), - ]); + return Promise.all( + Object.entries(thumbnailSpec) + .map(([ext, details]) => + promisifyProcess(convert('.' + ext, details), false))); } export default async function genThumbs(mediaPath, { @@ -208,14 +198,9 @@ export default async function genThumbs(mediaPath, { const quietInfo = quiet ? () => null : logInfo; const filterFile = (name) => { - // TODO: Why is this not working???????? - // thumbnail-cache.json is 8eing passed through, for some reason. - const ext = path.extname(name); if (ext !== '.jpg' && ext !== '.png') return false; - - const rest = path.basename(name, ext); - if (rest.endsWith('.medium') || rest.endsWith('.small')) return false; + if (isThumb(name)) return false; return true; }; @@ -371,6 +356,11 @@ export default async function genThumbs(mediaPath, { return true; } +export function isThumb(file) { + const thumbnailLabel = file.match(/\.([^.]+)\.[^.]+$/)?.[1]; + return Object.keys(thumbnailSpec).includes(thumbnailLabel); +} + if (isMain(import.meta.url)) { (async function () { const miscOptions = await parseOptions(process.argv.slice(2), { diff --git a/src/misc-templates.js b/src/misc-templates.js index 171b4825..e8c7496c 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -637,7 +637,9 @@ function unbound_getFlashGridHTML({ // Images function unbound_img({ + getSizeOfImageFile, html, + to, src, alt, @@ -652,6 +654,12 @@ function unbound_img({ lazy = false, square = false, }) { + let fileSize = null; + const mediaRoot = to('media.root'); + if (src.startsWith(mediaRoot)) { + fileSize = getSizeOfImageFile(src.slice(mediaRoot.length).replace(/^\//, '')); + } + const willSquare = square; const willLink = typeof link === 'string' || link; @@ -664,6 +672,7 @@ function unbound_img({ alt, width, height, + 'data-original-size': fileSize, }; const noSrcHTML = diff --git a/src/static/client.js b/src/static/client.js index 47936d82..af358210 100644 --- a/src/static/client.js +++ b/src/static/client.js @@ -670,14 +670,19 @@ function handleImageLinkClicked(evt) { container.classList.remove('loaded'); container.classList.remove('errored'); - const viewOriginal = document.getElementById('image-overlay-view-original'); + const allViewOriginal = document.getElementsByClassName('image-overlay-view-original'); const mainImage = document.getElementById('image-overlay-image'); const thumbImage = document.getElementById('image-overlay-image-thumb'); const source = evt.target.closest('a').href; mainImage.src = source.replace(/\.(jpg|png)$/, '.huge.jpg'); thumbImage.src = source.replace(/\.(jpg|png)$/, '.small.jpg'); - viewOriginal.href = source; + for (const viewOriginal of allViewOriginal) { + viewOriginal.href = source; + } + + const fileSize = evt.target.closest('a').querySelector('img').dataset.originalSize; + updateFileSizeInformation(fileSize); mainImage.addEventListener('load', handleMainImageLoaded); mainImage.addEventListener('error', handleMainImageErrored); @@ -695,4 +700,48 @@ function handleImageLinkClicked(evt) { } } +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; + console.log(round(3)); + + 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(); diff --git a/src/static/site3.css b/src/static/site3.css index 449e6fad..484c9f9d 100644 --- a/src/static/site3.css +++ b/src/static/site3.css @@ -1391,6 +1391,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r #image-overlay-image-thumb { filter: blur(16px); + transform: scale(1.5); } #image-overlay-container.loaded #image-overlay-image-thumb { @@ -1400,7 +1401,7 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r } #image-overlay-action-container { - padding: 8px 4px 6px 4px; + padding: 4px 4px 6px 4px; border-radius: 0 0 5px 5px; background: var(--bg-black-color); color: white; @@ -1408,6 +1409,19 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r text-align: center; } +#image-overlay-container #image-overlay-action-content-without-size:not(.visible), +#image-overlay-container #image-overlay-action-content-with-size:not(.visible), +#image-overlay-container #image-overlay-file-size-warning:not(.visible), +#image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), +#image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { + display: none; +} + +#image-overlay-file-size-warning { + opacity: 0.8; + font-size: 0.9em; +} + /* important easter egg mode */ html[data-language-code="preview-en"][data-url-key="localized.home"] #content diff --git a/src/strings-default.json b/src/strings-default.json index bfe358e4..c79c00cf 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -97,7 +97,9 @@ "releaseInfo.viewGallery": "View {LINK}!", "releaseInfo.viewGallery.link": "gallery page", "releaseInfo.viewOriginalFile": "View {LINK}.", - "releaseInfo.viewOriginalFile.link":" original file", + "releaseInfo.viewOriginalFile.withSize": "View {LINK} ({SIZE}).", + "releaseInfo.viewOriginalFile.link": "original file", + "releaseInfo.viewOriginalFile.sizeWarning": "(Heads up! If you're on a mobile plan, this is a large download.)", "releaseInfo.listenOn": "Listen on {LINKS}.", "releaseInfo.listenOn.noLinks": "This track has no URLs at which it can be listened.", "releaseInfo.visitOn": "Visit on {LINKS}.", diff --git a/src/upd8.js b/src/upd8.js index fd565228..2daa5f7d 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -36,7 +36,7 @@ import * as path from 'path'; import {fileURLToPath} from 'url'; import wrap from 'word-wrap'; -import genThumbs from './gen-thumbs.js'; +import genThumbs, {isThumb} from './gen-thumbs.js'; import {listingSpec, listingTargetSpec} from './listing-spec.js'; import urlSpec from './url-spec.js'; @@ -56,7 +56,7 @@ import { import find from './util/find.js'; import {findFiles} from './util/io.js'; import link from './util/link.js'; -import {isMain} from './util/node-utils.js'; +import {isMain, traverse} from './util/node-utils.js'; import {validateReplacerSpec} from './util/replacer.js'; import {empty, showAggregate, withEntries} from './util/sugar.js'; import {replacerSpec} from './util/transform-content.js'; @@ -648,18 +648,43 @@ async function main() { ), ]; - const getSizeOfAdditionalFile = (mediaPath) => { - const {device} = - additionalFilePaths.find(({media}) => media === mediaPath) || {}; - if (!device) return null; - return fileSizePreloader.getSizeOfPath(device); + // Same dealio for images. Since just about any image can be embedded and + // we can't super easily know which ones are referenced at runtime, just + // cheat and get file sizes for all images under media. (This includes + // additional files which are images.) + const imageFilePaths = (await traverse(mediaPath, { + filterDir: dir => dir !== '.git', + filterFile: file => ( + ['.png', '.gif', '.jpg'].includes(path.extname(file)) && + !isThumb(file)), + })) + .map(file => ({ + device: path.join(mediaPath, file), + media: + urls + .from('media.root') + .to('media.path', file.split(path.sep).join('/')), + })); + + const getSizeOfMediaFileHelper = paths => (mediaPath) => { + const pair = paths.find(({media}) => media === mediaPath); + if (!pair) return null; + return fileSizePreloader.getSizeOfPath(pair.device); }; + const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths); + const getSizeOfImageFile = getSizeOfMediaFileHelper(imageFilePaths); + logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`; fileSizePreloader.loadPaths(...additionalFilePaths.map((path) => path.device)); await fileSizePreloader.waitUntilDoneLoading(); + logInfo`Preloading filesizes for ${imageFilePaths.length} full-resolution images...`; + + fileSizePreloader.loadPaths(...imageFilePaths.map((path) => path.device)); + await fileSizePreloader.waitUntilDoneLoading(); + logInfo`Done preloading filesizes!`; if (noBuild) return; @@ -686,6 +711,7 @@ async function main() { cachebust: '?' + CACHEBUST, developersComment, getSizeOfAdditionalFile, + getSizeOfImageFile, }); } diff --git a/src/util/html.js b/src/util/html.js index 459a1647..1c55fb8c 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -63,9 +63,11 @@ export function tag(tagName, ...args) { const joiner = attrs?.[joinChildren]; content = content.filter(Boolean).join( - (joiner - ? `\n${joiner}\n` - : '\n')); + (joiner === '' + ? '' + : (joiner + ? `\n${joiner}\n` + : '\n'))); } if (attrs?.[onlyIfContent] && !content) { diff --git a/src/util/node-utils.js b/src/util/node-utils.js index 76684827..6c75bab6 100644 --- a/src/util/node-utils.js +++ b/src/util/node-utils.js @@ -1,5 +1,6 @@ // Utility functions which are only relevant to particular Node.js constructs. +import {readdir} from 'fs/promises'; import {fileURLToPath} from 'url'; import * as path from 'path'; @@ -54,3 +55,24 @@ export function isMain(importMetaURL) { isIndexJS && 'index.js' ].includes(relative); } + +// Like readdir... but it's recursive! +export function traverse(startDirPath, { + filterFile = () => true, + filterDir = () => true +} = {}) { + const recursive = (names, subDirPath) => + Promise.all( + names.map((name) => + readdir(path.join(startDirPath, subDirPath, name)).then( + (names) => + filterDir(name) + ? recursive(names, path.join(subDirPath, name)) + : [], + () => (filterFile(name) ? [path.join(subDirPath, name)] : []) + ) + ) + ).then((pathArrays) => pathArrays.flatMap((x) => x)); + + return readdir(startDirPath).then((names) => recursive(names, '')); +} diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 427111b4..993aa3ce 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -55,6 +55,7 @@ export function bindUtilities({ absoluteTo, defaultLanguage, getSizeOfAdditionalFile, + getSizeOfImageFile, language, languages, to, @@ -70,6 +71,7 @@ export function bindUtilities({ absoluteTo, defaultLanguage, getSizeOfAdditionalFile, + getSizeOfImageFile, html, language, languages, @@ -80,7 +82,9 @@ export function bindUtilities({ bound.img = bindOpts(img, { [bindOpts.bindIndex]: 0, + getSizeOfImageFile, html, + to, }); bound.getColors = bindOpts(getColors, { diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index a8fd3705..dfebda0e 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -63,6 +63,7 @@ export async function go({ cachebust, developersComment, getSizeOfAdditionalFile, + getSizeOfImageFile, }) { const host = cliOptions['host'] ?? defaultHost; const port = parseInt(cliOptions['port'] ?? defaultPort); @@ -313,6 +314,7 @@ export async function go({ absoluteTo, defaultLanguage, getSizeOfAdditionalFile, + getSizeOfImageFile, language, languages, to, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index fa724536..8e02342c 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -95,6 +95,7 @@ export async function go({ cachebust, developersComment, getSizeOfAdditionalFile, + getSizeOfImageFile, }) { const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; const appendIndexHTML = cliOptions['append-index-html'] ?? false; @@ -304,6 +305,7 @@ export async function go({ absoluteTo, defaultLanguage, getSizeOfAdditionalFile, + getSizeOfImageFile, language, languages, to, diff --git a/src/write/page-template.js b/src/write/page-template.js index bd52c456..e0b37d49 100644 --- a/src/write/page-template.js +++ b/src/write/page-template.js @@ -6,7 +6,6 @@ import {getColors} from '../util/colors.js'; import { getFooterLocalizationLinks, getRevealStringFromContentWarningMessage, - img, } from '../misc-templates.js'; export function generateDevelopersCommentHTML({ @@ -51,6 +50,7 @@ export function generateDocumentHTML(pageInfo, { developersComment, generateCoverLink, generateStickyHeadingContainer, + img, getThemeString, language, languages, @@ -487,7 +487,6 @@ export function generateDocumentHTML(pageInfo, { html.tag('div', {id: 'info-card'}, [ html.tag('div', {class: ['info-card-art-container', 'no-reveal']}, img({ - html, class: 'info-card-art', src: '', link: true, @@ -495,7 +494,6 @@ export function generateDocumentHTML(pageInfo, { })), html.tag('div', {class: ['info-card-art-container', 'reveal']}, img({ - html, class: 'info-card-art', src: '', link: true, @@ -527,10 +525,33 @@ export function generateDocumentHTML(pageInfo, { html.tag('img', {id: 'image-overlay-image-thumb'}), ]), html.tag('div', {id: 'image-overlay-action-container'}, [ - language.$('releaseInfo.viewOriginalFile', { - link: html.tag('a', {id: 'image-overlay-view-original'}, - language.$('releaseInfo.viewOriginalFile.link')), - }), + html.tag('div', {id: 'image-overlay-action-content-without-size'}, + language.$('releaseInfo.viewOriginalFile', { + link: html.tag('a', {class: 'image-overlay-view-original'}, + language.$('releaseInfo.viewOriginalFile.link')), + })), + + html.tag('div', {id: 'image-overlay-action-content-with-size'}, [ + language.$('releaseInfo.viewOriginalFile.withSize', { + link: html.tag('a', {class: 'image-overlay-view-original'}, + language.$('releaseInfo.viewOriginalFile.link')), + size: html.tag('span', + {[html.joinChildren]: ''}, + [ + html.tag('span', {id: 'image-overlay-file-size-kilobytes'}, + language.$('count.fileSize.kilobytes', { + kilobytes: html.tag('span', {class: 'image-overlay-file-size-count'}), + })), + html.tag('span', {id: 'image-overlay-file-size-megabytes'}, + language.$('count.fileSize.megabytes', { + megabytes: html.tag('span', {class: 'image-overlay-file-size-count'}), + })), + ]), + }), + + html.tag('span', {id: 'image-overlay-file-size-warning'}, + language.$('releaseInfo.viewOriginalFile.sizeWarning')), + ]), ]), ])); -- cgit 1.3.0-6-gf8a5