« get me outta code hell

content: generateCoverCarousel - 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-06-23 19:41:15 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-06-23 19:41:38 -0300
commit89e79008b02331b69660bb16b6ca737e37483e61 (patch)
tree90d24e41e72f72bb3e9898d52c19aad323456bdd
parentd7bd80239dead1179450b2a0b97f97c59e150905 (diff)
content: generateCoverCarousel
This also introduces a handy stitchArrays() utility, which
probably has some uses not caught in this commit.
-rw-r--r--src/content/dependencies/generateCoverCarousel.js54
-rw-r--r--src/content/dependencies/generateCoverGrid.js45
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js100
-rw-r--r--src/misc-templates.js83
-rw-r--r--src/util/sugar.js32
-rw-r--r--src/util/wiki-data.js62
6 files changed, 239 insertions, 137 deletions
diff --git a/src/content/dependencies/generateCoverCarousel.js b/src/content/dependencies/generateCoverCarousel.js
new file mode 100644
index 00000000..2a2503ac
--- /dev/null
+++ b/src/content/dependencies/generateCoverCarousel.js
@@ -0,0 +1,54 @@
+import {empty, repeat, stitchArrays} from '../../util/sugar.js';
+import {getCarouselLayoutForNumberOfItems} from '../../util/wiki-data.js';
+
+export default {
+  extraDependencies: ['html'],
+
+  slots: {
+    images: {validate: v => v.arrayOf(v.isHTML)},
+    links: {validate: v => v.arrayOf(v.isHTML)},
+
+    lazy: {validate: v => v.oneOf(v.isWholeNumber, v.isBoolean)},
+  },
+
+  generate(slots, {html}) {
+    const stitched =
+      stitchArrays({
+        image: slots.images,
+        link: slots.links,
+      });
+
+    if (empty(stitched)) {
+      return;
+    }
+
+    const layout = getCarouselLayoutForNumberOfItems(stitched.length);
+
+    return html.tag('div',
+      {
+        class: 'carousel-container',
+        'data-carousel-rows': layout.rows,
+        'data-carousel-columns': layout.columns,
+      },
+      repeat(3, [
+        html.tag('div',
+          {class: 'carousel-grid', 'aria-hidden': 'true'},
+          stitched.map(({image, link}, index) =>
+            html.tag('div', {class: 'carousel-item'},
+              link.slots({
+                attributes: {tabindex: '-1'},
+                content:
+                  image.slots({
+                    thumb: 'small',
+                    square: true,
+                    lazy:
+                      (typeof slots.lazy === 'number'
+                        ? index >= slots.lazy
+                     : typeof slots.lazy === 'boolean'
+                        ? slots.lazy
+                        : false),
+                  }),
+              })))),
+      ]));
+  },
+};
diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js
index a024ae25..970aa05c 100644
--- a/src/content/dependencies/generateCoverGrid.js
+++ b/src/content/dependencies/generateCoverGrid.js
@@ -1,3 +1,5 @@
+import {stitchArrays} from '../../util/sugar.js';
+
 export default {
   extraDependencies: ['html'],
 
@@ -12,27 +14,26 @@ export default {
   generate(slots, {html}) {
     return (
       html.tag('div', {class: 'grid-listing'},
-        slots.images.map((image, i) => {
-          const link = slots.links[i];
-          const name = slots.names[i];
-          return link.slots({
-            content: [
-              image.slots({
-                thumb: 'medium',
-                lazy:
-                  (typeof slots.lazy === 'number'
-                    ? i >= slots.lazy
-                 : typeof slots.lazy === 'boolean'
-                    ? slots.lazy
-                    : false),
-                square: true,
-              }),
-              html.tag('span', name),
-            ],
-            attributes: {
-              class: ['grid-item', 'box', /* large && 'large-grid-item' */],
-            },
-          });
-        })));
+        stitchArrays({
+          image: slots.images,
+          link: slots.links,
+          name: slots.names,
+        }).map(({image, link, name}, index) =>
+            link.slots({
+              attributes: {class: ['grid-item', 'box']},
+              content: [
+                image.slots({
+                  thumb: 'medium',
+                  square: true,
+                  lazy:
+                    (typeof slots.lazy === 'number'
+                      ? index >= slots.lazy
+                   : typeof slots.lazy === 'boolean'
+                      ? slots.lazy
+                      : false),
+                }),
+                html.tag('span', name),
+              ],
+            }))));
   },
 };
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
index 1ec53a0a..168bf799 100644
--- a/src/content/dependencies/generateGroupGalleryPage.js
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -1,4 +1,7 @@
+import {empty, stitchArrays} from '../../util/sugar.js';
+
 import {
+  filterItemsForCarousel,
   getTotalDuration,
   sortChronologically,
 } from '../../util/wiki-data.js';
