« get me outta code hell

implement flash act pages, rework flash sidebar layout - 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-10-11 14:32:28 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-10-11 14:44:34 -0300
commit3a871cf43a11b87392d26320c736b516925da684 (patch)
tree113eb17e10d4f542366578111bdaa169985911be
parent3cd6f9edc58171e33ed6af565db84113e2488f25 (diff)
implement flash act pages, rework flash sidebar layout
-rw-r--r--src/content/dependencies/generateFlashActGalleryPage.js91
-rw-r--r--src/content/dependencies/generateFlashActNavAccent.js74
-rw-r--r--src/content/dependencies/generateFlashActSidebar.js209
-rw-r--r--src/content/dependencies/generateFlashIndexPage.js21
-rw-r--r--src/content/dependencies/generateFlashInfoPage.js17
-rw-r--r--src/content/dependencies/generateFlashNavAccent.js7
-rw-r--r--src/content/dependencies/generateFlashSidebar.js236
-rw-r--r--src/content/dependencies/linkFlashAct.js14
-rw-r--r--src/content/dependencies/listArtTagNetwork.js1
-rw-r--r--src/data/things/flash.js6
-rw-r--r--src/data/yaml.js4
-rw-r--r--src/find.js5
-rw-r--r--src/page/flash-act.js23
-rw-r--r--src/page/index.js1
-rw-r--r--src/static/site5.css2
-rw-r--r--src/strings-default.json6
-rw-r--r--src/url-spec.js2
17 files changed, 452 insertions, 267 deletions
diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js
new file mode 100644
index 0000000..8eea58b
--- /dev/null
+++ b/src/content/dependencies/generateFlashActGalleryPage.js
@@ -0,0 +1,91 @@
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generateCoverGrid',
+    'generateFlashActNavAccent',
+    'generateFlashActSidebar',
+    'generatePageLayout',
+    'image',
+    'linkFlash',
+    'linkFlashIndex',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations: (relation, act) => ({
+    layout:
+      relation('generatePageLayout'),
+
+    flashIndexLink:
+      relation('linkFlashIndex'),
+
+    flashActNavAccent:
+      relation('generateFlashActNavAccent', act),
+
+    sidebar:
+      relation('generateFlashActSidebar', act, null),
+
+    coverGrid:
+      relation('generateCoverGrid'),
+
+    coverGridImages:
+      act.flashes
+        .map(_flash => relation('image')),
+
+    flashLinks:
+      act.flashes
+        .map(flash => relation('linkFlash', flash)),
+  }),
+
+  data: (act) => ({
+    name: act.name,
+    color: act.color,
+
+    flashNames:
+      act.flashes.map(flash => flash.name),
+
+    flashCoverPaths:
+      act.flashes.map(flash =>
+        ['media.flashArt', flash.directory, flash.coverArtFileExtension])
+  }),
+
+  generate(data, relations, {html, language}) {
+    return relations.layout.slots({
+      title:
+        language.$('flashPage.title', {
+          flash: new html.Tag(null, null, data.name),
+        }),
+
+      color: data.color,
+      headingMode: 'static',
+
+      mainClasses: ['flash-index'],
+      mainContent: [
+        relations.coverGrid.slots({
+          links: relations.flashLinks,
+          names: data.flashNames,
+          lazy: 6,
+
+          images:
+            stitchArrays({
+              image: relations.coverGridImages,
+              path: data.flashCoverPaths,
+            }).map(({image, path}) =>
+                image.slot('path', path)),
+        }),
+      ],
+
+      navLinkStyle: 'hierarchical',
+      navLinks: [
+        {auto: 'home'},
+        {html: relations.flashIndexLink},
+        {auto: 'current'},
+      ],
+
+      navBottomRowContent: relations.flashActNavAccent,
+
+      ...relations.sidebar,
+    });
+  },
+};
diff --git a/src/content/dependencies/generateFlashActNavAccent.js b/src/content/dependencies/generateFlashActNavAccent.js
new file mode 100644
index 0000000..9850438
--- /dev/null
+++ b/src/content/dependencies/generateFlashActNavAccent.js
@@ -0,0 +1,74 @@
+import {empty} from '#sugar';
+
+export default {
+  contentDependencies: [
+    'generatePreviousNextLinks',
+    'linkFlashAct',
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl({flashActData}) {
+    return {flashActData};
+  },
+
+  query(sprawl, flashAct) {
+    // Like with generateFlashNavAccent, don't sort chronologically here.
+    const flashActs =
+      sprawl.flashActData;
+
+    const index = flashActs.indexOf(flashAct);
+
+    const previousFlashAct =
+      (index > 0
+        ? flashActs[index - 1]
+        : null);
+
+    const nextFlashAct =
+      (index < flashActs.length - 1
+        ? flashActs[index + 1]
+        : null);
+
+    return {previousFlashAct, nextFlashAct};
+  },
+
+  relations(relation, query) {
+    const relations = {};
+
+    if (query.previousFlashAct || query.nextFlashAct) {
+      relations.previousNextLinks =
+        relation('generatePreviousNextLinks');
+
+      relations.previousFlashActLink =
+        (query.previousFlashAct
+          ? relation('linkFlashAct', query.previousFlashAct)
+          : null);
+
+      relations.nextFlashActLink =
+        (query.nextFlashAct
+          ? relation('linkFlashAct', query.nextFlashAct)
+          : null);
+    }
+
+    return relations;
+  },
+
+  generate(relations, {html, language}) {
+    const {content: previousNextLinks = []} =
+      relations.previousNextLinks &&
+        relations.previousNextLinks.slots({
+          previousLink: relations.previousFlashActLink,
+          nextLink: relations.nextFlashActLink,
+        });
+
+    const allLinks = [
+      ...previousNextLinks,
+    ].filter(Boolean);
+
+    if (empty(allLinks)) {
+      return html.blank();
+    }
+
+    return `(${language.formatUnitList(allLinks)})`;
+  },
+};
diff --git a/src/content/dependencies/generateFlashActSidebar.js b/src/content/dependencies/generateFlashActSidebar.js
new file mode 100644
index 0000000..ff5dc04
--- /dev/null
+++ b/src/content/dependencies/generateFlashActSidebar.js
@@ -0,0 +1,209 @@
+import find from '#find';
+import {stitchArrays} from '#sugar';
+
+export default {
+  contentDependencies: ['linkFlash', 'linkFlashAct', 'linkFlashIndex'],
+  extraDependencies: ['getColors', 'html', 'language', 'wikiData'],
+
+  // So help me Gog, the flash sidebar is heavily hard-coded.
+
+  sprawl: ({flashActData}) => ({flashActData}),
+
+  query(sprawl, act, flash) {
+    const findFlashAct = directory =>
+      find.flashAct(directory, sprawl.flashActData, {mode: 'error'});
+
+    const sideFirstActs = [
+      findFlashAct('flash-act:a1'),
+      findFlashAct('flash-act:a6a1'),
+      findFlashAct('flash-act:hiveswap'),
+      findFlashAct('flash-act:cool-and-new-web-comic'),
+      findFlashAct('flash-act:psycholonials'),
+    ];
+
+    const sideNames = [
+      `Side 1 (Acts 1-5)`,
+      `Side 2 (Acts 6-7)`,
+      `Additional Canon`,
+      `Fan Adventures`,
+      `More Flashes & Games`,
+    ];
+
+    const sideColors = [
+      '#4ac925',
+      '#3796c6',
+      '#f2a400',
+      '#c466ff',
+      '#32c7fe',
+    ];
+
+    const sideFirstActIndexes =
+      sideFirstActs
+        .map(act => sprawl.flashActData.indexOf(act));
+
+    const actSideIndexes =
+      sprawl.flashActData
+        .map((act, actIndex) => actIndex)
+        .map(actIndex =>
+          sideFirstActIndexes
+            .findIndex((firstActIndex, i) =>
+              i === sideFirstActs.length - 1 ||
+                firstActIndex <= actIndex &&
+                sideFirstActIndexes[i + 1] > actIndex));
+
+    const sideActs =
+      sideNames
+        .map((name, sideIndex) =>
+          stitchArrays({
+            act: sprawl.flashActData,
+            actSideIndex: actSideIndexes,
+          }).filter(({actSideIndex}) => actSideIndex === sideIndex)
+            .map(({act}) => act));
+
+    const currentActFlashes =
+      act.flashes;
+
+    const currentFlashIndex =
+      currentActFlashes.indexOf(flash);
+
+    const currentSideIndex =
+      actSideIndexes[sprawl.flashActData.indexOf(act)];
+
+    const currentSideActs =
+      sideActs[currentSideIndex];
+
+    const currentActIndex =
+      currentSideActs.indexOf(act);
+
+    const visualNovelActs = [
+      findFlashAct('flash-act:friendsim'),
+      findFlashAct('flash-act:pesterquest'),
+      findFlashAct('flash-act:psycholonials'),
+    ];
+
+    const gameSeriesActs = [
+      findFlashAct('flash-act:hiveswap'),
+    ];
+
+    const listTerminology =
+      (visualNovelActs.includes(act)
+        ? 'volumesInThisGame'
+     : gameSeriesActs.includes(act)
+        ? 'gamesInThisSeries'
+     : act === findFlashAct('flash-act:other-fan-adventures')
+        ? 'flashesInThisSection'
+     : currentSideIndex <= 1
+        ? 'flashesInThisAct'
+     : currentSideIndex === 3
+        ? 'flashesInThisStory'
+        : 'entriesInThisSection');
+
+    return {
+      sideNames,
+      sideColors,
+      sideActs,
+
+      currentSideIndex,
+      currentSideActs,
+      currentActIndex,
+      currentActFlashes,
+      currentFlashIndex,
+
+      listTerminology,
+    };
+  },
+
+  relations: (relation, query, sprawl, act, _flash) => ({
+    currentActLink:
+      relation('linkFlashAct', act),
+
+    flashIndexLink:
+      relation('linkFlashIndex'),
+
+    sideActLinks:
+      query.sideActs
+        .map(acts => acts
+          .map(act => relation('linkFlashAct', act))),
+
+    currentActFlashLinks:
+      act.flashes
+        .map(flash => relation('linkFlash', flash)),
+  }),
+
+  data: (query, sprawl, act, flash) => ({
+    isFlashActPage: !flash,
+
+    sideColors: query.sideColors,
+    sideNames: query.sideNames,
+
+    currentSideIndex: query.currentSideIndex,
+    currentActIndex: query.currentActIndex,
+    currentFlashIndex: query.currentFlashIndex,
+
+    listTerminology: query.listTerminology,
+  }),
+
+  generate(data, relations, {getColors, html, language}) {
+    const currentActBox = html.tags([
+      html.tag('h1', relations.currentActLink),
+
+      html.tag('details',
+        (data.isFlashActPage
+          ? {}
+          : {class: 'current', open: true}),
+        [
+          html.tag('summary',
+            html.tag('span', {class: 'group-name'},
+              language.$('flashSidebar.flashList', data.listTerminology))),
+
+          html.tag('ul',
+            relations.currentActFlashLinks
+              .map((flashLink, index) =>
+                html.tag('li',
+                  {class: index === data.currentFlashIndex && 'current'},
+                  flashLink))),
+        ]),
+    ]);
+
+    const sideMapBox = html.tags([
+      html.tag('h1', relations.flashIndexLink),
+
+      stitchArrays({
+        sideName: data.sideNames,
+        sideColor: data.sideColors,
+        actLinks: relations.sideActLinks,
+      }).map(({sideName, sideColor, actLinks}, sideIndex) =>
+          html.tag('details', {
+            class: sideIndex === data.currentSideIndex && 'current',
+            open: data.isFlashActPage && sideIndex === data.currentSideIndex,
+            style: sideColor && `--primary-color: ${getColors(sideColor).primary}`
+          }, [
+            html.tag('summary',
+              html.tag('span', {class: 'group-name'},
+                sideName)),
+
+            html.tag('ul',
+              actLinks.map((actLink, actIndex) =>
+                html.tag('li',
+                  {class:
+                    sideIndex === data.currentSideIndex &&
+                    actIndex === data.currentActIndex &&
+                      'current'},
+                  actLink))),
+          ])),
+    ]);
+
+    return {
+      leftSidebarMultiple:
+        (data.isFlashActPage
+          ? [
+              {content: sideMapBox},
+              {content: currentActBox},
+            ]
+          : [
+              {content: currentActBox},
+              {content: sideMapBox},
+            ]),
+    };
+  },
+};
diff --git a/src/content/dependencies/generateFlashIndexPage.js b/src/content/dependencies/generateFlashIndexPage.js
index 66588fd..ad1dab9 100644
--- a/src/content/dependencies/generateFlashIndexPage.js
+++ b/src/content/dependencies/generateFlashIndexPage.js
@@ -7,6 +7,7 @@ export default {
     'generatePageLayout',
     'image',
     'linkFlash',
+    'linkFlashAct',
   ],
 
   extraDependencies: ['html', 'language', 'wikiData'],
@@ -36,9 +37,9 @@ export default {
       query.flashActs
         .map(() => relation('generateColorStyleVariables')),
 
-    actFirstFlashLinks:
+    actLinks:
       query.flashActs
-        .map(act => relation('linkFlash', act.flashes[0])),
+        .map(act => relation('linkFlashAct', act)),
 
     actCoverGrids:
       query.flashActs
@@ -58,7 +59,7 @@ export default {
   data: (query) => ({
     jumpLinkAnchors:
       query.jumpActs
-        .map(act => act.anchor),
+        .map(act => act.directory),
 
     jumpLinkColors:
       query.jumpActs
@@ -70,16 +71,12 @@ export default {
 
     actAnchors:
       query.flashActs
-        .map(act => act.anchor),
+        .map(act => act.directory),
 
     actColors:
       query.flashActs
         .map(act => act.color),
 
-    actNames:
-      query.flashActs
-        .map(act => act.name),
-
     actCoverGridNames:
       query.flashActs
         .map(act => act.flashes
@@ -118,10 +115,9 @@ export default {
 
         stitchArrays({
           colorVariables: relations.actColorVariables,
-          firstFlashLink: relations.actFirstFlashLinks,
+          actLink: relations.actLinks,
           anchor: data.actAnchors,
           color: data.actColors,
-          name: data.actNames,
 
           coverGrid: relations.actCoverGrids,
           coverGridImages: relations.actCoverGridImages,
@@ -132,8 +128,7 @@ export default {
             colorVariables,
             anchor,
             color,
-            name,
-            firstFlashLink,
+            actLink,
 
             coverGrid,
             coverGridImages,
@@ -146,7 +141,7 @@ export default {
                 id: anchor,
                 style: colorVariables.slot('color', color).content,
               },
-              firstFlashLink.slot('content', name)),
+              actLink),
 
             coverGrid.slots({
               links: coverGridLinks,
diff --git a/src/content/dependencies/generateFlashInfoPage.js b/src/content/dependencies/generateFlashInfoPage.js
index 553d2f5..09c6b37 100644
--- a/src/content/dependencies/generateFlashInfoPage.js
+++ b/src/content/dependencies/generateFlashInfoPage.js
@@ -4,13 +4,13 @@ export default {
   contentDependencies: [
     'generateContentHeading',
     'generateContributionList',
+    'generateFlashActSidebar',
     'generateFlashCoverArtwork',
     'generateFlashNavAccent',
-    'generateFlashSidebar',
     'generatePageLayout',
     'generateTrackList',
     'linkExternal',
-    'linkFlashIndex',
+    'linkFlashAct',
   ],
 
   extraDependencies: ['html', 'language'],
@@ -41,7 +41,7 @@ export default {
       relation('generatePageLayout');
 
     relations.sidebar =
-      relation('generateFlashSidebar', flash);
+      relation('generateFlashActSidebar', flash.act, flash);
 
     if (query.urls) {
       relations.externalLinks =
@@ -59,8 +59,8 @@ export default {
 
     const nav = sections.nav = {};
 
-    nav.flashIndexLink =
-      relation('linkFlashIndex');
+    nav.flashActLink =
+      relation('linkFlashAct', flash.act);
 
     nav.flashNavAccent =
       relation('generateFlashNavAccent', flash);
@@ -163,14 +163,11 @@ export default {
       navLinkStyle: 'hierarchical',
       navLinks: [
         {auto: 'home'},
-        {html: sec.nav.flashIndexLink},
+        {html: sec.nav.flashActLink.slot('color', false)},
         {auto: 'current'},
       ],
 
-      navBottomRowContent:
-        sec.nav.flashNavAccent.slots({
-          showFlashNavigation: true,
-        }),
+      navBottomRowContent: sec.nav.flashNavAccent,
 
       ...relations.sidebar,
     });
diff --git a/src/content/dependencies/generateFlashNavAccent.js b/src/content/dependencies/generateFlashNavAccent.js
index 2c8205d..57196d0 100644
--- a/src/content/dependencies/generateFlashNavAccent.js
+++ b/src/content/dependencies/generateFlashNavAccent.js
@@ -55,13 +55,8 @@ export default {
     return relations;
   },
 
-  slots: {
-    showFlashNavigation: {type: 'boolean', default: false},
-  },
-
-  generate(relations, slots, {html, language}) {
+  generate(relations, {html, language}) {
     const {content: previousNextLinks = []} =
-      slots.showFlashNavigation &&
       relations.previousNextLinks &&
         relations.previousNextLinks.slots({
           previousLink: relations.previousFlashLink,
diff --git a/src/content/dependencies/generateFlashSidebar.js b/src/content/dependencies/generateFlashSidebar.js
deleted file mode 100644
index ba76192..0000000
--- a/src/content/dependencies/generateFlashSidebar.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import {stitchArrays} from '#sugar';
-
-export default {
-  contentDependencies: ['linkFlash', 'linkFlashIndex'],
-  extraDependencies: ['html', 'wikiData'],
-
-  // So help me Gog, the flash sidebar is heavily hard-coded.
-
-  sprawl: ({flashActData}) => ({flashActData}),
-
-  query(sprawl, flash) {
-    const flashActs =
-      sprawl.flashActData.slice();
-
-    const act6 =
-      flashActs
-        .findIndex(act => act.name.startsWith('Act 6'));
-
-    const postCanon =
-      flashActs
-        .findIndex(act => act.name.includes('Post Canon'));
-
-    const outsideCanon =
-      postCanon +
-      flashActs
-        .slice(postCanon)
-        .findIndex(act => !act.name.includes('Post Canon'));
-
-    const currentAct = flash.act;
-
-    const actIndex =
-      flashActs
-        .indexOf(currentAct);
-
-    const side =
-      (actIndex < 0
-        ? 0
-     : actIndex < act6
-        ? 1
-     : actIndex < outsideCanon
-        ? 2
-        : 3);
-
-    const sideActs =
-      flashActs
-        .filter((act, index) =>
-          act.name.startsWith('Act 1') ||
-          act.name.startsWith('Act 6 Act 1') ||
-          act.name.startsWith('Hiveswap') ||
-          index >= outsideCanon);
-
-    const currentSideIndex =
-      sideActs
-        .findIndex(act => {
-          if (act.name.startsWith('Act 1')) {
-            return side === 1;
-          } else if (act.name.startsWith('Act 6 Act 1')) {
-            return side === 2;
-          } else if (act.name.startsWith('Hiveswap Act 1')) {
-            return side === 3;
-          } else {
-            return act === currentAct;
-          }
-        })
-
-    const sideNames =
-      sideActs
-        .map(act => {
-          if (act.name.startsWith('Act 1')) {
-            return `Side 1 (Acts 1-5)`;
-          } else if (act.name.startsWith('Act 6 Act 1')) {
-            return `Side 2 (Acts 6-7)`;
-          } else if (act.name.startsWith('Hiveswap Act 1')) {
-            return `Outside Canon (Misc. Games)`;
-          } else {
-            return act.name;
-          }
-        });
-
-    const sideColors =
-      sideActs
-        .map(act => {
-          if (act.name.startsWith('Act 1')) {
-            return '#4ac925';
-          } else if (act.name.startsWith('Act 6 Act 1')) {
-            return '#1076a2';
-          } else if (act.name.startsWith('Hiveswap Act 1')) {
-            return '#008282';
-          } else {
-            return act.color;
-          }
-        });
-
-    const sideFirstFlashes =
-      sideActs
-        .map(act => act.flashes[0]);
-
-    const scopeActs =
-      flashActs
-        .filter((act, index) => {
-          if (index < act6) {
-            return side === 1;
-          } else if (index < outsideCanon) {
-            return side === 2;
-          } else {
-            return false;
-          }
-        });
-
-    const currentScopeActIndex =
-      scopeActs.indexOf(currentAct);
-
-    const scopeActNames =
-      scopeActs
-        .map(act => act.name);
-
-    const scopeActFirstFlashes =
-      scopeActs
-        .map(act => act.flashes[0]);
-
-    const currentActFlashes =
-      currentAct.flashes;
-
-    const currentFlashIndex =
-      currentActFlashes
-        .indexOf(flash);
-
-    return {
-      currentSideIndex,
-      sideNames,
-      sideColors,
-      sideFirstFlashes,
-
-      currentScopeActIndex,
-      scopeActNames,
-      scopeActFirstFlashes,
-
-      currentActFlashes,
-      currentFlashIndex,
-    };
-  },
-
-  relations: (relation, query) => ({
-    flashIndexLink:
-      relation('linkFlashIndex'),
-
-    sideFirstFlashLinks:
-      query.sideFirstFlashes
-        .map(flash => relation('linkFlash', flash)),
-
-    scopeActFirstFlashLinks:
-      query.scopeActFirstFlashes
-        .map(flash => relation('linkFlash', flash)),
-
-    currentActFlashLinks:
-      query.currentActFlashes
-        .map(flash => relation('linkFlash', flash)),
-  }),
-
-  data: (query) => ({
-    currentSideIndex: query.currentSideIndex,
-    sideColors: query.sideColors,
-    sideNames: query.sideNames,
-
-    currentScopeActIndex: query.currentScopeActIndex,
-    scopeActNames: query.scopeActNames,
-
-    currentFlashIndex: query.currentFlashIndex,
-  }),
-
-  generate(data, relations, {html}) {
-    const currentActFlashList =
-      html.tag('ul',
-        relations.currentActFlashLinks
-          .map((flashLink, index) =>
-            html.tag('li',
-              {class: index === data.currentFlashIndex && 'current'},
-              flashLink)));
-
-    return {
-      leftSidebarContent: html.tags([
-        html.tag('h1', relations.flashIndexLink),
-
-        html.tag('dl',
-          stitchArrays({
-            sideFirstFlashLink: relations.sideFirstFlashLinks,
-            sideColor: data.sideColors,
-            sideName: data.sideNames,
-          }).map(({sideFirstFlashLink, sideColor, sideName}, index) => [
-              // Side acts are displayed whether part of Homestuck proper or
-              // not, and they're always the same regardless the current flash
-              // page. Scope acts, if applicable, and the list of flashes
-              // belonging to the current act, will be inserted after the
-              // heading of the current side.
-              html.tag('dt',
-                {class: [
-                  'side',
-                  index === data.currentSideIndex && 'current',
-                ]},
-                sideFirstFlashLink.slots({
-                  color: sideColor,
-                  content: sideName,
-                })),
-
-              // Scope acts are only applicable when inside Homestuck proper.
-              // Hiveswap and all acts beyond are each considered to be its
-              // own "side".
-              index === data.currentSideIndex &&
-              data.currentScopeActIndex !== -1 &&
-                stitchArrays({
-                  scopeActFirstFlashLink: relations.scopeActFirstFlashLinks,
-                  scopeActName: data.scopeActNames,
-                }).map(({scopeActFirstFlashLink, scopeActName}, index) => [
-                    html.tag('dt',
-                      {class: index === data.currentScopeActIndex && 'current'},
-                      scopeActFirstFlashLink.slot('content', scopeActName)),
-
-                    // Inside Homestuck proper, the flash list of the current
-                    // act should show after the heading for the relevant
-                    // scope act.
-                    index === data.currentScopeActIndex &&
-                      html.tag('dd', currentActFlashList),
-                  ]),
-
-              // Outside of Homestuck proper, the current act is represented
-              // by a side instead of a scope act, so place its flash list
-              // after the heading for the relevant side.
-              index === data.currentSideIndex &&
-              data.currentScopeActIndex === -1 &&
-                html.tag('dd', currentActFlashList),
-            ])),
-
-      ]),
-    };
-  },
-};
diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js
new file mode 100644
index 0000000..fbb819e
--- /dev/null
+++ b/src/content/dependencies/linkFlashAct.js
@@ -0,0 +1,14 @@
+export default {
+  contentDependencies: ['linkThing'],
+  extraDependencies: ['html'],
+
+  relations: (relation, flashAct) =>
+    ({link: relation('linkThing', 'localized.flashActGallery', flashAct)}),
+
+  data: (flashAct) =>
+    ({name: flashAct.name}),
+
+  generate: (data, relations, {html}) =>
+    relations.link
+      .slot('content', new html.Tag(null, null, data.name)),
+};
diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js
new file mode 100644
index 0000000..b3a5474
--- /dev/null
+++ b/src/content/dependencies/listArtTagNetwork.js
@@ -0,0 +1 @@
+export default {generate() {}};
diff --git a/src/data/things/flash.js b/src/data/things/flash.js
index 8fb1edf..52e30f8 100644
--- a/src/data/things/flash.js
+++ b/src/data/things/flash.js
@@ -12,6 +12,7 @@ import {
 import {
   color,
   contributionList,
+  directory,
   fileExtension,
   name,
   referenceList,
@@ -117,12 +118,15 @@ export class Flash extends Thing {
 }
 
 export class FlashAct extends Thing {
+  static [Thing.referenceType] = 'flash-act';
+
   static [Thing.getPropertyDescriptors] = () => ({
     // Update & expose
 
     name: name('Unnamed Flash Act'),
+    directory: directory(),
     color: color(),
-    anchor: simpleString(),
+
     jump: simpleString(),
 
     jumpColor: {
diff --git a/src/data/yaml.js b/src/data/yaml.js
index c799be5..0ecc1f1 100644
--- a/src/data/yaml.js
+++ b/src/data/yaml.js
@@ -434,8 +434,10 @@ export const processFlashDocument = makeProcessDocument(T.Flash, {
 export const processFlashActDocument = makeProcessDocument(T.FlashAct, {
   propertyFieldMapping: {
     name: 'Act',
+    directory: 'Directory',
+
     color: 'Color',
-    anchor: 'Anchor',
+
     jump: 'Jump',
     jumpColor: 'Jump Color',
   },
diff --git a/src/find.js b/src/find.js
index 5a249c2..8c9413b 100644
--- a/src/find.js
+++ b/src/find.js
@@ -150,6 +150,10 @@ const find = {
     referenceTypes: ['flash'],
   }),
 
+  flashAct: findHelper({
+    referenceTypes: ['flash-act'],
+  }),
+
   group: findHelper({
     referenceTypes: ['group', 'group-gallery'],
   }),
@@ -190,6 +194,7 @@ export function bindFind(wikiData, opts1) {
       artist: 'artistData',
       artTag: 'artTagData',
       flash: 'flashData',
+      flashAct: 'flashActData',
       group: 'groupData',
       listing: 'listingSpec',
       newsEntry: 'newsData',
diff --git a/src/page/flash-act.js b/src/page/flash-act.js
new file mode 100644
index 0000000..e54525a
--- /dev/null
+++ b/src/page/flash-act.js
@@ -0,0 +1,23 @@
+export const description = `flash act gallery pages`;
+
+export function condition({wikiData}) {
+  return wikiData.wikiInfo.enableFlashesAndGames;
+}
+
+export function targets({wikiData}) {
+  return wikiData.flashActData;
+}
+
+export function pathsForTarget(flashAct) {
+  return [
+    {
+      type: 'page',
+      path: ['flashActGallery', flashAct.directory],
+
+      contentFunction: {
+        name: 'generateFlashActGalleryPage',
+        args: [flashAct],
+      },
+    },
+  ];
+}
diff --git a/src/page/index.js b/src/page/index.js
index 48e22d2..21d93c8 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -2,6 +2,7 @@ export * as album from './album.js';
 export * as artist from './artist.js';
 export * as artistAlias from './artist-alias.js';
 export * as flash from './flash.js';
+export * as flashAct from './flash-act.js';
 export * as group from './group.js';
 export * as homepage from './homepage.js';
 export * as listing from './listing.js';
diff --git a/src/static/site5.css b/src/static/site5.css
index 7b3e3e0..b7f1b66 100644
--- a/src/static/site5.css
+++ b/src/static/site5.css
@@ -285,6 +285,8 @@ body::before {
 .sidebar > h3,
 .sidebar > p {
   text-align: center;
+  padding-left: 4px;
+  padding-right: 4px;
 }
 
 .sidebar h1 {
diff --git a/src/strings-default.json b/src/strings-default.json
index 904d25d..7a7fa04 100644
--- a/src/strings-default.json
+++ b/src/strings-default.json
@@ -323,6 +323,12 @@
   "flashIndex.title": "Flashes & Games",
   "flashPage.title": "{FLASH}",
   "flashPage.nav.flash": "{FLASH}",
+  "flashSidebar.flashList.flashesInThisAct": "Flashes in this act",
+  "flashSidebar.flashList.flashesInThisStory": "Flashes in this story",
+  "flashSidebar.flashList.flashesInThisSection": "Flashes in this section",
+  "flashSidebar.flashList.volumesInThisGame": "Volumes in this game",
+  "flashSidebar.flashList.gamesInThisSeries": "Games in this series",
+  "flashSidebar.flashList.entriesInThisSection": "Entries in this section",
   "groupSidebar.title": "Groups",
   "groupSidebar.groupList.category": "{CATEGORY}",
   "groupSidebar.groupList.item": "{GROUP}",
diff --git a/src/url-spec.js b/src/url-spec.js
index 4d10313..2ff0fa5 100644
--- a/src/url-spec.js
+++ b/src/url-spec.js
@@ -37,6 +37,8 @@ const urlSpec = {
       flashIndex: 'flash/',
       flash: 'flash/<>/',
 
+      flashActGallery: 'flash-act/<>/',
+
       groupInfo: 'group/<>/',
       groupGallery: 'group/<>/gallery/',