« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/content/dependencies/generateAlbumGalleryTrackGrid.js6
-rw-r--r--src/content/dependencies/generateArtistGalleryPage.js6
-rw-r--r--src/content/dependencies/generateCoverGrid.js38
-rw-r--r--src/content/dependencies/generateGroupGalleryPageAlbumGrid.js18
-rw-r--r--src/data/composite/things/artwork/index.js2
-rw-r--r--src/data/composite/things/artwork/withArtTags.js99
-rw-r--r--src/data/composite/things/artwork/withContentWarningArtTags.js27
-rw-r--r--src/data/things/artwork.js62
-rw-r--r--src/static/css/site.css58
-rw-r--r--src/static/js/client/index.js2
-rw-r--r--src/static/js/client/reveal-all-grid-control.js72
-rw-r--r--src/strings-default.yaml5
12 files changed, 349 insertions, 46 deletions
diff --git a/src/content/dependencies/generateAlbumGalleryTrackGrid.js b/src/content/dependencies/generateAlbumGalleryTrackGrid.js
index fb5ed7ea..86c35b6f 100644
--- a/src/content/dependencies/generateAlbumGalleryTrackGrid.js
+++ b/src/content/dependencies/generateAlbumGalleryTrackGrid.js
@@ -77,6 +77,9 @@ export default {
           ? artwork.artistContribs
               .map(contrib => contrib.artist.name)
           : null)),
+
+    allWarnings:
+      query.artworks.flatMap(artwork => artwork?.contentWarnings),
   }),
 
   slots: {
@@ -117,6 +120,9 @@ export default {
                 artists:
                   language.formatUnitList(artists),
               })),
+
+          revealAllWarnings:
+            data.allWarnings,
         }),
       ]),
 };
diff --git a/src/content/dependencies/generateArtistGalleryPage.js b/src/content/dependencies/generateArtistGalleryPage.js
index 6a24275e..094edc0c 100644
--- a/src/content/dependencies/generateArtistGalleryPage.js
+++ b/src/content/dependencies/generateArtistGalleryPage.js
@@ -58,6 +58,10 @@ export default {
         .map(artwork => artwork.artistContribs
           .filter(contrib => contrib.artist !== artist)
           .map(contrib => contrib.artist.name)),
+
+    allWarnings:
+      query.artworks
+        .flatMap(artwork => artwork.contentWarnings),
   }),
 
   generate: (data, relations, {html, language}) =>
@@ -93,6 +97,8 @@ export default {
 
                     artists: language.formatUnitList(names),
                   })),
+
+              revealAllWarnings: data.allWarnings,
             }),
         ],
 
diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js
index 01613f32..89371015 100644
--- a/src/content/dependencies/generateCoverGrid.js
+++ b/src/content/dependencies/generateCoverGrid.js
@@ -1,4 +1,4 @@
-import {stitchArrays} from '#sugar';
+import {empty, stitchArrays, unique} from '#sugar';
 
 export default {
   contentDependencies: ['generateGridActionLinks'],
@@ -41,6 +41,10 @@ export default {
 
     lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)},
     actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)},
+
+    revealAllWarnings: {
+      validate: v => v.looseArrayOf(v.isString),
+    },
   },
 
   generate: (relations, slots, {html, language}) =>
