« get me outta code hell

display original file size in image overlay - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-03-01 21:11:36 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-03-01 21:11:36 -0400
commit62cd6e574b89a5bfa75dc52ef2383fddf5cbc87a (patch)
treed0c79910842e977847b125fed8c8d7170e630b25
parent62f64b3aa019747e9c764eda853591b321850ca0 (diff)
display original file size in image overlay
-rw-r--r--src/gen-thumbs.js56
-rw-r--r--src/misc-templates.js9
-rw-r--r--src/static/client.js53
-rw-r--r--src/static/site3.css16
-rw-r--r--src/strings-default.json4
-rwxr-xr-xsrc/upd8.js40
-rw-r--r--src/util/html.js8
-rw-r--r--src/util/node-utils.js22
-rw-r--r--src/write/bind-utilities.js4
-rw-r--r--src/write/build-modes/live-dev-server.js2
-rw-r--r--src/write/build-modes/static-build.js2
-rw-r--r--src/write/page-template.js35
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')),
+        ]),
       ]),
     ]));