diff options
Diffstat (limited to 'src/content')
-rw-r--r-- | src/content/dependencies/generateAlbumInfoPage.js | 4 | ||||
-rw-r--r-- | src/content/dependencies/generateAlbumSidebar.js | 241 | ||||
-rw-r--r-- | src/content/dependencies/generatePageLayout.js | 128 | ||||
-rw-r--r-- | src/content/dependencies/generateTrackInfoPage.js | 4 | ||||
-rw-r--r-- | src/content/dependencies/generateTrackInfoPageContent.js | 23 | ||||
-rw-r--r-- | src/content/dependencies/linkGroup.js | 8 |
6 files changed, 391 insertions, 17 deletions
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js index e5ce193c..21d5ec87 100644 --- a/src/content/dependencies/generateAlbumInfoPage.js +++ b/src/content/dependencies/generateAlbumInfoPage.js @@ -1,6 +1,7 @@ export default { contentDependencies: [ 'generateAlbumInfoPageContent', + 'generateAlbumSidebar', 'generateAlbumSocialEmbed', 'generateAlbumStyleRules', 'generateColorStyleRules', @@ -14,6 +15,7 @@ export default { layout: relation('generatePageLayout'), content: relation('generateAlbumInfoPageContent', album), + sidebar: relation('generateAlbumSidebar', album, null), socialEmbed: relation('generateAlbumSocialEmbed', album), albumStyleRules: relation('generateAlbumStyleRules', album), colorStyleRules: relation('generateColorStyleRules', album.color), @@ -38,6 +40,8 @@ export default { cover: relations.content.cover, mainContent: relations.content.main.content, + ...relations.sidebar, + // socialEmbed: relations.socialEmbed, }); }, diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js new file mode 100644 index 00000000..223b1b7c --- /dev/null +++ b/src/content/dependencies/generateAlbumSidebar.js @@ -0,0 +1,241 @@ +import {empty} from '../../util/sugar.js'; + +function groupRelationships(album) { + return album.groups.map(group => { + const albums = group.albums.filter(album => album.date); + const index = albums.indexOf(album); + + const previousAlbum = (index > 0) && albums[index - 1]; + const nextAlbum = (index < albums.length - 1) && albums[index + 1]; + + return {group, previousAlbum, nextAlbum}; + }); +} + +export default { + contentDependencies: [ + 'linkAlbum', + 'linkExternal', + 'linkGroup', + 'linkTrack', + ], + + extraDependencies: [ + 'getColors', + 'html', + 'language', + 'transformMultiline', + ], + + relations(relation, album, track) { + const relations = {}; + + relations.albumLink = + relation('linkAlbum', album); + + relations.trackLinks = + album.trackSections.map(trackSection => + trackSection.tracks.map(track => + relation('linkTrack', track))); + + relations.groupLinks = + groupRelationships(album) + .map(({group, previousAlbum, nextAlbum}) => ({ + groupLink: + relation('linkGroup', group), + + externalLinks: + group.urls.map(url => + relation('linkExternal', url)), + + previousAlbumLink: + previousAlbum && + relation('linkAlbum', previousAlbum), + + nextAlbumLink: + nextAlbum && + relation('linkAlbum', nextAlbum), + })) + + return relations; + }, + + data(album, track) { + const data = {}; + + data.isAlbumPage = !track; + data.isTrackPage = !!track; + + data.hasTrackNumbers = album.hasTrackNumbers; + + data.trackSectionInfo = + album.trackSections.map(trackSection => ({ + name: trackSection.name, + color: trackSection.color, + isDefaultTrackSection: trackSection.isDefaultTrackSection, + + firstTrackNumber: trackSection.startIndex + 1, + lastTrackNumber: trackSection.startIndex + trackSection.tracks.length, + + includesCurrentTrack: track && trackSection.tracks.includes(track), + currentTrackIndex: trackSection.tracks.indexOf(track), + })); + + data.groupInfo = + album.groups.map(group => ({ + description: group.descriptionShort, + })); + + return data; + }, + + generate(data, relations, { + getColors, + html, + language, + transformMultiline, + }) { + const {isTrackPage, isAlbumPage} = data; + + const trackListPart = html.tags([ + html.tag('h1', relations.albumLink), + data.trackSectionInfo.map( + ({ + name, + color, + isDefaultTrackSection, + + firstTrackNumber, + lastTrackNumber, + + includesCurrentTrack, + currentTrackIndex, + }, index) => { + const trackLinks = relations.trackLinks[index]; + + const sectionName = + html.tag('span', {class: 'group-name'}, + (isDefaultTrackSection + ? language.$('albumSidebar.trackList.fallbackSectionName') + : name)); + + let style; + if (color) { + const {primary} = getColors(color); + style = `--primary-color: ${primary}`; + } + + const trackListItems = + trackLinks.map((trackLink, index) => + html.tag('li', + { + class: + includesCurrentTrack && + index === currentTrackIndex && + 'current', + }, + language.$('albumSidebar.trackList.item', { + track: trackLink, + }))); + + return html.tag('details', + { + class: includesCurrentTrack && 'current', + + 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. + isTrackPage && + + // Only expand the track section which includes the track + // currently being viewed by default. + includesCurrentTrack), + }, + [ + html.tag('summary', {style}, + html.tag('span', + (data.hasTrackNumbers + ? language.$('albumSidebar.trackList.group.withRange', { + group: sectionName, + range: `${firstTrackNumber}–${lastTrackNumber}` + }) + : language.$('albumSidebar.trackList.group', { + group: sectionName, + })))), + + (data.hasTrackNumbers + ? html.tag('ol', + {start: firstTrackNumber}, + trackListItems) + : html.tag('ul', trackListItems)), + ]); + }), + ]); + + const groupParts = data.groupInfo.map( + ({description}, index) => { + const links = relations.groupLinks[index]; + + return html.tags([ + html.tag('h1', + language.$('albumSidebar.groupBox.title', { + group: links.groupLink, + })), + + isAlbumPage && + transformMultiline(description), + + !empty(links.externalLinks) && + html.tag('p', + language.$('releaseInfo.visitOn', { + links: language.formatDisjunctionList(links.externalLinks), + })), + + isAlbumPage && + links.nextAlbumLink && + html.tag('p', {class: 'group-chronology-link'}, + language.$('albumSidebar.groupBox.next', { + album: links.nextAlbumLink, + })), + + isAlbumPage && + links.previousAlbumLink && + html.tag('p', {class: 'group-chronology-link'}, + language.$('albumSidebar.groupBox.previous', { + album: links.previousAlbumLink, + })), + ]); + }); + + if (isAlbumPage) { + return { + // leftSidebarStickyMode: 'last', + leftSidebarMultiple: [ + ...groupParts.map(groupPart => ({content: groupPart})), + {content: trackListPart}, + ], + }; + } else { + return { + // leftSidebarStickyMode: 'column', + leftSidebarMultiple: [ + {content: trackListPart}, + // ...groupParts.map(groupPart => ({content: groupPart})), + { + content: + groupParts + .flatMap((part, i) => [ + part, + i < groupParts.length - 1 && + html.tag('hr', { + style: `border-color: var(--primary-color); border-style: none none dotted none` + }) + ]) + .filter(Boolean), + }, + ], + }; + } + }, +}; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 98b2d350..1ea5ce24 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -29,6 +29,47 @@ export default { transformMultiline, wikiInfo, }) { + const sidebarSlots = side => ({ + // Content is a flat HTML array. It'll generate one sidebar section + // if specified. + [side + 'Content']: {type: 'html'}, + + // Multiple is an array of {content: (HTML)} objects. Each of these + // will generate one sidebar section. + [side + 'Multiple']: { + validate: v => + v.arrayOf( + v.validateProperties({ + content: v.isHTML, + })), + }, + + // Sticky mode controls which sidebar section(s), if any, follow the + // scroll position, "sticking" to the top of the browser viewport. + // + // 'last' - last or only sidebar box is sticky + // 'column' - entire column, incl. multiple boxes from top, is sticky + // 'none' - sidebar not sticky at all, stays at top of page + // + // Note: This doesn't affect the content of any sidebar section, only + // the whole section's containing box (or the sidebar column as a whole). + [side + 'StickyMode']: { + validate: v => v.is('last', 'column', 'static'), + }, + + // Collapsing sidebars disappear when the viewport is sufficiently + // thin. (This is the default.) Override as false to make the sidebar + // stay visible in thinner viewports, where the page layout will be + // reflowed so the sidebar is as wide as the screen and appears below + // nav, above the main content. + [side + 'Collapse']: {type: 'boolean', default: true}, + + // Wide sidebars generally take up more horizontal space in the normal + // page layout, and should be used if the content of the sidebar has + // a greater than typical focus compared to main content. + [side + 'Wide']: {type: 'boolean', defualt: false}, + }); + return html.template({ annotation: 'generatePageLayout', @@ -36,15 +77,8 @@ export default { title: {type: 'html'}, cover: {type: 'html'}, - mainContent: {type: 'html'}, - footerContent: {type: 'html'}, socialEmbed: {type: 'html'}, - headingMode: { - validate: v => v.is('sticky', 'static'), - default: 'static', - }, - styleRules: { validate: v => v.arrayOf(v.isString), default: [], @@ -54,6 +88,24 @@ export default { validate: v => v.arrayOf(v.isString), default: [], }, + + // Main + + mainContent: {type: 'html'}, + + headingMode: { + validate: v => v.is('sticky', 'static'), + default: 'static', + }, + + // Sidebars + + ...sidebarSlots('leftSidebar'), + ...sidebarSlots('rightSidebar'), + + // Nav & Footer + + footerContent: {type: 'html'}, }, content(slots) { @@ -114,6 +166,52 @@ export default { relations.footerLocalizationLinks, ]); + const generateSidebarHTML = (side, id) => { + const content = slots[side + 'Content']; + const multiple = slots[side + 'Multiple']; + const stickyMode = slots[side + 'StickyMode']; + const wide = slots[side + 'Wide']; + const collapse = slots[side + 'Collapse']; + + let sidebarClasses = []; + let sidebarContent = html.blank(); + + if (!html.isBlank(content)) { + sidebarClasses = ['sidebar']; + sidebarContent = content; + } else if (multiple) { + sidebarClasses = ['sidebar-multiple']; + sidebarContent = + multiple + .filter(Boolean) + .map(({content}) => + html.tag('div', + { + [html.onlyIfContent]: true, + class: 'sidebar', + }, + content)); + } + + return html.tag('div', + { + [html.onlyIfContent]: true, + id, + class: [ + 'sidebar-column', + wide && 'wide', + !collapse && 'no-hide', + stickyMode !== 'static' && `sticky-${stickyMode}`, + ...sidebarClasses, + ], + }, + sidebarContent); + } + + const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left'); + const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right'); + const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse; + const layoutHTML = [ // navHTML, // banner.position === 'top' && bannerHTML, @@ -122,18 +220,18 @@ 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', + !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', ], }, [ - // sidebarLeftHTML, + sidebarLeftHTML, mainHTML, - // sidebarRightHTML, + sidebarRightHTML, ]), // banner.position === 'bottom' && bannerHTML, footerHTML, diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js index f7f14573..0519b7e8 100644 --- a/src/content/dependencies/generateTrackInfoPage.js +++ b/src/content/dependencies/generateTrackInfoPage.js @@ -1,6 +1,7 @@ export default { contentDependencies: [ 'generateTrackInfoPageContent', + 'generateAlbumSidebar', 'generateAlbumStyleRules', 'generateColorStyleRules', 'generatePageLayout', @@ -13,6 +14,7 @@ export default { layout: relation('generatePageLayout'), content: relation('generateTrackInfoPageContent', track), + sidebar: relation('generateAlbumSidebar', track.album, track), albumStyleRules: relation('generateAlbumStyleRules', track.album), colorStyleRules: relation('generateColorStyleRules', track.color), }; @@ -35,6 +37,8 @@ export default { cover: relations.content.cover, mainContent: relations.content.main.content, + + ...relations.sidebar, }); }, } diff --git a/src/content/dependencies/generateTrackInfoPageContent.js b/src/content/dependencies/generateTrackInfoPageContent.js index 0ebb4121..3f37a0c9 100644 --- a/src/content/dependencies/generateTrackInfoPageContent.js +++ b/src/content/dependencies/generateTrackInfoPageContent.js @@ -33,6 +33,10 @@ export default { } else if (album.hasCoverArt) { relations.cover = relation('generateCoverArtwork', album.artTags); + relations.coverArtistLinks = null; + } else { + relations.cover = null; + relations.coverArtistLinks = null; } relations.artistLinks = @@ -95,6 +99,15 @@ export default { }) { const content = {}; + const formatContributions = contributionLinks => + language.formatConjunctionList( + contributionLinks.map(link => + link + .slots({ + showContribution: true, + showIcons: true, + }))); + if (data.hasUniqueCoverArt) { content.cover = relations.cover .slots({ @@ -114,6 +127,8 @@ export default { data.coverArtFileExtension, ], }); + } else { + content.cover = null; } content.main = { @@ -125,10 +140,14 @@ export default { [html.joinChildren]: html.tag('br'), }, [ !empty(relations.artistLinks) && - language.$('releaseInfo.by', {artists: relations.artistLinks}), + language.$('releaseInfo.by', { + artists: formatContributions(relations.artistLinks), + }), !empty(relations.coverArtistLinks) && - language.$('releaseInfo.coverArtBy', {artists: relations.coverArtistLinks}), + language.$('releaseInfo.coverArtBy', { + artists: formatContributions(relations.coverArtistLinks), + }), data.date && language.$('releaseInfo.released', { diff --git a/src/content/dependencies/linkGroup.js b/src/content/dependencies/linkGroup.js new file mode 100644 index 00000000..ebab1b5b --- /dev/null +++ b/src/content/dependencies/linkGroup.js @@ -0,0 +1,8 @@ +export default { + contentDependencies: ['linkThing'], + + relations: (relation, group) => + ({link: relation('linkThing', 'localized.groupInfo', group)}), + + generate: (relations) => relations.link, +}; |