@@ -49,6 +53,37 @@ export default {
       {[html.onlyIfContent]: true},
 
       [
+        !empty((slots.revealAllWarnings ?? []).filter(Boolean)) &&
+          language.encapsulate('misc.coverGrid.revealAll', capsule =>
+            html.tag('div', {class: 'reveal-all-container'},
+              ((slots.tab ?? [])
+                .slice(0, 4)
+                .some(tab => tab && !html.isBlank(tab))) &&
+
+                {class: 'has-nearby-tab'},
+
+              html.tag('p', {class: 'reveal-all'}, [
+                html.tag('a', {href: '#'}, [
+                  html.tag('span', {class: 'reveal-label'},
+                    language.$(capsule, 'reveal')),
+
+                  html.tag('span', {class: 'conceal-label'},
+                    {style: 'display: none'},
+                    language.$(capsule, 'conceal')),
+                ]),
+
+                html.tag('br'),
+
+                html.tag('span', {class: 'warnings'},
+                  language.$(capsule, 'warnings', {
+                    warnings:
+                      language.formatUnitList(
+                        unique(slots.revealAllWarnings.filter(Boolean))
+                          .sort()
+                          .map(warning => html.tag('b', warning))),
+                  })),
+              ]))),
+
         stitchArrays({
           classes: slots.classes,
           attributes: slots.itemAttributes,
@@ -77,6 +112,7 @@ export default {
 
                 {class: ['grid-item', 'box']},
 
+                tab &&
                 !html.isBlank(tab) &&
                   {class: 'has-tab'},
 
diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js
index 25e57a67..9167a5ad 100644
--- a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js
+++ b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js
@@ -37,6 +37,12 @@ export default {
 
         return album.artistContribs;
       }),
+
+    artworks:
+      albums.map(album =>
+        (album.hasCoverArt
+          ? album.coverArtworks[0]
+          : null)),
   }),
 
   relations: (relation, query, albums, _group) => ({
@@ -52,11 +58,8 @@ export default {
         .map(album => relation('linkAlbum', album)),
 
     images:
-      albums
-        .map(album =>
-          (album.hasCoverArt
-            ? relation('image', album.coverArtworks[0])
-            : relation('image')))
+      query.artworks
+        .map(artwork => relation('image', artwork)),
   }),
 
   data: (query, albums, group) => ({
@@ -69,6 +72,9 @@ export default {
     tracks:
       albums.map(album => album.tracks.length),
 
+    allWarnings:
+      query.artworks.flatMap(artwork => artwork?.contentWarnings),
+
     durations:
       albums.map(album =>
         (album.hideDuration
@@ -141,5 +147,7 @@ export default {
                     time: language.formatDuration(duration),
                   })
                 : null)),
+
+        revealAllWarnings: data.allWarnings,
       })),
 };
diff --git a/src/data/composite/things/artwork/index.js b/src/data/composite/things/artwork/index.js
index 3693c10f..b5e5e167 100644
--- a/src/data/composite/things/artwork/index.js
+++ b/src/data/composite/things/artwork/index.js
@@ -1,5 +1,7 @@
+export {default as withArtTags} from './withArtTags.js';
 export {default as withAttachedArtwork} from './withAttachedArtwork.js';
 export {default as withContainingArtworkList} from './withContainingArtworkList.js';
+export {default as withContentWarningArtTags} from './withContentWarningArtTags.js';
 export {default as withContribsFromAttachedArtwork} from './withContribsFromAttachedArtwork.js';
 export {default as withDate} from './withDate.js';
 export {default as withPropertyFromAttachedArtwork} from './withPropertyFromAttachedArtwork.js';
diff --git a/src/data/composite/things/artwork/withArtTags.js b/src/data/composite/things/artwork/withArtTags.js
new file mode 100644
index 00000000..1fed3c31
--- /dev/null
+++ b/src/data/composite/things/artwork/withArtTags.js
@@ -0,0 +1,99 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency, withResultOfAvailabilityCheck}
+  from '#composite/control-flow';
+import {withPropertyFromObject} from '#composite/data';
+import {withResolvedReferenceList} from '#composite/wiki-data';
+import {soupyFind} from '#composite/wiki-properties';
+
+import withPropertyFromAttachedArtwork
+  from './withPropertyFromAttachedArtwork.js';
+
+export default templateCompositeFrom({
+  annotation: `withArtTags`,
+
+  inputs: {
+    from: input({
+      type: 'array',
+      acceptsNull: true,
+      defaultDependency: 'artTags',
+    }),
+  },
+
+  outputs: ['#artTags'],
+
+  steps: () => [
+    withResolvedReferenceList({
+      list: input('from'),
+      find: soupyFind.input('artTag'),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#resolvedReferenceList',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', '#resolvedReferenceList'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#resolvedReferenceList']: resolvedReferenceList,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({
+              '#artTags': resolvedReferenceList,
+            })
+          : continuation()),
+    },
+
+    withPropertyFromAttachedArtwork({
+      property: input.value('artTags'),
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#attachedArtwork.artTags',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', '#attachedArtwork.artTags'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#attachedArtwork.artTags']: attachedArtworkArtTags,
+      }) =>
+        (availability
+          ? continuation.raiseOutput({
+              '#artTags': attachedArtworkArtTags,
+            })
+          : continuation()),
+    },
+
+    raiseOutputWithoutDependency({
+      dependency: 'artTagsFromThingProperty',
+      output: input.value({'#artTags': []}),
+    }),
+
+    withPropertyFromObject({
+      object: 'thing',
+      property: 'artTagsFromThingProperty',
+    }).outputs({
+      ['#value']: '#thing.artTags',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: '#thing.artTags',
+      mode: input.value('empty'),
+    }),
+
+    {
+      dependencies: ['#availability', '#thing.artTags'],
+      compute: (continuation, {
+        ['#availability']: availability,
+        ['#thing.artTags']: thingArtTags,
+      }) =>
+        (availability
+          ? continuation({'#artTags': thingArtTags})
+          : continuation({'#artTags': []})),
+    },
+  ],
+});
diff --git a/src/data/composite/things/artwork/withContentWarningArtTags.js b/src/data/composite/things/artwork/withContentWarningArtTags.js
new file mode 100644
index 00000000..4c07e837
--- /dev/null
+++ b/src/data/composite/things/artwork/withContentWarningArtTags.js
@@ -0,0 +1,27 @@
+import {input, templateCompositeFrom} from '#composite';
+
+import {withFilteredList, withPropertyFromList} from '#composite/data';
+
+import withArtTags from './withArtTags.js';
+
+export default templateCompositeFrom({
+  annotation: `withContentWarningArtTags`,
+
+  outputs: ['#contentWarningArtTags'],
+
+  steps: () => [
+    withArtTags(),
+
+    withPropertyFromList({
+      list: '#artTags',
+      property: input.value('isContentWarning'),
+    }),
+
+    withFilteredList({
+      list: '#artTags',
+      filter: '#artTags.isContentWarning',
+    }).outputs({
+      '#filteredList': '#contentWarningArtTags',
+    }),
+  ],
+});
diff --git a/src/data/things/artwork.js b/src/data/things/artwork.js
index 4aced874..c54bcced 100644
--- a/src/data/things/artwork.js
+++ b/src/data/things/artwork.js
@@ -25,7 +25,7 @@ import {
   parseDimensions,
 } from '#yaml';
 
