diff options
Diffstat (limited to 'src/content')
39 files changed, 1033 insertions, 546 deletions
diff --git a/src/content/dependencies/generateAlbumCommentaryPage.js b/src/content/dependencies/generateAlbumCommentaryPage.js index de619251..3ad1549e 100644 --- a/src/content/dependencies/generateAlbumCommentaryPage.js +++ b/src/content/dependencies/generateAlbumCommentaryPage.js @@ -2,10 +2,13 @@ import {stitchArrays} from '#sugar'; export default { contentDependencies: [ + 'generateAlbumCoverArtwork', 'generateAlbumNavAccent', + 'generateAlbumSidebarTrackSection', 'generateAlbumStyleRules', 'generateColorStyleVariables', 'generateContentHeading', + 'generateTrackCoverArtwork', 'generatePageLayout', 'linkAlbum', 'linkTrack', @@ -21,7 +24,7 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', album); + relation('generateAlbumStyleRules', album, null); relations.albumLink = relation('linkAlbum', album); @@ -30,6 +33,11 @@ export default { relation('generateAlbumNavAccent', album, null); if (album.commentary) { + if (album.hasCoverArt) { + relations.albumCommentaryCover = + relation('generateAlbumCoverArtwork', album); + } + relations.albumCommentaryContent = relation('transformContent', album.commentary); } @@ -46,6 +54,13 @@ export default { tracksWithCommentary .map(track => relation('linkTrack', track)); + relations.trackCommentaryCovers = + tracksWithCommentary + .map(track => + (track.hasUniqueCoverArt + ? relation('generateTrackCoverArtwork', track) + : null)); + relations.trackCommentaryContent = tracksWithCommentary .map(track => relation('transformContent', track.commentary)); @@ -57,6 +72,13 @@ export default { ? null : relation('generateColorStyleVariables'))); + relations.sidebarAlbumLink = + relation('linkAlbum', album); + + relations.sidebarTrackSections = + album.trackSections.map(trackSection => + relation('generateAlbumSidebarTrackSection', album, null, trackSection)); + return relations; }, @@ -129,6 +151,9 @@ export default { {class: ['content-heading']}, language.$('albumCommentaryPage.entry.title.albumCommentary')), + relations.albumCommentaryCover + ?.slots({mode: 'commentary'}), + html.tag('blockquote', relations.albumCommentaryContent), ], @@ -137,15 +162,19 @@ export default { heading: relations.trackCommentaryHeadings, link: relations.trackCommentaryLinks, directory: data.trackCommentaryDirectories, + cover: relations.trackCommentaryCovers, content: relations.trackCommentaryContent, colorVariables: relations.trackCommentaryColorVariables, color: data.trackCommentaryColors, - }).map(({heading, link, directory, content, colorVariables, color}) => [ + }).map(({heading, link, directory, cover, content, colorVariables, color}) => [ heading.slots({ tag: 'h3', id: directory, title: link, }), + + cover?.slots({mode: 'commentary'}), + html.tag('blockquote', (color ? {style: colorVariables.slot('color', color).content} @@ -170,6 +199,17 @@ export default { }), }, ], + + leftSidebarStickyMode: 'column', + leftSidebarContent: [ + html.tag('h1', relations.sidebarAlbumLink), + relations.sidebarTrackSections.map(section => + section.slots({ + anchor: true, + open: true, + mode: 'commentary', + })), + ], }); }, }; diff --git a/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js new file mode 100644 index 00000000..ad99cb87 --- /dev/null +++ b/src/content/dependencies/generateAlbumGalleryNoTrackArtworksLine.js @@ -0,0 +1,7 @@ +export default { + extraDependencies: ['html', 'language'], + + generate: ({html, language}) => + html.tag('p', {class: 'quick-info'}, + language.$('albumGalleryPage.noTrackArtworksLine')), +}; diff --git a/src/content/dependencies/generateAlbumGalleryPage.js b/src/content/dependencies/generateAlbumGalleryPage.js index 68b56bd9..f61b1983 100644 --- a/src/content/dependencies/generateAlbumGalleryPage.js +++ b/src/content/dependencies/generateAlbumGalleryPage.js @@ -3,8 +3,10 @@ import {compareArrays, stitchArrays} from '#sugar'; export default { contentDependencies: [ 'generateAlbumGalleryCoverArtistsLine', + 'generateAlbumGalleryNoTrackArtworksLine', 'generateAlbumGalleryStatsLine', 'generateAlbumNavAccent', + 'generateAlbumSecondaryNav', 'generateAlbumStyleRules', 'generateCoverGrid', 'generatePageLayout', @@ -51,7 +53,7 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', album); + relation('generateAlbumStyleRules', album, null); relations.albumLink = relation('linkAlbum', album); @@ -59,9 +61,17 @@ export default { relations.albumNavAccent = relation('generateAlbumNavAccent', album, null); + relations.secondaryNav = + relation('generateAlbumSecondaryNav', album); + relations.statsLine = relation('generateAlbumGalleryStatsLine', album); + if (album.tracks.every(track => !track.hasUniqueCoverArt)) { + relations.noTrackArtworksLine = + relation('generateAlbumGalleryNoTrackArtworksLine'); + } + if (query.coverArtistsForAllTracks) { relations.coverArtistsLine = relation('generateAlbumGalleryCoverArtistsLine', query.coverArtistsForAllTracks); @@ -70,15 +80,25 @@ export default { relations.coverGrid = relation('generateCoverGrid'); - relations.links = - album.tracks.map(track => - relation('linkTrack', track)); + relations.links = [ + relation('linkAlbum', album), - relations.images = - album.tracks.map(track => - (track.hasUniqueCoverArt - ? relation('image', track.artTags) - : relation('image'))); + ... + album.tracks + .map(track => relation('linkTrack', track)), + ]; + + relations.images = [ + (album.hasCoverArt + ? relation('image', album.artTags) + : relation('image')), + + ... + album.tracks.map(track => + (track.hasUniqueCoverArt + ? relation('image', track.artTags) + : relation('image'))), + ]; return relations; }, @@ -89,27 +109,41 @@ export default { data.name = album.name; data.color = album.color; - data.names = - album.tracks.map(track => track.name); + data.names = [ + album.name, + ...album.tracks.map(track => track.name), + ]; - data.coverArtists = - album.tracks.map(track => { - if (query.coverArtistsForAllTracks) { - return null; - } + data.coverArtists = [ + (album.hasCoverArt + ? album.coverArtistContribs.map(({who: artist}) => artist.name) + : null), - if (track.hasUniqueCoverArt) { - return track.coverArtistContribs.map(({who: artist}) => artist.name); - } + ... + album.tracks.map(track => { + if (query.coverArtistsForAllTracks) { + return null; + } - return null; - }); + if (track.hasUniqueCoverArt) { + return track.coverArtistContribs.map(({who: artist}) => artist.name); + } + + return null; + }), + ]; - data.paths = - album.tracks.map(track => - (track.hasUniqueCoverArt - ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] - : null)); + data.paths = [ + (album.hasCoverArt + ? ['media.albumCover', album.directory, album.coverArtFileExtension] + : null), + + ... + album.tracks.map(track => + (track.hasUniqueCoverArt + ? ['media.trackCover', track.album.directory, track.directory, track.coverArtFileExtension] + : null)), + ]; return data; }, @@ -131,6 +165,7 @@ export default { mainContent: [ relations.statsLine, relations.coverArtistsLine, + relations.noTrackArtworksLine, relations.coverGrid .slots({ @@ -172,6 +207,8 @@ export default { }), }, ], + + secondaryNav: relations.secondaryNav, }); }, }; diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index ce17ab21..5fe27caf 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -37,14 +37,14 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', album); + relation('generateAlbumStyleRules', album, null); relations.socialEmbed = relation('generateAlbumSocialEmbed', album); relations.coverArtistChronologyContributions = getChronologyRelations(album, { - contributions: album.coverArtistContribs, + contributions: album.coverArtistContribs ?? [], linkArtist: artist => relation('linkArtist', artist), diff --git a/src/content/dependencies/generateAlbumNavAccent.js b/src/content/dependencies/generateAlbumNavAccent.js index c79219bb..7eb1dac0 100644 --- a/src/content/dependencies/generateAlbumNavAccent.js +++ b/src/content/dependencies/generateAlbumNavAccent.js @@ -33,10 +33,8 @@ export default { } } - if (album.tracks.some(t => t.hasUniqueCoverArt)) { - relations.albumGalleryLink = - relation('linkAlbumGallery', album); - } + relations.albumGalleryLink = + relation('linkAlbumGallery', album); if (album.commentary || album.tracks.some(t => t.commentary)) { relations.albumCommentaryLink = @@ -49,6 +47,7 @@ export default { data(album, track) { return { hasMultipleTracks: album.tracks.length > 1, + galleryIsStub: album.tracks.every(t => !t.hasUniqueCoverArt), isTrackPage: !!track, }; }, @@ -66,10 +65,11 @@ export default { const {content: extraLinks = []} = slots.showExtraLinks && {content: [ - relations.albumGalleryLink?.slots({ - attributes: {class: slots.currentExtra === 'gallery' && 'current'}, - content: language.$('albumPage.nav.gallery'), - }), + (!data.galleryIsStub || slots.currentExtra === 'gallery') && + relations.albumGalleryLink?.slots({ + attributes: {class: slots.currentExtra === 'gallery' && 'current'}, + content: language.$('albumPage.nav.gallery'), + }), relations.albumCommentaryLink?.slots({ attributes: {class: slots.currentExtra === 'commentary' && 'current'}, diff --git a/src/content/dependencies/generateAlbumSecondaryNav.js b/src/content/dependencies/generateAlbumSecondaryNav.js index 705dec51..8cf36fa4 100644 --- a/src/content/dependencies/generateAlbumSecondaryNav.js +++ b/src/content/dependencies/generateAlbumSecondaryNav.js @@ -5,7 +5,7 @@ export default { 'generateColorStyleVariables', 'generatePreviousNextLinks', 'generateSecondaryNav', - 'linkAlbum', + 'linkAlbumDynamically', 'linkGroup', 'linkTrack', ], @@ -64,14 +64,14 @@ export default { query.adjacentGroupInfo .map(({previousAlbum}) => (previousAlbum - ? relation('linkAlbum', previousAlbum) + ? relation('linkAlbumDynamically', previousAlbum) : null)); relations.nextAlbumLinks = query.adjacentGroupInfo .map(({nextAlbum}) => (nextAlbum - ? relation('linkAlbum', nextAlbum) + ? relation('linkAlbumDynamically', nextAlbum) : null)); } diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index 2aca6da1..d3cd37f0 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -33,10 +33,28 @@ export default { } } + data.trackDirectories = + trackSection.tracks + .map(track => track.directory); + + data.tracksAreMissingCommentary = + trackSection.tracks + .map(track => !track.commentary); + return data; }, - generate(data, relations, {getColors, html, language}) { + slots: { + anchor: {type: 'boolean'}, + open: {type: 'boolean'}, + + mode: { + validate: v => v.is('info', 'commentary'), + default: 'info', + }, + }, + + generate(data, relations, slots, {getColors, html, language}) { const sectionName = html.tag('span', {class: 'group-name'}, (data.isDefaultTrackSection @@ -53,13 +71,28 @@ export default { relations.trackLinks.map((trackLink, index) => html.tag('li', { - class: + class: [ data.includesCurrentTrack && index === data.currentTrackIndex && - 'current', + 'current', + + slots.mode === 'commentary' && + data.tracksAreMissingCommentary[index] && + 'no-commentary', + ], }, language.$('albumSidebar.trackList.item', { - track: trackLink, + track: + (slots.mode === 'commentary' && data.tracksAreMissingCommentary[index] + ? trackLink.slots({ + linkless: true, + }) + : slots.anchor + ? trackLink.slots({ + anchor: true, + hash: data.trackDirectories[index], + }) + : trackLink), }))); return html.tag('details', @@ -67,6 +100,11 @@ export default { class: data.includesCurrentTrack && 'current', open: ( + // Allow forcing open via a template slot. + // This isn't exactly janky, but the rest of this function + // kind of is when you contextualize it in a template... + slots.open || + // Leave sidebar track sections collapsed on album info page, // since there's already a view of the full track listing // in the main content area. @@ -82,7 +120,7 @@ export default { (data.hasTrackNumbers ? language.$('albumSidebar.trackList.group.withRange', { group: sectionName, - range: `${data.firstTrackNumber}–${data.lastTrackNumber}` + range: `${data.firstTrackNumber}–${data.lastTrackNumber}` }) : language.$('albumSidebar.trackList.group', { group: sectionName, diff --git a/src/content/dependencies/generateAlbumStyleRules.js b/src/content/dependencies/generateAlbumStyleRules.js index 1acaea17..c5acf374 100644 --- a/src/content/dependencies/generateAlbumStyleRules.js +++ b/src/content/dependencies/generateAlbumStyleRules.js @@ -3,14 +3,13 @@ import {empty} from '#sugar'; export default { extraDependencies: ['to'], - data(album) { + data(album, track) { const data = {}; data.hasWallpaper = !empty(album.wallpaperArtistContribs); data.hasBanner = !empty(album.bannerArtistContribs); if (data.hasWallpaper) { - data.hasWallpaperStyle = !!album.wallpaperStyle; data.wallpaperPath = ['media.albumWallpaper', album.directory, album.wallpaperFileExtension]; data.wallpaperStyle = album.wallpaperStyle; } @@ -20,40 +19,54 @@ export default { data.bannerStyle = album.bannerStyle; } + data.albumDirectory = album.directory; + + if (track) { + data.trackDirectory = track.directory; + } + return data; }, generate(data, {to}) { - const wallpaperPart = - (data.hasWallpaper - ? [ - `body::before {`, - ` background-image: url("${to(...data.wallpaperPath)}");`, - ...(data.hasWallpaperStyle - ? data.wallpaperStyle - .split('\n') - .map(line => ` ${line}`) - : []), - `}`, - ] - : []); + const indent = parts => + (parts ?? []) + .filter(Boolean) + .join('\n') + .split('\n') + .map(line => ' '.repeat(4) + line) + .join('\n'); - const bannerPart = - (data.hasBannerStyle - ? [ - `#banner img {`, - ...data.bannerStyle - .split('\n') - .map(line => ` ${line}`), - `}`, - ] + const rule = (selector, parts) => + (!empty(parts.filter(Boolean)) + ? [`${selector} {`, indent(parts), `}`] : []); - return [ - ...wallpaperPart, - ...bannerPart, - ] - .filter(Boolean) - .join('\n'); + const wallpaperRule = + data.hasWallpaper && + rule(`body::before`, [ + `background-image: url("${to(...data.wallpaperPath)}");`, + data.wallpaperStyle, + ]); + + const bannerRule = + data.hasBanner && + rule(`#banner img`, [ + data.bannerStyle, + ]); + + const dataRule = + rule(`:root`, [ + data.albumDirectory && + `--album-directory: ${data.albumDirectory};`, + data.trackDirectory && + `--track-directory: ${data.trackDirectory};`, + ]); + + return ( + [wallpaperRule, bannerRule, dataRule] + .filter(Boolean) + .flat() + .join('\n')); }, }; diff --git a/src/content/dependencies/generateAlbumTrackListItem.js b/src/content/dependencies/generateAlbumTrackListItem.js index f65b47c9..f92712f9 100644 --- a/src/content/dependencies/generateAlbumTrackListItem.js +++ b/src/content/dependencies/generateAlbumTrackListItem.js @@ -1,4 +1,4 @@ -import {compareArrays} from '#sugar'; +import {compareArrays, empty} from '#sugar'; export default { contentDependencies: [ @@ -11,9 +11,11 @@ export default { relations(relation, track) { const relations = {}; - relations.contributionLinks = - track.artistContribs - .map(contrib => relation('linkContribution', contrib)); + if (!empty(track.artistContribs)) { + relations.contributionLinks = + track.artistContribs + .map(contrib => relation('linkContribution', contrib)); + } relations.trackLink = relation('linkTrack', track); @@ -31,10 +33,12 @@ export default { } data.showArtists = - !compareArrays( - track.artistContribs.map(c => c.who), - album.artistContribs.map(c => c.who), - {checkOrder: false}); + !empty(track.artistContribs) && + (empty(album.artistContribs) || + !compareArrays( + track.artistContribs.map(c => c.who), + album.artistContribs.map(c => c.who), + {checkOrder: false})); return data; }, diff --git a/src/content/dependencies/generateArtistInfoPageChunkItem.js b/src/content/dependencies/generateArtistInfoPageChunkItem.js index 36f0ebcc..9f99513d 100644 --- a/src/content/dependencies/generateArtistInfoPageChunkItem.js +++ b/src/content/dependencies/generateArtistInfoPageChunkItem.js @@ -5,7 +5,7 @@ export default { content: {type: 'html'}, otherArtistLinks: {validate: v => v.strictArrayOf(v.isHTML)}, - contribution: {type: 'string'}, + contribution: {type: 'html'}, rerelease: {type: 'boolean'}, }, @@ -30,7 +30,7 @@ export default { options.artists = language.formatConjunctionList(slots.otherArtistLinks); } - if (slots.contribution) { + if (!html.isBlank(slots.contribution)) { parts.push('withContribution'); options.contribution = slots.contribution; } diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index 0566f713..654f759c 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -1,4 +1,4 @@ -import {accumulateSum, stitchArrays} from '#sugar'; +import {accumulateSum, empty, stitchArrays} from '#sugar'; import { chunkByProperties, @@ -16,7 +16,7 @@ export default { 'linkTrack', ], - extraDependencies: ['language'], + extraDependencies: ['html', 'language'], query(artist) { const tracksAsArtistAndContributor = @@ -122,11 +122,16 @@ export default { trackContributions: query.chunks.map(({chunk}) => - chunk.map(({contribs}) => - contribs - .filter(({who}) => who === artist) - .filter(({what}) => what) - .map(({what}) => what))), + chunk + .map(({contribs}) => + contribs + .filter(({who}) => who === artist) + .filter(({what}) => what) + .map(({what}) => what)) + .map(contributions => + (empty(contributions) + ? null + : contributions))), trackRereleases: query.chunks.map(({chunk}) => @@ -134,7 +139,7 @@ export default { }; }, - generate(data, relations, {language}) { + generate(data, relations, {html, language}) { return relations.chunkedList.slots({ chunks: stitchArrays({ @@ -192,7 +197,9 @@ export default { rerelease, contribution: - language.formatUnitList(contribution), + (contribution + ? language.formatUnitList(contribution) + : html.blank()), content: (duration diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js index 4060c6b0..aeba97de 100644 --- a/src/content/dependencies/generateCoverArtwork.js +++ b/src/content/dependencies/generateCoverArtwork.js @@ -32,7 +32,7 @@ export default { }, mode: { - validate: v => v.is('primary', 'thumbnail'), + validate: v => v.is('primary', 'thumbnail', 'commentary'), default: 'primary', }, }, @@ -73,6 +73,19 @@ export default { square: true, }); + case 'commentary': + return relations.image + .slots({ + path: slots.path, + alt: slots.alt, + thumb: 'medium', + class: 'commentary-art', + reveal: true, + link: true, + square: true, + lazy: true, + }); + default: return html.blank(); } diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 9822e1ae..5636e4f3 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -2,7 +2,7 @@ import {stitchArrays} from '#sugar'; export default { contentDependencies: ['generateGridActionLinks'], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations(relation) { return { @@ -20,7 +20,7 @@ export default { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(relations, slots, {html}) { + generate(relations, slots, {html, language}) { return ( html.tag('div', {class: 'grid-listing'}, [ stitchArrays({ @@ -42,8 +42,12 @@ export default { ? slots.lazy : false), }), - html.tag('span', {[html.onlyIfContent]: true}, name), - html.tag('span', {[html.onlyIfContent]: true}, info), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(name)), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(info)), ], })), diff --git a/src/content/dependencies/generateFlashActGalleryPage.js b/src/content/dependencies/generateFlashActGalleryPage.js new file mode 100644 index 00000000..8eea58bb --- /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 00000000..98504385 --- /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 00000000..bd6063c9 --- /dev/null +++ b/src/content/dependencies/generateFlashActSidebar.js @@ -0,0 +1,194 @@ +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:sunday-night-strifin'), + ]; + + const sideNames = [ + `Side 1 (Acts 1-5)`, + `Side 2 (Acts 6-7)`, + `Additional Canon`, + `Fan Adventures`, + `Fan Games & More`, + ]; + + 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 fallbackListTerminology = + (currentSideIndex <= 1 + ? 'flashesInThisAct' + : 'entriesInThisSection'); + + return { + sideNames, + sideColors, + sideActs, + + currentSideIndex, + currentSideActs, + currentActIndex, + currentActFlashes, + currentFlashIndex, + + fallbackListTerminology, + }; + }, + + 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, + + customListTerminology: act.listTerminology, + fallbackListTerminology: query.fallbackListTerminology, + }), + + 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'}, + (data.customListTerminology + ? language.sanitize(data.customListTerminology) + : language.$('flashSidebar.flashList', data.fallbackListTerminology)))), + + 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 66588fdb..ad1dab94 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 553d2f54..09c6b37c 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 2c8205d3..57196d06 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 ba761922..00000000 --- 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/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index b4970b17..5df83566 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -38,7 +38,7 @@ export default { return html.tag('div', {class: 'footer-localization-links'}, language.$('misc.uiLanguage', { - languages: links.join('\n'), + languages: language.formatListWithoutSeparator(links), })); }, }; diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 47239f55..259f5dce 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -11,6 +11,7 @@ export default { 'generateCoverCarousel', 'generateCoverGrid', 'generateGroupNavLinks', + 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', 'image', @@ -20,18 +21,8 @@ export default { extraDependencies: ['html', 'language', 'wikiData'], - sprawl({listingSpec, wikiInfo}) { - const sprawl = {}; - sprawl.enableGroupUI = wikiInfo.enableGroupUI; - - if (wikiInfo.enableListings && wikiInfo.enableGroupUI) { - sprawl.groupsByCategoryListing = - listingSpec - .find(l => l.directory === 'groups/by-category'); - } - - return sprawl; - }, + sprawl: ({wikiInfo}) => + ({enableGroupUI: wikiInfo.enableGroupUI}), relations(relation, sprawl, group) { const relations = {}; @@ -46,15 +37,13 @@ export default { relation('generateGroupNavLinks', group); if (sprawl.enableGroupUI) { + relations.secondaryNav = + relation('generateGroupSecondaryNav', group); + relations.sidebar = relation('generateGroupSidebar', group); } - if (sprawl.groupsByCategoryListing) { - relations.groupListingLink = - relation('linkListing', sprawl.groupsByCategoryListing); - } - const carouselAlbums = filterItemsForCarousel(group.featuredAlbums); if (!empty(carouselAlbums)) { @@ -160,15 +149,6 @@ export default { })), })), - relations.groupListingLink && - html.tag('p', - {class: 'quick-info'}, - language.$('groupGalleryPage.anotherGroupLine', { - link: - relations.groupListingLink - .slot('content', language.$('groupGalleryPage.anotherGroupLine.link')), - })), - relations.coverGrid .slots({ links: relations.gridLinks, @@ -208,6 +188,9 @@ export default { relations.navLinks .slot('currentExtra', 'gallery') .content, + + secondaryNav: + relations.secondaryNav ?? null, }); }, }; diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js index e162a26a..0583755e 100644 --- a/src/content/dependencies/generateGroupInfoPage.js +++ b/src/content/dependencies/generateGroupInfoPage.js @@ -4,6 +4,7 @@ export default { contentDependencies: [ 'generateContentHeading', 'generateGroupNavLinks', + 'generateGroupSecondaryNav', 'generateGroupSidebar', 'generatePageLayout', 'linkAlbum', @@ -32,6 +33,9 @@ export default { relation('generateGroupNavLinks', group); if (sprawl.enableGroupUI) { + relations.secondaryNav = + relation('generateGroupSecondaryNav', group); + relations.sidebar = relation('generateGroupSidebar', group); } @@ -161,6 +165,8 @@ export default { navLinkStyle: 'hierarchical', navLinks: relations.navLinks.content, + + secondaryNav: relations.secondaryNav ?? null, }); }, }; diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js index 68341e0a..5cde2ab4 100644 --- a/src/content/dependencies/generateGroupNavLinks.js +++ b/src/content/dependencies/generateGroupNavLinks.js @@ -2,10 +2,8 @@ import {empty} from '#sugar'; export default { contentDependencies: [ - 'generatePreviousNextLinks', 'linkGroup', 'linkGroupGallery', - 'linkGroupExtra', ], extraDependencies: ['html', 'language', 'wikiData'], @@ -28,24 +26,6 @@ export default { relations.mainLink = relation('linkGroup', group); - relations.previousNextLinks = - relation('generatePreviousNextLinks'); - - const groups = sprawl.groupCategoryData - .flatMap(category => category.groups); - - const index = groups.indexOf(group); - - if (index > 0) { - relations.previousLink = - relation('linkGroupExtra', groups[index - 1]); - } - - if (index < groups.length - 1) { - relations.nextLink = - relation('linkGroupExtra', groups[index + 1]); - } - relations.infoLink = relation('linkGroup', group); @@ -80,26 +60,6 @@ export default { ]; } - const previousNextLinks = - (relations.previousLink || relations.nextLink) && - relations.previousNextLinks.slots({ - previousLink: - relations.previousLink - ?.slot('extra', slots.currentExtra) - ?.content - ?? null, - nextLink: - relations.nextLink - ?.slot('extra', slots.currentExtra) - ?.content - ?? null, - }); - - const previousNextPart = - previousNextLinks && - language.formatUnitList( - previousNextLinks.content.filter(Boolean)); - const infoLink = relations.infoLink.slots({ attributes: {class: slots.currentExtra === null && 'current'}, @@ -119,7 +79,9 @@ export default { : language.formatUnitList([infoLink, ...extraLinks])); const accent = - `(${[extrasPart, previousNextPart].filter(Boolean).join('; ')})`; + (extrasPart + ? `(${extrasPart})` + : null); return [ {auto: 'home'}, diff --git a/src/content/dependencies/generateGroupSecondaryNav.js b/src/content/dependencies/generateGroupSecondaryNav.js new file mode 100644 index 00000000..e3b28099 --- /dev/null +++ b/src/content/dependencies/generateGroupSecondaryNav.js @@ -0,0 +1,99 @@ +export default { + contentDependencies: [ + 'generateColorStyleVariables', + 'generatePreviousNextLinks', + 'generateSecondaryNav', + 'linkGroupDynamically', + 'linkListing', + ], + + extraDependencies: ['html', 'language', 'wikiData'], + + sprawl: ({listingSpec, wikiInfo}) => ({ + groupsByCategoryListing: + (wikiInfo.enableListings + ? listingSpec + .find(l => l.directory === 'groups/by-category') + : null), + }), + + query(sprawl, group) { + const groups = group.category.groups; + const index = groups.indexOf(group); + + return { + previousGroup: + (index > 0 + ? groups[index - 1] + : null), + + nextGroup: + (index < groups.length - 1 + ? groups[index + 1] + : null), + }; + }, + + relations(relation, query, sprawl, _group) { + const relations = {}; + + relations.secondaryNav = + relation('generateSecondaryNav'); + + if (sprawl.groupsByCategoryListing) { + relations.categoryLink = + relation('linkListing', sprawl.groupsByCategoryListing); + } + + relations.colorVariables = + relation('generateColorStyleVariables'); + + if (query.previousGroup || query.nextGroup) { + relations.previousNextLinks = + relation('generatePreviousNextLinks'); + } + + relations.previousGroupLink = + (query.previousGroup + ? relation('linkGroupDynamically', query.previousGroup) + : null); + + relations.nextGroupLink = + (query.nextGroup + ? relation('linkGroupDynamically', query.nextGroup) + : null); + + return relations; + }, + + data: (query, sprawl, group) => ({ + categoryName: group.category.name, + categoryColor: group.category.color, + }), + + generate(data, relations, {html, language}) { + const {content: previousNextPart} = + relations.previousNextLinks.slots({ + previousLink: relations.previousGroupLink, + nextLink: relations.nextGroupLink, + id: true, + }); + + const {categoryLink} = relations; + + categoryLink?.setSlot('content', data.categoryName); + + return relations.secondaryNav.slots({ + class: 'nav-links-groups', + content: + (!relations.previousGroupLink && !relations.nextGroupLink + ? categoryLink + : html.tag('span', + {style: relations.colorVariables.slot('color', data.categoryColor).content}, + [ + categoryLink.slot('color', false), + `(${language.formatUnitList(previousNextPart)})`, + ])), + }); + }, +}; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 95a5dbec..cd831ba7 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -105,7 +105,7 @@ export default { color: {validate: v => v.isColor}, styleRules: { - validate: v => v.sparseArrayOf(v.isString), + validate: v => v.sparseArrayOf(v.isHTML), default: [], }, @@ -183,7 +183,7 @@ export default { } else { aggregate.call(v.validateProperties({ path: v.strictArrayOf(v.isString), - title: v.isString, + title: v.isHTML, }), { path: object.path, title: object.title, @@ -394,6 +394,10 @@ export default { const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left'); const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right'); + + const hasSidebarLeft = !html.isBlank(sidebarLeftHTML); + const hasSidebarRight = !html.isBlank(sidebarRightHTML); + const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse; const hasID = (() => { @@ -422,20 +426,20 @@ export default { processSkippers([ {condition: true, id: 'content', string: 'content'}, { - condition: !html.isBlank(sidebarLeftHTML), + condition: hasSidebarLeft, id: 'sidebar-left', string: - (html.isBlank(sidebarRightHTML) - ? 'sidebar' - : 'sidebar.left'), + (hasSidebarRight + ? 'sidebar.left' + : 'sidebar'), }, { - condition: !html.isBlank(sidebarRightHTML), + condition: hasSidebarRight, id: 'sidebar-right', string: - (html.isBlank(sidebarLeftHTML) - ? 'sidebar' - : 'sidebar.right'), + (hasSidebarLeft + ? 'sidebar.right' + : 'sidebar'), }, {condition: navHTML, id: 'header', string: 'header'}, {condition: footerHTML, id: 'footer', string: 'footer'}, @@ -507,11 +511,6 @@ export default { class: [ 'layout-columns', !collapseSidebars && 'vertical-when-thin', - (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar', - (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars', - !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars', - sidebarLeftHTML && 'has-sidebar-left', - sidebarRightHTML && 'has-sidebar-right', ], }, [ @@ -521,7 +520,7 @@ export default { ]), slots.bannerPosition === 'bottom' && slots.banner, footerHTML, - ].filter(Boolean).join('\n'); + ]; const pageHTML = html.tags([ `<!DOCTYPE html>`, @@ -609,7 +608,7 @@ export default { html.tag('link', { rel: 'stylesheet', - href: to('shared.staticFile', 'site4.css', cachebust), + href: to('shared.staticFile', 'site5.css', cachebust), }), html.tag('style', [ @@ -624,12 +623,22 @@ export default { ]), html.tag('body', - // {style: body.style || ''}, [ - html.tag('div', {id: 'page-container'}, [ - skippersHTML, - layoutHTML, - ]), + html.tag('div', + { + id: 'page-container', + class: [ + (hasSidebarLeft || hasSidebarRight) && 'has-one-sidebar', + (hasSidebarLeft && hasSidebarRight) && 'has-two-sidebars', + !(hasSidebarLeft || hasSidebarRight) && 'has-zero-sidebars', + hasSidebarLeft && 'has-sidebar-left', + hasSidebarRight && 'has-sidebar-right', + ], + }, + [ + skippersHTML, + layoutHTML, + ]), // infoCardHTML, imageOverlayHTML, diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index 334c5422..1083d863 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -44,14 +44,17 @@ export default { relation('generatePageLayout'); relations.albumStyleRules = - relation('generateAlbumStyleRules', track.album); + relation('generateAlbumStyleRules', track.album, track); relations.socialEmbed = relation('generateTrackSocialEmbed', track); relations.artistChronologyContributions = getChronologyRelations(track, { - contributions: [...track.artistContribs, ...track.contributorContribs], + contributions: [ + ...track.artistContribs ?? [], + ...track.contributorContribs ?? [], + ], linkArtist: artist => relation('linkArtist', artist), linkThing: track => relation('linkTrack', track), @@ -65,7 +68,7 @@ export default { relations.coverArtistChronologyContributions = getChronologyRelations(track, { - contributions: track.coverArtistContribs, + contributions: track.coverArtistContribs ?? [], linkArtist: artist => relation('linkArtist', artist), diff --git a/src/content/dependencies/generateTrackList.js b/src/content/dependencies/generateTrackList.js index f001c3b3..65f5552b 100644 --- a/src/content/dependencies/generateTrackList.js +++ b/src/content/dependencies/generateTrackList.js @@ -1,4 +1,4 @@ -import {empty} from '#sugar'; +import {empty, stitchArrays} from '#sugar'; export default { contentDependencies: ['linkTrack', 'linkContribution'], @@ -11,14 +11,17 @@ export default { } return { - items: tracks.map(track => ({ - trackLink: - relation('linkTrack', track), + trackLinks: + tracks + .map(track => relation('linkTrack', track)), - contributionLinks: - track.artistContribs - .map(contrib => relation('linkContribution', contrib)), - })), + contributionLinks: + tracks + .map(track => + (empty(track.artistContribs) + ? null + : track.artistContribs + .map(contrib => relation('linkContribution', contrib)))), }; }, @@ -28,22 +31,28 @@ export default { }, generate(relations, slots, {html, language}) { - return html.tag('ul', - relations.items.map(({trackLink, contributionLinks}) => - html.tag('li', - language.$('trackList.item.withArtists', { - track: trackLink, - by: - html.tag('span', {class: 'by'}, - language.$('trackList.item.withArtists.by', { - artists: - language.formatConjunctionList( - contributionLinks.map(link => - link.slots({ - showContribution: slots.showContribution, - showIcons: slots.showIcons, - }))), - })), - })))); + return ( + html.tag('ul', + stitchArrays({ + trackLink: relations.trackLinks, + contributionLinks: relations.contributionLinks, + }).map(({trackLink, contributionLinks}) => + html.tag('li', + (empty(contributionLinks) + ? trackLink + : language.$('trackList.item.withArtists', { + track: trackLink, + by: + html.tag('span', {class: 'by'}, + language.$('trackList.item.withArtists.by', { + artists: + language.formatConjunctionList( + contributionLinks.map(link => + link.slots({ + showContribution: slots.showContribution, + showIcons: slots.showIcons, + }))), + })), + })))))); }, }; diff --git a/src/content/dependencies/generateWikiHomeAlbumsRow.js b/src/content/dependencies/generateWikiHomeAlbumsRow.js index 99c1be55..cb0860f5 100644 --- a/src/content/dependencies/generateWikiHomeAlbumsRow.js +++ b/src/content/dependencies/generateWikiHomeAlbumsRow.js @@ -16,7 +16,7 @@ export default { sprawl({albumData}, row) { const sprawl = {}; - switch (row.sourceGroupByRef) { + switch (row.sourceGroup) { case 'new-releases': sprawl.albums = getNewReleases(row.countAlbumsFromGroup, {albumData}); break; diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index 71b905f7..6c0aeecd 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -1,11 +1,16 @@ +import {logInfo, logWarn} from '#cli'; import {empty} from '#sugar'; export default { extraDependencies: [ - 'getSizeOfImageFile', + 'checkIfImagePathHasCachedThumbnails', + 'getDimensionsOfImagePath', + 'getSizeOfImagePath', + 'getThumbnailEqualOrSmaller', + 'getThumbnailsAvailableForDimensions', 'html', 'language', - 'thumb', + 'missingImagePaths', 'to', ], @@ -52,10 +57,14 @@ export default { }, generate(data, slots, { - getSizeOfImageFile, + checkIfImagePathHasCachedThumbnails, + getDimensionsOfImagePath, + getSizeOfImagePath, + getThumbnailEqualOrSmaller, + getThumbnailsAvailableForDimensions, html, language, - thumb, + missingImagePaths, to, }) { let originalSrc; @@ -68,43 +77,48 @@ export default { originalSrc = ''; } - const thumbSrc = - originalSrc && - (slots.thumb - ? thumb[slots.thumb](originalSrc) - : originalSrc); + let mediaSrc = null; + if (originalSrc.startsWith(to('media.root'))) { + mediaSrc = + originalSrc + .slice(to('media.root').length) + .replace(/^\//, ''); + } - const willLink = typeof slots.link === 'string' || slots.link; - const customLink = typeof slots.link === 'string'; + const isMissingImageFile = + missingImagePaths.includes(mediaSrc); + + if (isMissingImageFile) { + logInfo`No image file for ${mediaSrc} - build again for list of missing images.`; + } + + const willLink = + !isMissingImageFile && + (typeof slots.link === 'string' || slots.link); + + const customLink = + typeof slots.link === 'string'; const willReveal = slots.reveal && originalSrc && + !isMissingImageFile && !empty(data.contentWarnings); const willSquare = slots.square; const idOnImg = willLink ? null : slots.id; const idOnLink = willLink ? slots.id : null; + const classOnImg = willLink ? null : slots.class; const classOnLink = willLink ? slots.class : null; - if (!originalSrc) { + if (!originalSrc || isMissingImageFile) { return prepare( html.tag('div', {class: 'image-text-area'}, - slots.missingSourceContent)); - } - - let fileSize = null; - if (willLink) { - const mediaRoot = to('media.root'); - if (originalSrc.startsWith(mediaRoot)) { - fileSize = - getSizeOfImageFile( - originalSrc - .slice(mediaRoot.length) - .replace(/^\//, '')); - } + (html.isBlank(slots.missingSourceContent) + ? language.$(`misc.missingImage`) + : slots.missingSourceContent))); } let reveal = null; @@ -119,22 +133,84 @@ export default { ]; } + const hasThumbnails = + mediaSrc && + checkIfImagePathHasCachedThumbnails(mediaSrc); + + // Warn for images that *should* have cached thumbnail information but are + // missing from the thumbs cache. + if ( + slots.thumb && + !hasThumbnails && + !mediaSrc.endsWith('.gif') + ) { + logWarn`No thumbnail info cached: ${mediaSrc} - displaying original image here (instead of ${slots.thumb})`; + } + + // Important to note that these might not be set at all, even if + // slots.thumb was provided. + let thumbSrc = null; + let availableThumbs = null; + let originalLength = null; + + if (hasThumbnails && slots.thumb) { + // Note: This provides mediaSrc to getThumbnailEqualOrSmaller, since + // it's the identifier which thumbnail utilities use to query from the + // thumbnail cache. But we use the result to operate on originalSrc, + // which is the HTML output-appropriate path including `../../` or + // another alternate base path. + const selectedSize = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc); + thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${selectedSize}.jpg`); + + const dimensions = getDimensionsOfImagePath(mediaSrc); + availableThumbs = getThumbnailsAvailableForDimensions(dimensions); + + const [width, height] = dimensions; + originalLength = Math.max(width, height) + } + + let fileSize = null; + if (willLink && mediaSrc) { + fileSize = getSizeOfImagePath(mediaSrc); + } + const imgAttributes = { id: idOnImg, class: classOnImg, alt: slots.alt, width: slots.width, height: slots.height, - 'data-original-size': fileSize, - 'data-no-image-preview': customLink, }; + if (customLink) { + imgAttributes['data-no-image-preview'] = true; + } + + // These attributes are only relevant when a thumbnail are available *and* + // being used. + if (hasThumbnails && slots.thumb) { + if (fileSize) { + imgAttributes['data-original-size'] = fileSize; + } + + if (originalLength) { + imgAttributes['data-original-length'] = originalLength; + } + + if (!empty(availableThumbs)) { + imgAttributes['data-thumbs'] = + availableThumbs + .map(([name, size]) => `${name}:${size}`) + .join(' '); + } + } + const nonlazyHTML = originalSrc && prepare( html.tag('img', { ...imgAttributes, - src: thumbSrc, + src: thumbSrc ?? originalSrc, })); if (slots.lazy) { @@ -145,7 +221,7 @@ export default { { ...imgAttributes, class: 'lazy', - 'data-original': thumbSrc, + 'data-original': thumbSrc ?? originalSrc, }), true), ]); diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js index 3bc34845..58bac0d2 100644 --- a/src/content/dependencies/index.js +++ b/src/content/dependencies/index.js @@ -6,7 +6,7 @@ import {fileURLToPath} from 'node:url'; import chokidar from 'chokidar'; import {ESLint} from 'eslint'; -import {color, logWarn} from '#cli'; +import {colors, logWarn} from '#cli'; import contentFunction, {ContentFunctionSpecError} from '#content-function'; import {annotateFunction} from '#sugar'; @@ -30,7 +30,6 @@ export function watchContentDependencies({ const contentDependencies = {}; let emittedReady = false; - let allDependenciesFulfilled = false; let closed = false; let _close = () => {}; @@ -77,12 +76,12 @@ export function watchContentDependencies({ // prematurely find out there aren't any nulls - before the nulls have // been entered at all!). - readdir(metaDirname).then(files => { + readdir(watchPath).then(files => { if (closed) { return; } - const filePaths = files.map(file => path.join(metaDirname, file)); + const filePaths = files.map(file => path.join(watchPath, file)); for (const filePath of filePaths) { if (filePath === metaPath) continue; const functionName = getFunctionName(filePath); @@ -91,7 +90,7 @@ export function watchContentDependencies({ } } - const watcher = chokidar.watch(metaDirname); + const watcher = chokidar.watch(watchPath); watcher.on('all', (event, filePath) => { if (!['add', 'change'].includes(event)) return; @@ -178,7 +177,14 @@ export function watchContentDependencies({ // Just skip newly created files. They'll be processed again when // written. if (spec === undefined) { - contentDependencies[functionName] = null; + // For practical purposes the file is treated as though it doesn't + // even exist (undefined), rather than not being ready yet (null). + // Apart from if existing contents of the file were erased (but not + // the file itself), this value might already be set (to null!) by + // the readdir performed at the beginning to evaluate which files + // should be read and processed at least once before reporting all + // dependencies as ready. + delete contentDependencies[functionName]; return; } @@ -192,7 +198,7 @@ export function watchContentDependencies({ if (logging && emittedReady) { const timestamp = new Date().toLocaleString('en-US', {timeStyle: 'medium'}); - console.log(color.green(`[${timestamp}] Updated ${functionName}`)); + console.log(colors.green(`[${timestamp}] Updated ${functionName}`)); } contentDependencies[functionName] = fn; @@ -219,9 +225,9 @@ export function watchContentDependencies({ } if (typeof error === 'string') { - console.error(color.yellow(error)); + console.error(colors.yellow(error)); } else if (error instanceof ContentFunctionSpecError) { - console.error(color.yellow(error.message)); + console.error(colors.yellow(error.message)); } else { console.error(error); } diff --git a/src/content/dependencies/linkAlbumDynamically.js b/src/content/dependencies/linkAlbumDynamically.js new file mode 100644 index 00000000..3adc64df --- /dev/null +++ b/src/content/dependencies/linkAlbumDynamically.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkAlbumGallery', 'linkAlbum'], + extraDependencies: ['pagePath'], + + relations: (relation, album) => ({ + galleryLink: relation('linkAlbumGallery', album), + infoLink: relation('linkAlbum', album), + }), + + generate: (relations, {pagePath}) => + (pagePath[0] === 'albumGallery' + ? relations.galleryLink + : relations.infoLink), +}; diff --git a/src/content/dependencies/linkFlashAct.js b/src/content/dependencies/linkFlashAct.js new file mode 100644 index 00000000..fbb819ed --- /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/linkGroupDynamically.js b/src/content/dependencies/linkGroupDynamically.js new file mode 100644 index 00000000..90303ed1 --- /dev/null +++ b/src/content/dependencies/linkGroupDynamically.js @@ -0,0 +1,14 @@ +export default { + contentDependencies: ['linkGroupGallery', 'linkGroup'], + extraDependencies: ['pagePath'], + + relations: (relation, group) => ({ + galleryLink: relation('linkGroupGallery', group), + infoLink: relation('linkGroup', group), + }), + + generate: (relations, {pagePath}) => + (pagePath[0] === 'groupGallery' + ? relations.galleryLink + : relations.infoLink), +}; diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index 1cf64c59..d9af726c 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -15,8 +15,9 @@ export default { href: {type: 'string'}, path: {validate: v => v.validateArrayItems(v.isString)}, hash: {type: 'string'}, + linkless: {type: 'boolean', default: false}, - tooltip: {validate: v => v.isString}, + tooltip: {type: 'string'}, attributes: {validate: v => v.isAttributes}, color: {validate: v => v.isColor}, content: {type: 'html'}, @@ -29,27 +30,33 @@ export default { language, to, }) { - let href = slots.href; + let href; let style; let title; - if (href) { - href = encodeURI(href); - } else if (!empty(slots.path)) { - href = to(...slots.path); - } + if (slots.linkless) { + href = null; + } else { + if (slots.href) { + href = encodeURI(slots.href); + } else if (!empty(slots.path)) { + href = to(...slots.path); + } else { + href = ''; + } - if (appendIndexHTML) { - if ( - /^(?!https?:\/\/).+\/$/.test(href) && - href.endsWith('/') - ) { - href += 'index.html'; + if (appendIndexHTML) { + if ( + /^(?!https?:\/\/).+\/$/.test(href) && + href.endsWith('/') + ) { + href += 'index.html'; + } } - } - if (slots.hash) { - href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash; + if (slots.hash) { + href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash; + } } if (slots.color) { diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index e3e2608f..b20b132b 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -1,6 +1,6 @@ export default { contentDependencies: ['linkTemplate'], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations(relation) { return { @@ -26,7 +26,7 @@ export default { preferShortName: {type: 'boolean', default: false}, tooltip: { - validate: v => v.oneOf(v.isBoolean, v.isString), + validate: v => v.oneOf(v.isBoolean, v.isHTML), default: false, }, @@ -36,12 +36,13 @@ export default { }, anchor: {type: 'boolean', default: false}, + linkless: {type: 'boolean', default: false}, attributes: {validate: v => v.isAttributes}, hash: {type: 'string'}, }, - generate(data, relations, slots, {html}) { + generate(data, relations, slots, {html, language}) { const path = [data.pathKey, data.directory]; const name = @@ -51,7 +52,7 @@ export default { const content = (html.isBlank(slots.content) - ? name + ? language.sanitize(name) : slots.content); let color = null; @@ -78,6 +79,7 @@ export default { attributes: slots.attributes, hash: slots.hash, + linkless: slots.linkless, }); }, } diff --git a/src/content/dependencies/listArtTagNetwork.js b/src/content/dependencies/listArtTagNetwork.js new file mode 100644 index 00000000..b3a54747 --- /dev/null +++ b/src/content/dependencies/listArtTagNetwork.js @@ -0,0 +1 @@ +export default {generate() {}}; diff --git a/src/content/dependencies/listTracksWithExtra.js b/src/content/dependencies/listTracksWithExtra.js index 73d25e3d..c9f80f35 100644 --- a/src/content/dependencies/listTracksWithExtra.js +++ b/src/content/dependencies/listTracksWithExtra.js @@ -65,10 +65,14 @@ export default { stitchArrays({ albumLink: relations.albumLinks, date: data.dates, - }).map(({albumLink, date}) => ({ - album: albumLink, - date: language.formatDate(date), - })), + }).map(({albumLink, date}) => + (date + ? { + stringsKey: 'withDate', + album: albumLink, + date: language.formatDate(date), + } + : {album: albumLink})), chunkRows: relations.trackLinks diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js index 9a5ac456..3c2c3521 100644 --- a/src/content/dependencies/transformContent.js +++ b/src/content/dependencies/transformContent.js @@ -53,6 +53,10 @@ export const replacerSpec = { } }, }, + 'flash-act': { + find: 'flashAct', + link: 'flashAct', + }, group: { find: 'group', link: 'groupInfo', @@ -119,6 +123,7 @@ const linkThingRelationMap = { artist: 'linkArtist', artistGallery: 'linkArtistGallery', flash: 'linkFlash', + flashAct: 'linkFlashAct', groupInfo: 'linkGroup', groupGallery: 'linkGroupGallery', listing: 'linkListing', |