@@ -6,6 +9,7 @@ import {
 export default {
   contentDependencies: [
     'generateColorStyleRules',
+    'generateCoverCarousel',
     'generateCoverGrid',
     'generateGroupNavLinks',
     'generateGroupSidebar',
@@ -55,14 +59,29 @@ export default {
         relation('linkListing', sprawl.groupsByCategoryListing);
     }
 
+    const carouselAlbums = filterItemsForCarousel(group.featuredAlbums);
+
+    if (!empty(carouselAlbums)) {
+      relations.coverCarousel =
+        relation('generateCoverCarousel');
+
+      relations.carouselLinks =
+        carouselAlbums
+          .map(album => relation('linkAlbum', album));
+
+      relations.carouselImages =
+        carouselAlbums
+          .map(album => relation('image', album.artTags));
+    }
+
     relations.coverGrid =
       relation('generateCoverGrid');
 
-    relations.links =
+    relations.gridLinks =
       albums
         .map(album => relation('linkAlbum', album));
 
-    relations.images =
+    relations.gridImages =
       albums.map(album =>
         (album.hasCoverArt
           ? relation('image', album.artTags)
@@ -72,25 +91,35 @@ export default {
   },
 
   data(sprawl, group) {
-    const albums =
-      sortChronologically(group.albums.slice(), {latestFirst: true});
+    const data = {};
 
-    const tracks = albums.flatMap((album) => album.tracks);
-    const totalDuration = getTotalDuration(tracks, {originalReleasesOnly: true});
+    data.name = group.name;
 
-    return {
-      name: group.name,
+    const albums = sortChronologically(group.albums.slice(), {latestFirst: true});
+    const tracks = albums.flatMap((album) => album.tracks);
 
-      numAlbums: albums.length,
-      numTracks: tracks.length,
-      totalDuration,
+    data.numAlbums = albums.length;
+    data.numTracks = tracks.length;
+    data.totalDuration = getTotalDuration(tracks, {originalReleasesOnly: true});
 
-      names: albums.map(album => album.name),
-      paths: albums.map(album =>
+    data.gridNames = albums.map(album => album.name);
+    data.gridPaths =
+      albums.map(album =>
         (album.hasCoverArt
           ? ['media.albumCover', album.directory, album.coverArtFileExtension]
-          : null)),
-    };
+          : null));
+
+    const carouselAlbums = filterItemsForCarousel(group.featuredAlbums);
+
+    if (!empty(group.featuredAlbums)) {
+      data.carouselPaths =
+        carouselAlbums.map(album =>
+          (album.hasCoverArt
+            ? ['media.albumCover', album.directory, album.coverArtFileExtension]
+            : null));
+    }
+
+    return data;
   },
 
   generate(data, relations, {html, language}) {
@@ -103,13 +132,16 @@ export default {
 
         mainClasses: ['top-index'],
         mainContent: [
-          /*
-          getCarouselHTML({
-            items: group.featuredAlbums.slice(0, 12 + 1),
-            srcFn: getAlbumCover,
-            linkFn: link.album,
-          }),
-          */
+          relations.coverCarousel
+            ?.slots({
+              links: relations.carouselLinks,
+              images:
+                stitchArrays({
+                  image: relations.carouselImages,
+                  path: data.carouselPaths,
+                }).map(({image, path}) =>
+                    image.slot('path', path)),
+            }),
 
           html.tag('p',
             {class: 'quick-info'},
@@ -139,17 +171,21 @@ export default {
 
           relations.coverGrid
             .slots({
-              links: relations.links,
-              names: data.names,
+              links: relations.gridLinks,
+              names: data.gridNames,
               images:
-                relations.images.map((image, i) =>
-                  image.slots({
-                    path: data.paths[i],
-                    missingSourceContent:
-                      language.$('misc.albumGrid.noCoverArt', {
-                        album: data.names[i],
-                      }),
-                  })),
+                stitchArrays({
+                  image: relations.gridImages,
+                  path: data.gridPaths,
+                  name: data.gridNames,
+                }).map(({image, path, name}) =>
+                    image.slots({
+                      path,
+                      missingSourceContent:
+                        language.$('misc.albumGrid.noCoverArt', {
+                          album: name,
+                        }),
+                    })),
             }),
         ],
 
diff --git a/src/misc-templates.js b/src/misc-templates.js
index dfff4d88..ba1a60f1 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -99,48 +99,6 @@ function unbound_getFlashGridHTML({
 
 // Carousel reels
 
-// Layout constants:
-//
-// Carousels support fitting 4-18 items, with a few "dead" zones to watch out
-// for, namely when a multiple of 6, 5, or 4 columns would drop the last tiles.
-//
-// Carousels are limited to 1-3 rows and 4-6 columns.
-// Lower edge case: 1-3 items are treated as 4 items (with blank space).
-// Upper edge case: all items past 18 are dropped (treated as 18 items).
-//
-// This is all done through JS instead of CSS because it's just... ANNOYING...
-// to write a mapping like this in CSS lol.
-const carouselLayoutMap = [
-  // 0-3
-  null, null, null, null,
-
-  // 4-6
-  {rows: 1, columns: 4}, //  4: 1x4, drop 0
-  {rows: 1, columns: 5}, //  5: 1x5, drop 0
-  {rows: 1, columns: 6}, //  6: 1x6, drop 0
-
-  // 7-12
-  {rows: 1, columns: 6}, //  7: 1x6, drop 1
-  {rows: 2, columns: 4}, //  8: 2x4, drop 0
-  {rows: 2, columns: 4}, //  9: 2x4, drop 1
-  {rows: 2, columns: 5}, // 10: 2x5, drop 0
-  {rows: 2, columns: 5}, // 11: 2x5, drop 1
-  {rows: 2, columns: 6}, // 12: 2x6, drop 0
-
-  // 13-18
-  {rows: 2, columns: 6}, // 13: 2x6, drop 1
-  {rows: 2, columns: 6}, // 14: 2x6, drop 2
-  {rows: 3, columns: 5}, // 15: 3x5, drop 0
-  {rows: 3, columns: 5}, // 16: 3x5, drop 1
-  {rows: 3, columns: 5}, // 17: 3x5, drop 2
-  {rows: 3, columns: 6}, // 18: 3x6, drop 0
-];
-
-const minCarouselLayoutItems = carouselLayoutMap.findIndex(x => x !== null);
-const maxCarouselLayoutItems = carouselLayoutMap.length - 1;
-const shortestCarouselLayout = carouselLayoutMap[minCarouselLayoutItems];
-const longestCarouselLayout = carouselLayoutMap[maxCarouselLayoutItems];
-
 function unbound_getCarouselHTML({
   html,
   img,
@@ -152,47 +110,6 @@ function unbound_getCarouselHTML({
   linkFn = (x, {text}) => text,
   srcFn,
 }) {
-  if (empty(items)) {
-    return;
-  }
-
-  const {rows, columns} = (
-    items.length < minCarouselLayoutItems ? shortestCarouselLayout :
-    items.length > maxCarouselLayoutItems ? longestCarouselLayout :
-    carouselLayoutMap[items.length]);
-
-  items = items.slice(0, maxCarouselLayoutItems + 1);
-
-  return html.tag('div',
-    {
-      class: 'carousel-container',
-      'data-carousel-rows': rows,
-      'data-carousel-columns': columns,
-    },
-    repeat(3,
-      html.tag('div',
-        {
-          class: 'carousel-grid',
-          'aria-hidden': 'true',
-        },
-        items
-          .filter(item => srcFn(item))
-          .filter(item => item.artTags.every(tag => !tag.isContentWarning))
-          .map((item, i) =>
-            html.tag('div', {class: 'carousel-item'},
-              linkFn(item, {
-                attributes: {
-                  tabindex: '-1',
-                },
-                text:
-                  img({
-                    src: srcFn(item),
-                    alt: altFn(item),
-                    thumb: 'small',
-                    lazy: typeof lazy === 'number' ? i >= lazy : lazy,
-                    square: true,
-                  }),
-              }))))));
 }
 
 // Exports
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 3a7e6f82..5f54c3b9 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -73,6 +73,38 @@ export function accumulateSum(array, fn = x => x) {
     0);
 }
 
+// Stitches together the items of separate arrays into one array of objects
+// whose keys are the corresponding items from each array at that index.
+// This is mostly useful for iterating over multiple arrays at once!
+export function stitchArrays(keyToArray) {
+  const errors = [];
+
+  for (const [key, value] of Object.entries(keyToArray)) {
+    if (!Array.isArray(value)) {
+      errors.push(new TypeError(`(${key}) Expected array, got ${value}`));
+    }
+  }
+
+  if (!empty(errors)) {
+    throw new AggregateError(errors, `Expected all values to be arrays`);
+  }
+
+  const keys = Object.keys(keyToArray);
+  const arrays = Object.values(keyToArray);
+  const length = Math.max(...arrays.map(({length}) => length));
+  const results = [];
+
+  for (let i = 0; i < length; i++) {
+    const object = {};
+    for (const key of keys) {
+      object[key] = keyToArray[key][i];
+    }
+    results.push(object);
+  }
+
+  return results;
+}
+
 export const mapInPlace = (array, fn) =>
   array.splice(0, array.length, ...array.map(fn));
 
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index 89c621c5..8a2897d2 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -554,3 +554,65 @@ export function getNewReleases(numReleases, {wikiData}) {
     .slice(0, numReleases)
     .map((album) => ({item: album}));
 }
+
+// Carousel layout and utilities
+
+// Layout constants:
+//
+// Carousels support fitting 4-18 items, with a few "dead" zones to watch out
+// for, namely when a multiple of 6, 5, or 4 columns would drop the last tiles.
+//
+// Carousels are limited to 1-3 rows and 4-6 columns.
+// Lower edge case: 1-3 items are treated as 4 items (with blank space).
+// Upper edge case: all items past 18 are dropped (treated as 18 items).
+//
+// This is all done through JS instead of CSS because it's just... ANNOYING...
+// to write a mapping like this in CSS lol.
+const carouselLayoutMap = [
+  // 0-3
+  null, null, null, null,
+
+  // 4-6
+  {rows: 1, columns: 4}, //  4: 1x4, drop 0
+  {rows: 1, columns: 5}, //  5: 1x5, drop 0
+  {rows: 1, columns: 6}, //  6: 1x6, drop 0
+
+  // 7-12
+  {rows: 1, columns: 6}, //  7: 1x6, drop 1
+  {rows: 2, columns: 4}, //  8: 2x4, drop 0
+  {rows: 2, columns: 4}, //  9: 2x4, drop 1
+  {rows: 2, columns: 5}, // 10: 2x5, drop 0
+  {rows: 2, columns: 5}, // 11: 2x5, drop 1
+  {rows: 2, columns: 6}, // 12: 2x6, drop 0
+
+  // 13-18
+  {rows: 2, columns: 6}, // 13: 2x6, drop 1
+  {rows: 2, columns: 6}, // 14: 2x6, drop 2
+  {rows: 3, columns: 5}, // 15: 3x5, drop 0
+  {rows: 3, columns: 5}, // 16: 3x5, drop 1
+  {rows: 3, columns: 5}, // 17: 3x5, drop 2
+  {rows: 3, columns: 6}, // 18: 3x6, drop 0
+];
+
+const minCarouselLayoutItems = carouselLayoutMap.findIndex(x => x !== null);
+const maxCarouselLayoutItems = carouselLayoutMap.length - 1;
+const shortestCarouselLayout = carouselLayoutMap[minCarouselLayoutItems];
+const longestCarouselLayout = carouselLayoutMap[maxCarouselLayoutItems];
+
+export function getCarouselLayoutForNumberOfItems(numItems) {
+  return (
+    numItems < minCarouselLayoutItems ? shortestCarouselLayout :
+    numItems > maxCarouselLayoutItems ? longestCarouselLayout :
+    carouselLayoutMap[numItems]);
+}
+
+export function filterItemsForCarousel(items) {
+  if (empty(items)) {
+    return [];
+  }
+
+  return items
+    .filter(item => item.hasCoverArt)
+    .filter(item => item.artTags.every(tag => !tag.isContentWarning))
+    .slice(0, maxCarouselLayoutItems + 1);
+}