-import {withPropertyFromObject} from '#composite/data';
+import {withPropertyFromList, withPropertyFromObject} from '#composite/data';
 
 import {
   exitWithoutDependency,
@@ -55,8 +55,10 @@ import {
 } from '#composite/wiki-properties';
 
 import {
+  withArtTags,
   withAttachedArtwork,
   withContainingArtworkList,
+  withContentWarningArtTags,
   withContribsFromAttachedArtwork,
   withPropertyFromAttachedArtwork,
   withDate,
@@ -209,47 +211,16 @@ export class Artwork extends Thing {
     artTagsFromThingProperty: simpleString(),
 
     artTags: [
-      withResolvedReferenceList({
-        list: input.updateValue({
+      withArtTags({
+        from: input.updateValue({
           validate:
             validateReferenceList(ArtTag[Thing.referenceType]),
         }),
-
-        find: soupyFind.input('artTag'),
-      }),
-
-      exposeDependencyOrContinue({
-        dependency: '#resolvedReferenceList',
-        mode: input.value('empty'),
-      }),
-
-      withPropertyFromAttachedArtwork({
-        property: input.value('artTags'),
-      }),
-
-      exposeDependencyOrContinue({
-        dependency: '#attachedArtwork.artTags',
-      }),
-
-      exitWithoutDependency({
-        dependency: 'artTagsFromThingProperty',
-        value: input.value([]),
-      }),
-
-      withPropertyFromObject({
-        object: 'thing',
-        property: 'artTagsFromThingProperty',
-      }).outputs({
-        ['#value']: '#artTags',
       }),
 
-      exposeDependencyOrContinue({
+      exposeDependency({
         dependency: '#artTags',
       }),
-
-      exposeConstant({
-        value: input.value([]),
-      }),
     ],
 
     referencedArtworksFromThingProperty: simpleString(),
@@ -392,6 +363,27 @@ export class Artwork extends Thing {
         value: input.value([]),
       }),
     ],
+
+    contentWarningArtTags: [
+      withContentWarningArtTags(),
+
+      exposeDependency({
+        dependency: '#contentWarningArtTags',
+      }),
+    ],
+
+    contentWarnings: [
+      withContentWarningArtTags(),
+
+      withPropertyFromList({
+        list: '#contentWarningArtTags',
+        property: input.value('name'),
+      }),
+
+      exposeDependency({
+        dependency: '#contentWarningArtTags.name',
+      }),
+    ],
   });
 
   static [Thing.yamlDocumentSpec] = {
diff --git a/src/static/css/site.css b/src/static/css/site.css
index 5faed373..29c1396a 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -2903,6 +2903,16 @@ img {
   object-fit: cover;
 }
 
+.image {
+  --reveal-filter: ;
+  --shadow-filter: ;
+
+  backdrop-filter: blur(0);
+  filter:
+    var(--reveal-filter)
+    var(--shadow-filter);
+}
+
 p > img, li > img {
   max-width: 100%;
   object-fit: contain;
@@ -2969,9 +2979,9 @@ video.pixelate, .pixelate video {
   text-decoration-style: dotted;
 }
 
-.reveal .image {
+.reveal:not(.revealed) .image {
   opacity: 0.7;
-  filter: blur(20px) brightness(0.7);
+  --reveal-filter: blur(20px) brightness(0.7);
 }
 
 .reveal .image.reveal-thumbnail {
@@ -2995,7 +3005,6 @@ video.pixelate, .pixelate video {
 }
 
 .reveal.revealed .image {
-  filter: none;
   opacity: 1;
 }
 
@@ -3006,7 +3015,6 @@ video.pixelate, .pixelate video {
 .reveal:not(.revealed) .image-outer-area > * {
   --reveal-border-radius: 6px;
   position: relative;
-  overflow: hidden;
   border-radius: var(--reveal-border-radius);
 }
 
@@ -3042,7 +3050,7 @@ video.pixelate, .pixelate video {
 }
 
 .reveal:not(.revealed) .image-outer-area > *:hover .image {
-  filter: blur(20px) brightness(0.6);
+  --reveal-filter: blur(20px) brightness(0.6);
   opacity: 0.6;
 }
 
@@ -3078,6 +3086,40 @@ video.pixelate, .pixelate video {
   border: 1px dashed #fff3;
 }
 
+.grid-listing .reveal-all-container {
+  flex-basis: 100%;
+}
+
+.grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) .reveal-all-container {
+  display: none;
+}
+
+.grid-listing .reveal-all-container.has-nearby-tab {
+  margin-bottom: 0.6em;
+}
+
+.grid-listing .reveal-all {
+  max-width: 400px;
+  margin: 0.20em auto 0;
+  text-align: center;
+}
+
+.grid-listing .reveal-all .warnings:not(.reveal-all:hover *) {
+  opacity: 0.4;
+}
+
+.grid-listing .reveal-all a {
+  display: inline-block;
+  margin-bottom: 0.15em;
+
+  text-decoration: underline;
+  text-decoration-style: dotted;
+}
+
+.grid-listing .reveal-all b {
+  white-space: nowrap;
+}
+
 .grid-item {
   line-height: 1.2;
   font-size: 0.9em;
@@ -3132,10 +3174,16 @@ video.pixelate, .pixelate video {
 }
 
 .grid-item .image {
+  --shadow-filter:
+    drop-shadow(0 3px 2px #0004)
+    drop-shadow(0 1px 5px #0001)
+    drop-shadow(0 3px 4px #0001);
+
   width: 100%;
   height: 100% !important;
   margin-top: auto;
   margin-bottom: auto;
+  object-fit: contain;
 }
 
 .grid-item:hover {
diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js
index 4ca4700e..016ce9ad 100644
--- a/src/static/js/client/index.js
+++ b/src/static/js/client/index.js
@@ -19,6 +19,7 @@ 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';
+import * as revealAllGridControlModule from './reveal-all-grid-control.js';
 import * as scriptedLinkModule from './scripted-link.js';
 import * as sidebarSearchModule from './sidebar-search.js';
 import * as stickyHeadingModule from './sticky-heading.js';
@@ -44,6 +45,7 @@ export const modules = [
   intrapageDotSwitcherModule,
   liveMousePositionModule,
   quickDescriptionModule,
+  revealAllGridControlModule,
   scriptedLinkModule,
   sidebarSearchModule,
   stickyHeadingModule,
diff --git a/src/static/js/client/reveal-all-grid-control.js b/src/static/js/client/reveal-all-grid-control.js
new file mode 100644
index 00000000..1b362bea
--- /dev/null
+++ b/src/static/js/client/reveal-all-grid-control.js
@@ -0,0 +1,72 @@
+/* eslint-env browser */
+
+import {cssProp} from '../client-util.js';
+
+export const info = {
+  id: 'revealAllGridControlInfo',
+
+  revealAllLinks: null,
+  revealables: null,
+
+  revealLabels: null,
+  concealLabels: null,
+};
+
+export function getPageReferences() {
+  info.revealAllLinks =
+    Array.from(document.querySelectorAll('.reveal-all a'));
+
+  info.revealables =
+    info.revealAllLinks
+      .map(link => link.closest('.grid-listing'))
+      .map(listing => listing.querySelectorAll('.reveal'));
+
+  info.revealLabels =
+    info.revealAllLinks
+      .map(link => link.querySelector('.reveal-label'));
+
+  info.concealLabels =
+    info.revealAllLinks
+      .map(link => link.querySelector('.conceal-label'));
+}
+
+export function addPageListeners() {
+  for (const [index, link] of info.revealAllLinks.entries()) {
+    link.addEventListener('click', domEvent => {
+      domEvent.preventDefault();
+      handleRevealAllLinkClicked(index);
+    });
+  }
+}
+
+export function addInternalListeners() {
+  // Don't even think about it. "Reveal all artworks" is a stable control,
+  // meaning it only changes because the user interacted with it directly.
+}
+
+function handleRevealAllLinkClicked(index) {
+  const revealables = info.revealables[index];
+  const revealLabel = info.revealLabels[index];
+  const concealLabel = info.concealLabels[index];
+
+  const shouldReveal =
+    (cssProp(revealLabel, 'display') === 'none'
+      ? false
+      : true);
+
+  for (const revealable of revealables) {
+    if (shouldReveal) {
+      revealable.classList.add('revealed');
+    } else {
+      revealable.classList.remove('revealed');
+    }
+  }
+
+  if (shouldReveal) {
+    cssProp(revealLabel, 'display', 'none');
+    cssProp(concealLabel, 'display', null);
+  } else {
+    cssProp(revealLabel, 'display', null);
+    cssProp(concealLabel, 'display', 'none');
+  }
+}
diff --git a/src/strings-default.yaml b/src/strings-default.yaml
index f7fb1f2b..02e1ee31 100644
--- a/src/strings-default.yaml
+++ b/src/strings-default.yaml
@@ -1002,6 +1002,11 @@ misc:
   #   that thing.
 
   coverGrid:
+    revealAll:
+      reveal: "Reveal all artworks"
+      conceal: "Conceal all artworks"
+      warnings: "In this gallery: {WARNINGS}"
+
     noCoverArt: "{ALBUM}"
 
     tab: