« 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/generateAlbumInfoPage.js16
-rw-r--r--src/content/dependencies/generateAlbumNavLinks.js121
-rw-r--r--src/content/dependencies/generatePageLayout.js139
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js37
-rw-r--r--src/content/dependencies/linkTemplate.js7
-rw-r--r--src/content/dependencies/linkThing.js22
-rw-r--r--src/static/site4.css (renamed from src/static/site3.css)10
-rw-r--r--src/util/sugar.js4
8 files changed, 347 insertions, 9 deletions
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index 21d5ec87..f5dc6bfd 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -1,6 +1,7 @@
 export default {
   contentDependencies: [
     'generateAlbumInfoPageContent',
+    'generateAlbumNavLinks',
     'generateAlbumSidebar',
     'generateAlbumSocialEmbed',
     'generateAlbumStyleRules',
@@ -13,6 +14,7 @@ export default {
   relations(relation, album) {
     return {
       layout: relation('generatePageLayout'),
+      albumNavLinks: relation('generateAlbumNavLinks', album, null),
 
       content: relation('generateAlbumInfoPageContent', album),
       sidebar: relation('generateAlbumSidebar', album, null),
@@ -40,6 +42,20 @@ export default {
         cover: relations.content.cover,
         mainContent: relations.content.main.content,
 
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {
+            auto: 'current',
+            accent:
+              relations.albumNavLinks.slots({
+                showTrackNavigation: true,
+                showExtraLinks: true,
+              }),
+          },
+        ],
+        navContent: '(Chronology links here)',
+
         ...relations.sidebar,
 
         // socialEmbed: relations.socialEmbed,
diff --git a/src/content/dependencies/generateAlbumNavLinks.js b/src/content/dependencies/generateAlbumNavLinks.js
new file mode 100644
index 00000000..d9645319
--- /dev/null
+++ b/src/content/dependencies/generateAlbumNavLinks.js
@@ -0,0 +1,121 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: [
+    'linkTrack',
+    'linkAlbumCommentary',
+    'linkAlbumGallery',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations(relation, album, track) {
+    const relations = {};
+
+    if (track) {
+      const index = album.tracks.indexOf(track);
+
+      if (index > 0) {
+        relations.previousTrackLink =
+          relation('linkTrack', album.tracks[index - 1]);
+      }
+
+      if (index < album.tracks.length - 1) {
+        relations.nextTrackLink =
+          relation('linkTrack', album.tracks[index + 1]);
+      }
+    }
+
+    if (album.tracks.some(t => t.hasUniqueCoverArt)) {
+      relations.albumGalleryLink =
+        relation('linkAlbumGallery', album);
+    }
+
+    if (album.commentary || album.tracks.some(t => t.commentary)) {
+      relations.albumCommentaryLink =
+        relation('linkAlbumCommentary', album);
+    }
+
+    return relations;
+  },
+
+  data(album, track) {
+    return {
+      hasMultipleTracks: album.tracks.length > 1,
+      isTrackPage: !!track,
+    };
+  },
+
+  generate(data, relations, {html, language}) {
+    return html.template({
+      annotation: `generateAlbumNavLinks`,
+
+      slots: {
+        showTrackNavigation: {type: 'boolean', default: false},
+        showExtraLinks: {type: 'boolean', default: false},
+
+        currentExtra: {
+          validate: v => v.is('gallery', 'commentary'),
+        },
+      },
+
+      content(slots) {
+        const extraLinks =
+          (slots.showExtraLinks
+            ? [
+                relations.albumGalleryLink.slots({
+                  attributes: {class: slots.currentExtra === 'gallery' && 'current'},
+                  content: language.$('albumPage.nav.gallery'),
+                }),
+
+                relations.albumCommentaryLink.slots({
+                  attributes: {class: slots.currentExtra === 'commentary' && 'current'},
+                  content: language.$('albumPage.nav.commentary'),
+                }),
+              ]
+            : []);
+
+        const previousNextLinks =
+          (slots.showTrackNavigation
+            ? [
+                relations.previousTrackLink?.slots({
+                  tooltip: true,
+                  attributes: {id: 'previous-button'},
+                  content: language.$('misc.nav.previous'),
+                }),
+                relations.nextTrackLink?.slots({
+                  tooltip: true,
+                  attributes: {id: 'next-button'},
+                  content: language.$('misc.nav.next'),
+                }),
+              ]
+            : []);
+
+        const randomLink =
+          slots.showTrackNavigation &&
+          data.hasMultipleTracks &&
+            html.tag('a',
+              {
+                href: '#',
+                'data-random': 'track-in-album',
+                id: 'random-button',
+              },
+              (data.isTrackPage
+                ? language.$('trackPage.nav.random')
+                : language.$('albumPage.nav.randomTrack')));
+
+        const allLinks = [
+          ...previousNextLinks,
+          ...extraLinks,
+          randomLink,
+        ].filter(Boolean);
+
+        if (empty(allLinks)) {
+          return html.blank();
+        }
+
+        return `(${language.formatUnitList(allLinks)})`
+      },
+    });
+  },
+};
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 1ea5ce24..86054437 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -1,3 +1,5 @@
+import {empty, openAggregate} from '../../util/sugar.js';
+
 export default {
   contentDependencies: [
     'generateFooterLocalizationLinks',
@@ -105,6 +107,58 @@ export default {
 
         // Nav & Footer
 
+        navContent: {type: 'html'},
+        navBottomRowContent: {type: 'html'},
+
+        navLinkStyle: {
+          validate: v => v.is('hierarchical', 'index'),
+          default: 'index',
+        },
+
+        navLinks: {
+          validate: v =>
+            v.arrayOf(object => {
+              v.isObject(object);
+
+              const aggregate = openAggregate({message: `Errors validating navigation link`});
+
+              aggregate.call(v.validateProperties({
+                auto: () => true,
+                html: () => true,
+
+                path: () => true,
+                title: () => true,
+                accent: () => true,
+              }), object);
+
+              if (object.auto || object.html) {
+                if (object.auto && object.html) {
+                  aggregate.push(new TypeError(`Don't specify both auto and html`));
+                } else if (object.auto) {
+                  aggregate.call(v.is('home', 'current'), object.auto);
+                } else {
+                  aggregate.call(v.isHTML, object.html);
+                }
+
+                if (object.path || object.title) {
+                  aggregate.push(new TypeError(`Don't specify path or title along with auto or html`));
+                }
+              } else {
+                aggregate.call(v.validateProperties({
+                  path: v.arrayOf(v.isString),
+                  title: v.isString,
+                }), {
+                  path: object.path,
+                  title: object.title,
+                });
+              }
+
+              aggregate.close();
+
+              return true;
+            })
+        },
+
         footerContent: {type: 'html'},
       },
 
@@ -166,6 +220,87 @@ export default {
               relations.footerLocalizationLinks,
             ]);
 
+        const navHTML = html.tag('nav',
+          {
+            [html.onlyIfContent]: true,
+            id: 'header',
+            class: [
+              !empty(slots.navLinks) && 'nav-has-main-links',
+              !html.isBlank(slots.navContent) && 'nav-has-content',
+              !html.isBlank(slots.navBottomRowContent) && 'nav-has-bottom-row',
+            ],
+          },
+          [
+            html.tag('div',
+              {
+                [html.onlyIfContent]: true,
+                class: [
+                  'nav-main-links',
+                  'nav-links-' + slots.navLinkStyle,
+                ],
+              },
+              slots.navLinks?.map((cur, i) => {
+                let content;
+
+                if (cur.html) {
+                  content = cur.html;
+                } else {
+                  let title;
+                  let href;
+
+                  switch (cur.auto) {
+                    case 'home':
+                      title = wikiInfo.nameShort;
+                      href = to('localized.home');
+                      break;
+                    case 'current':
+                      title = slots.title;
+                      href = '';
+                      break;
+                    case null:
+                    case undefined:
+                      title = cur.title;
+                      href = to(...cur.path);
+                      break;
+                  }
+
+                  content = html.tag('a',
+                    {href},
+                    title);
+                }
+
+                let className;
+
+                if (cur.auto === 'current') {
+                  className = 'current';
+                } else if (
+                  slots.navLinkStyle === 'hierarchical' &&
+                  i === slots.navLinks.length - 1
+                ) {
+                  className = 'current';
+                }
+
+                return html.tag('span',
+                  {class: className},
+                  [
+                    html.tag('span',
+                      {class: 'nav-link-content'},
+                      content),
+                    html.tag('span',
+                      {[html.onlyIfContent]: true, class: 'nav-link-accent'},
+                      cur.accent),
+                  ]);
+              })),
+
+            html.tag('div',
+              {[html.onlyIfContent]: true, class: 'nav-bottom-row'},
+              slots.navBottomRowContent),
+
+            html.tag('div',
+              {[html.onlyIfContent]: true, class: 'nav-content'},
+              slots.navContent),
+          ])
+
         const generateSidebarHTML = (side, id) => {
           const content = slots[side + 'Content'];
           const multiple = slots[side + 'Multiple'];
@@ -213,7 +348,7 @@ export default {
         const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse;
 
         const layoutHTML = [
-          // navHTML,
+          navHTML,
           // banner.position === 'top' && bannerHTML,
           // secondaryNavHTML,
           html.tag('div',
@@ -301,7 +436,7 @@ export default {
 
                 html.tag('link', {
                   rel: 'stylesheet',
-                  href: to('shared.staticFile', `site3.css?${cachebust}`),
+                  href: to('shared.staticFile', `site4.css?${cachebust}`),
                 }),
 
                 html.tag('style',
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index 0519b7e8..acf8461b 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,10 +1,13 @@
 export default {
   contentDependencies: [
     'generateTrackInfoPageContent',
+    'generateAlbumNavLinks',
     'generateAlbumSidebar',
     'generateAlbumStyleRules',
     'generateColorStyleRules',
     'generatePageLayout',
+    'linkAlbum',
+    'linkTrack',
   ],
 
   extraDependencies: ['language'],
@@ -13,6 +16,10 @@ export default {
     return {
       layout: relation('generatePageLayout'),
 
+      albumLink: relation('linkAlbum', track.album),
+      trackLink: relation('linkTrack', track),
+      albumNavLinks: relation('generateAlbumNavLinks', track.album, track),
+
       content: relation('generateTrackInfoPageContent', track),
       sidebar: relation('generateAlbumSidebar', track.album, track),
       albumStyleRules: relation('generateAlbumStyleRules', track.album),
@@ -23,6 +30,9 @@ export default {
   data(track) {
     return {
       name: track.name,
+
+      hasTrackNumbers: track.album.hasTrackNumbers,
+      trackNumber: track.album.tracks.indexOf(track) + 1,
     };
   },
 
@@ -38,6 +48,33 @@ export default {
         cover: relations.content.cover,
         mainContent: relations.content.main.content,
 
+        navLinkStyle: 'hierarchical',
+        navLinks: [
+          {auto: 'home'},
+          {html: relations.albumLink},
+          {
+            html:
+              (data.hasTrackNumbers
+                ? language.$('trackPage.nav.track.withNumber', {
+                    number: data.trackNumber,
+                    track: relations.trackLink
+                      .slot('attributes', {class: 'current'}),
+                  })
+                : language.$('trackPage.nav.track', {
+                    track: relations.trackLink
+                      .slot('attributes', {class: 'current'}),
+                  })),
+          },
+        ],
+
+        navContent: '(Chronology links here)',
+
+        navBottomRowContent:
+          relations.albumNavLinks.slots({
+            showTrackNavigation: true,
+            showExtraLinks: false,
+          }),
+
         ...relations.sidebar,
       });
   },
diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js
index b87f3180..9109ab50 100644
--- a/src/content/dependencies/linkTemplate.js
+++ b/src/content/dependencies/linkTemplate.js
@@ -22,6 +22,7 @@ export default {
         path: {validate: v => v.validateArrayItems(v.isString)},
         hash: {type: 'string'},
 
+        tooltip: {validate: v => v.isString},
         attributes: {validate: v => v.isAttributes},
         color: {validate: v => v.isColor},
         content: {type: 'html'},
@@ -30,6 +31,7 @@ export default {
       content(slots) {
         let href = slots.href;
         let style;
+        let title;
 
         if (!href && !empty(slots.path)) {
           href = to(...slots.path);
@@ -53,11 +55,16 @@ export default {
           style = `--primary-color: ${primary}; --dim-color: ${dim}`;
         }
 
+        if (slots.tooltip) {
+          title = slots.tooltip;
+        }
+
         return html.tag('a',
           {
             ...slots.attributes ?? {},
             href,
             style,
+            title,
           },
           slots.content);
       },
diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js
index 70c86fc4..fea68ae5 100644
--- a/src/content/dependencies/linkThing.js
+++ b/src/content/dependencies/linkThing.js
@@ -35,6 +35,10 @@ export default {
         content: relations.linkTemplate.getSlotDescription('content'),
         preferShortName: {type: 'boolean', default: false},
 
+        tooltip: {
+          validate: v => v.oneOf(v.isBoolean, v.isString),
+        },
+
         color: relations.linkTemplate.getSlotDescription('color'),
         attributes: relations.linkTemplate.getSlotDescription('attributes'),
         hash: relations.linkTemplate.getSlotDescription('hash'),
@@ -43,20 +47,30 @@ export default {
       content(slots) {
         let content = slots.content;
 
+        const name =
+          (slots.preferShortName
+            ? data.nameShort ?? data.name
+            : data.name);
+
         if (html.isBlank(content)) {
-          content =
-            (slots.preferShortName
-              ? data.nameShort ?? data.name
-              : data.name);
+          content = name;
         }
 
         const color = slots.color ?? data.color ?? null;
 
+        let tooltip = null;
+        if (slots.tooltip === true) {
+          tooltip = name;
+        } else if (typeof slots.tooltip === 'string') {
+          tooltip = slots.tooltip;
+        }
+
         return relations.linkTemplate
           .slots({
             path,
             content,
             color,
+            tooltip,
 
             attributes: slots.attributes,
             hash: slots.hash,
diff --git a/src/static/site3.css b/src/static/site4.css
index 684b6d7d..08512559 100644
--- a/src/static/site3.css
+++ b/src/static/site4.css
@@ -433,11 +433,15 @@ a:hover {
   text-decoration: underline;
 }
 
-.nav-main-links > span {
+a.current {
+  font-weight: 800;
+}
+
+.nav-main-links > span > span {
   white-space: nowrap;
 }
 
-.nav-main-links > span > a.current {
+.nav-main-links > span.current > span.nav-link-content > a {
   font-weight: 800;
 }
 
@@ -447,7 +451,7 @@ a:hover {
   font-weight: 800;
 }
 
-.nav-links-hierarchy > span:not(:first-child):not(.no-divider)::before {
+.nav-links-hierarchical > span:not(:first-child):not(.no-divider)::before {
   content: "\0020/\0020";
 }
 
diff --git a/src/util/sugar.js b/src/util/sugar.js
index f3b9e732..0362e327 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -226,6 +226,10 @@ export function openAggregate({
       );
     };
 
+  aggregate.push = (error) => {
+    errors.push(error);
+  };
+
   aggregate.call = (fn, ...args) => {
     return aggregate.wrap(fn)(...args);
   };