From da5ba89f4171c395f5e7fa2c764272e7d2de93f3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Fri, 30 Jun 2023 20:45:00 -0300 Subject: content: generateArtistGroupContributionsInfo: table layout 👻 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../generateArtistGroupContributionsInfo.js | 161 ++++++++++++++++----- src/content/dependencies/generateArtistInfoPage.js | 47 ++++-- .../generateArtistInfoPageArtworksChunkedList.js | 112 +++++++------- .../generateArtistInfoPageChunkedList.js | 16 ++ .../generateArtistInfoPageTracksChunkedList.js | 128 ++++++++-------- src/data/things/language.js | 1 + src/static/client.js | 34 +++++ src/static/site4.css | 19 +++ src/strings-default.json | 17 ++- 9 files changed, 371 insertions(+), 164 deletions(-) create mode 100644 src/content/dependencies/generateArtistInfoPageChunkedList.js diff --git a/src/content/dependencies/generateArtistGroupContributionsInfo.js b/src/content/dependencies/generateArtistGroupContributionsInfo.js index 0ecfaa3..1e7086e 100644 --- a/src/content/dependencies/generateArtistGroupContributionsInfo.js +++ b/src/content/dependencies/generateArtistGroupContributionsInfo.js @@ -1,4 +1,9 @@ -import {stitchArrays, unique} from '../../util/sugar.js'; +import { + empty, + filterProperties, + stitchArrays, + unique, +} from '../../util/sugar.js'; export default { contentDependencies: ['linkGroup'], @@ -47,29 +52,48 @@ export default { allGroupsOrdered .sort((a, b) => groupToCountMap.get(b) - groupToCountMap.get(a)); + // The filter here ensures all displayed groups have at least some duration + // when sorting by duration. const groupsSortedByDuration = allGroupsOrdered .filter(group => groupToDurationMap.get(group) > 0) .sort((a, b) => groupToDurationMap.get(b) - groupToDurationMap.get(a)); - const groupCounts = + const groupCountsSortedByCount = groupsSortedByCount .map(group => groupToCountMap.get(group)); - const groupDurations = + const groupDurationsSortedByCount = + groupsSortedByCount + .map(group => groupToDurationMap.get(group)); + + const groupDurationsApproximateSortedByCount = + groupsSortedByCount + .map(group => groupToDurationCountMap.get(group) > 1); + + const groupCountsSortedByDuration = + groupsSortedByDuration + .map(group => groupToCountMap.get(group)); + + const groupDurationsSortedByDuration = groupsSortedByDuration .map(group => groupToDurationMap.get(group)); - const groupDurationsApproximate = + const groupDurationsApproximateSortedByDuration = groupsSortedByDuration .map(group => groupToDurationCountMap.get(group) > 1); return { groupsSortedByCount, groupsSortedByDuration, - groupCounts, - groupDurations, - groupDurationsApproximate, + + groupCountsSortedByCount, + groupDurationsSortedByCount, + groupDurationsApproximateSortedByCount, + + groupCountsSortedByDuration, + groupDurationsSortedByDuration, + groupDurationsApproximateSortedByDuration, }; }, @@ -86,39 +110,104 @@ export default { }, data(query) { - return { - groupCounts: query.groupCounts, - groupDurations: query.groupDurations, - groupDurationsApproximate: query.groupDurationsApproximate, - }; + return filterProperties(query, [ + 'groupCountsSortedByCount', + 'groupDurationsSortedByCount', + 'groupDurationsApproximateSortedByCount', + + 'groupCountsSortedByDuration', + 'groupDurationsSortedByDuration', + 'groupDurationsApproximateSortedByDuration', + ]); }, slots: { - mode: { - validate: v => v.is('count', 'duration'), - }, + title: {type: 'html'}, + showBothColumns: {type: 'boolean'}, + showSortButton: {type: 'boolean'}, + visible: {type: 'boolean', default: true}, + + sort: {validate: v => v.is('count', 'duration')}, + countUnit: {validate: v => v.is('tracks', 'artworks')}, }, - generate(data, relations, slots, {language}) { - return ( - language.formatUnitList( - (slots.mode === 'count' - ? stitchArrays({ - groupLink: relations.groupLinksSortedByCount, - count: data.groupCounts, - }).map(({groupLink, count}) => - language.$('artistPage.groupsLine.item.withCount', { - group: groupLink, - count, - })) - : stitchArrays({ - groupLink: relations.groupLinksSortedByDuration, - duration: data.groupDurations, - approximate: data.groupDurationsApproximate, - }).map(({groupLink, duration, approximate}) => - language.$('artistPage.groupsLine.item.withDuration', { - group: groupLink, - duration: language.formatDuration(duration, {approximate}), - }))))); + generate(data, relations, slots, {html, language}) { + if (slots.sort === 'count' && empty(relations.groupLinksSortedByCount)) { + return html.blank(); + } else if (slots.sort === 'duration' && empty(relations.groupLinksSortedByDuration)) { + return html.blank(); + } + + const getCounts = counts => + counts.map(count => { + switch (slots.countUnit) { + case 'tracks': return language.countTracks(count, {unit: true}); + case 'artworks': return language.countArtworks(count, {unit: true}); + } + }); + + // We aren't displaying the "~" approximate symbol here for now. + // The general notion that these sums aren't going to be 100% accurate + // is made clear by the "XYZ has contributed ~1:23:45 hours of music..." + // line that's always displayed above this table. + const getDurations = (durations, approximate) => + stitchArrays({ + duration: durations, + approximate: approximate, + }).map(({duration}) => language.formatDuration(duration)); + + const topLevelClasses = [ + 'group-contributions-sorted-by-' + slots.sort, + slots.visible && 'visible', + ]; + + return html.tags([ + html.tag('dt', {class: topLevelClasses}, + (slots.showSortButton + ? language.$('artistPage.groupContributions.title.withSortButton', { + title: slots.title, + sort: + html.tag('a', {href: '#', class: 'group-contributions-sort-button'}, + (slots.sort === 'count' + ? language.$('artistPage.groupContributions.title.sorting.count') + : language.$('artistPage.groupContributions.title.sorting.duration'))), + }) + : slots.title)), + + html.tag('dd', {class: topLevelClasses}, + html.tag('ul', {class: 'group-contributions-table', role: 'list'}, + (slots.sort === 'count' + ? stitchArrays({ + group: relations.groupLinksSortedByCount, + count: getCounts(data.groupCountsSortedByCount), + duration: getDurations(data.groupDurationsSortedByCount, data.groupDurationsApproximateSortedByCount), + }).map(({group, count, duration}) => + html.tag('li', + html.tag('div', {class: 'group-contributions-row'}, [ + group, + html.tag('span', {class: 'group-contributions-metrics'}, + // When sorting by count, duration details aren't necessarily + // available for all items. + (slots.showBothColumns && duration + ? language.$('artistPage.groupContributions.item.countDurationAccent', {count, duration}) + : language.$('artistPage.groupContributions.item.countAccent', {count}))), + ]))) + : stitchArrays({ + group: relations.groupLinksSortedByDuration, + count: getCounts(data.groupCountsSortedByDuration), + duration: getDurations(data.groupDurationsSortedByDuration, data.groupDurationsApproximateSortedByCount), + }).map(({group, count, duration}) => + html.tag('li', + html.tag('div', {class: 'group-contributions-row'}, [ + group, + html.tag('span', {class: 'group-contributions-metrics'}, + // Count details are always available, since they're just the + // number of contributions directly. And duration details are + // guaranteed for every item when sorting by duration. + (slots.showBothColumns + ? language.$('artistPage.groupContributions.item.durationCountAccent', {duration, count}) + : language.$('artistPage.groupContributions.item.durationAccent', {duration}))), + ])))))), + ]); }, }; diff --git a/src/content/dependencies/generateArtistInfoPage.js b/src/content/dependencies/generateArtistInfoPage.js index 1f6c66f..7f79a60 100644 --- a/src/content/dependencies/generateArtistInfoPage.js +++ b/src/content/dependencies/generateArtistInfoPage.js @@ -218,14 +218,30 @@ export default { }), })), - // TODO: How to check if a template is blank!? - // !html.isBlank(sec.tracks.groupInfo.content) && - html.tag('p', - language.$('artistPage.musicGroupsLine', { - groups: sec.tracks.groupInfo.slot('mode', 'duration'), - })), + sec.tracks.list + .slots({ + groupInfo: [ + sec.tracks.groupInfo + .clone() + .slots({ + title: language.$('artistPage.groupContributions.title.music'), + showSortButton: true, + sort: 'count', + countUnit: 'tracks', + visible: true, + }), - sec.tracks.list, + sec.tracks.groupInfo + .clone() + .slots({ + title: language.$('artistPage.groupContributions.title.music'), + showSortButton: true, + sort: 'duration', + countUnit: 'tracks', + visible: false, + }), + ], + }), ], sec.artworks && [ @@ -236,11 +252,6 @@ export default { title: language.$('artistPage.artList.title'), }), - html.tag('p', - language.$('artistPage.artGroupsLine', { - groups: sec.artworks.groupInfo.slot('mode', 'count'), - })), - sec.artworks.artistGalleryLink && html.tag('p', language.$('artistPage.viewArtGallery.orBrowseList', { @@ -249,7 +260,17 @@ export default { }), })), - sec.artworks.list, + sec.artworks.list + .slots({ + groupInfo: + sec.artworks.groupInfo + .slots({ + title: language.$('artistPage.groupContributions.title.artworks'), + showBothColumns: false, + sort: 'count', + countUnit: 'artworks', + }), + }), ], sec.flashes && [ diff --git a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js index 8654859..656121c 100644 --- a/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageArtworksChunkedList.js @@ -9,6 +9,7 @@ import { export default { contentDependencies: [ 'generateArtistInfoPageChunk', + 'generateArtistInfoPageChunkedList', 'generateArtistInfoPageChunkItem', 'generateArtistInfoPageOtherArtistLinks', 'linkAlbum', @@ -81,6 +82,9 @@ export default { relations(relation, query, artist) { return { + chunkedList: + relation('generateArtistInfoPageChunkedList'), + chunks: query.chunks.map(() => relation('generateArtistInfoPageChunk')), @@ -120,63 +124,65 @@ export default { }, generate(data, relations, {html, language}) { - return html.tag('dl', - stitchArrays({ - chunk: relations.chunks, - albumLink: relations.albumLinks, - date: data.chunkDates, - - items: relations.items, - itemTrackLinks: relations.itemTrackLinks, - itemOtherArtistLinks: relations.itemOtherArtistLinks, - itemTypes: data.itemTypes, - itemContributions: data.itemContributions, - }).map(({ - chunk, - albumLink, - date, - - items, - itemTrackLinks, - itemOtherArtistLinks, - itemTypes, - itemContributions, - }) => - chunk.slots({ - mode: 'album', + return relations.chunkedList.slots({ + chunks: + stitchArrays({ + chunk: relations.chunks, + albumLink: relations.albumLinks, + date: data.chunkDates, + + items: relations.items, + itemTrackLinks: relations.itemTrackLinks, + itemOtherArtistLinks: relations.itemOtherArtistLinks, + itemTypes: data.itemTypes, + itemContributions: data.itemContributions, + }).map(({ + chunk, albumLink, date, - items: - stitchArrays({ - item: items, - trackLink: itemTrackLinks, - otherArtistLinks: itemOtherArtistLinks, - type: itemTypes, - contribution: itemContributions, - }).map(({ - item, - trackLink, - otherArtistLinks, - type, - contribution, - }) => - item.slots({ + items, + itemTrackLinks, + itemOtherArtistLinks, + itemTypes, + itemContributions, + }) => + chunk.slots({ + mode: 'album', + albumLink, + date, + + items: + stitchArrays({ + item: items, + trackLink: itemTrackLinks, + otherArtistLinks: itemOtherArtistLinks, + type: itemTypes, + contribution: itemContributions, + }).map(({ + item, + trackLink, otherArtistLinks, + type, contribution, - - content: - (type === 'trackCover' - ? language.$('artistPage.creditList.entry.track', { - track: trackLink, - }) - : html.tag('i', - language.$('artistPage.creditList.entry.album.' + { - albumWallpaper: 'wallpaperArt', - albumBanner: 'bannerArt', - albumCover: 'coverArt', - }[type]))), - })), - }))); + }) => + item.slots({ + otherArtistLinks, + contribution, + + content: + (type === 'trackCover' + ? language.$('artistPage.creditList.entry.track', { + track: trackLink, + }) + : html.tag('i', + language.$('artistPage.creditList.entry.album.' + { + albumWallpaper: 'wallpaperArt', + albumBanner: 'bannerArt', + albumCover: 'coverArt', + }[type]))), + })), + })), + }); }, }; diff --git a/src/content/dependencies/generateArtistInfoPageChunkedList.js b/src/content/dependencies/generateArtistInfoPageChunkedList.js new file mode 100644 index 0000000..a0334cb --- /dev/null +++ b/src/content/dependencies/generateArtistInfoPageChunkedList.js @@ -0,0 +1,16 @@ +export default { + extraDependencies: ['html'], + + slots: { + groupInfo: {type: 'html'}, + chunks: {type: 'html'}, + }, + + generate(slots, {html}) { + return ( + html.tag('dl', [ + slots.groupInfo, + slots.chunks, + ])); + }, +}; diff --git a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js index 4dd4d46..d6ae9ae 100644 --- a/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js +++ b/src/content/dependencies/generateArtistInfoPageTracksChunkedList.js @@ -9,13 +9,14 @@ import { export default { contentDependencies: [ 'generateArtistInfoPageChunk', + 'generateArtistInfoPageChunkedList', 'generateArtistInfoPageChunkItem', 'generateArtistInfoPageOtherArtistLinks', 'linkAlbum', 'linkTrack', ], - extraDependencies: ['html', 'language'], + extraDependencies: ['language'], query(artist) { const entries = [ @@ -52,6 +53,9 @@ export default { relations(relation, query, artist) { return { + chunkedList: + relation('generateArtistInfoPageChunkedList'), + chunks: query.chunks.map(() => relation('generateArtistInfoPageChunk')), @@ -107,73 +111,75 @@ export default { }; }, - generate(data, relations, {html, language}) { - return html.tag('dl', - stitchArrays({ - chunk: relations.chunks, - albumLink: relations.albumLinks, - date: data.chunkDates, - duration: data.chunkDurations, - durationApproximate: data.chunkDurationsApproximate, - - items: relations.items, - trackLinks: relations.trackLinks, - trackOtherArtistLinks: relations.trackOtherArtistLinks, - trackDurations: data.trackDurations, - trackContributions: data.trackContributions, - trackRereleases: data.trackRereleases, - }).map(({ - chunk, - albumLink, - date, - duration, - durationApproximate, - - items, - trackLinks, - trackOtherArtistLinks, - trackDurations, - trackContributions, - trackRereleases, - }) => - chunk.slots({ - mode: 'album', + generate(data, relations, {language}) { + return relations.chunkedList.slots({ + chunks: + stitchArrays({ + chunk: relations.chunks, + albumLink: relations.albumLinks, + date: data.chunkDates, + duration: data.chunkDurations, + durationApproximate: data.chunkDurationsApproximate, + + items: relations.items, + trackLinks: relations.trackLinks, + trackOtherArtistLinks: relations.trackOtherArtistLinks, + trackDurations: data.trackDurations, + trackContributions: data.trackContributions, + trackRereleases: data.trackRereleases, + }).map(({ + chunk, albumLink, date, duration, durationApproximate, - items: - stitchArrays({ - item: items, - trackLink: trackLinks, - otherArtistLinks: trackOtherArtistLinks, - duration: trackDurations, - contribution: trackContributions, - rerelease: trackRereleases, - }).map(({ - item, - trackLink, - otherArtistLinks, - duration, - contribution, - rerelease, - }) => - item.slots({ + items, + trackLinks, + trackOtherArtistLinks, + trackDurations, + trackContributions, + trackRereleases, + }) => + chunk.slots({ + mode: 'album', + albumLink, + date, + duration, + durationApproximate, + + items: + stitchArrays({ + item: items, + trackLink: trackLinks, + otherArtistLinks: trackOtherArtistLinks, + duration: trackDurations, + contribution: trackContributions, + rerelease: trackRereleases, + }).map(({ + item, + trackLink, otherArtistLinks, + duration, contribution, rerelease, - - content: - (duration - ? language.$('artistPage.creditList.entry.track.withDuration', { - track: trackLink, - duration: language.formatDuration(duration), - }) - : language.$('artistPage.creditList.entry.track', { - track: trackLink, - })), - })), - }))); + }) => + item.slots({ + otherArtistLinks, + contribution, + rerelease, + + content: + (duration + ? language.$('artistPage.creditList.entry.track.withDuration', { + track: trackLink, + duration: language.formatDuration(duration), + }) + : language.$('artistPage.creditList.entry.track', { + track: trackLink, + })), + })), + })), + }); }, }; diff --git a/src/data/things/language.js b/src/data/things/language.js index ec62de4..004678d 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -311,6 +311,7 @@ const countHelper = (stringKey, argName = stringKey) => Object.assign(Language.prototype, { countAdditionalFiles: countHelper('additionalFiles', 'files'), countAlbums: countHelper('albums'), + countArtworks: countHelper('artworks'), countCommentaryEntries: countHelper('commentaryEntries', 'entries'), countContributions: countHelper('contributions'), countCoverArts: countHelper('coverArts'), diff --git a/src/static/client.js b/src/static/client.js index e75fbd9..2f0b6ae 100644 --- a/src/static/client.js +++ b/src/static/client.js @@ -870,3 +870,37 @@ function loadImage(imageUrl, onprogress) { xhr.send(); }); } + +// Group contributions table ------------------------------ + +const groupContributionsTableInfo = + Array.from(document.querySelectorAll('#content dl')) + .filter(dl => dl.querySelector('a.group-contributions-sort-button')) + .map(dl => ({ + sortingByCountLink: dl.querySelector('dt.group-contributions-sorted-by-count a.group-contributions-sort-button'), + sortingByDurationLink: dl.querySelector('dt.group-contributions-sorted-by-duration a.group-contributions-sort-button'), + sortingByCountElements: dl.querySelectorAll('.group-contributions-sorted-by-count'), + sortingByDurationElements: dl.querySelectorAll('.group-contributions-sorted-by-duration'), + })); + +function sortGroupContributionsTableBy(info, sort) { + const [showThese, hideThese] = + (sort === 'count' + ? [info.sortingByCountElements, info.sortingByDurationElements] + : [info.sortingByDurationElements, info.sortingByCountElements]); + + for (const element of showThese) element.classList.add('visible'); + for (const element of hideThese) element.classList.remove('visible'); +} + +for (const info of groupContributionsTableInfo) { + info.sortingByCountLink.addEventListener('click', evt => { + evt.preventDefault(); + sortGroupContributionsTableBy(info, 'duration'); + }); + + info.sortingByDurationLink.addEventListener('click', evt => { + evt.preventDefault(); + sortGroupContributionsTableBy(info, 'count'); + }); +} diff --git a/src/static/site4.css b/src/static/site4.css index d7801f2..c7ecc39 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -722,6 +722,25 @@ li > ul { margin-top: 5px; } +.group-contributions-table { + display: inline-block; +} + +.group-contributions-table .group-contributions-row { + display: flex; + justify-content: space-between; +} + +.group-contributions-table .group-contributions-metrics { + margin-left: 1.5ch; + white-space: nowrap; +} + +.group-contributions-sorted-by-count:not(.visible), +.group-contributions-sorted-by-duration:not(.visible) { + display: none; +} + /* Images */ .image-container { diff --git a/src/strings-default.json b/src/strings-default.json index 0a93990..4771dc4 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -19,10 +19,16 @@ "count.albums.withUnit.zero": "", "count.albums.withUnit.one": "{ALBUMS} album", "count.albums.withUnit.two": "", - "count.albums.withUnit.two": "", "count.albums.withUnit.few": "", "count.albums.withUnit.many": "", "count.albums.withUnit.other": "{ALBUMS} albums", + "count.artworks": "{ARTWORKS}", + "count.artworks.withUnit.zero": "", + "count.artworks.withUnit.one": "{ARTWORKS} artwork", + "count.artworks.withUnit.two": "", + "count.artworks.withUnit.few": "", + "count.artworks.withUnit.many": "", + "count.artworks.withUnit.other": "{ARTWORKS} artworks", "count.commentaryEntries": "{ENTRIES}", "count.commentaryEntries.withUnit.zero": "", "count.commentaryEntries.withUnit.one": "{ENTRIES} entry", @@ -279,6 +285,15 @@ "artistPage.artGroupsLine": "Contributed art to groups: {GROUPS}", "artistPage.groupsLine.item.withCount": "{GROUP} ({COUNT})", "artistPage.groupsLine.item.withDuration": "{GROUP} ({DURATION})", + "artistPage.groupContributions.title.music": "Contributed music to groups:", + "artistPage.groupContributions.title.artworks": "Contributed artworks to groups:", + "artistPage.groupContributions.title.withSortButton": "{TITLE} ({SORT})", + "artistPage.groupContributions.title.sorting.count": "Sorting by count.", + "artistPage.groupContributions.title.sorting.duration": "Sorting by duration.", + "artistPage.groupContributions.item.countAccent": "({COUNT})", + "artistPage.groupContributions.item.durationAccent": "({DURATION})", + "artistPage.groupContributions.item.countDurationAccent": "({COUNT} — {DURATION})", + "artistPage.groupContributions.item.durationCountAccent": "({DURATION} — {COUNT})", "artistPage.trackList.title": "Tracks", "artistPage.artList.title": "Artworks", "artistPage.flashList.title": "Flashes & Games", -- cgit 1.3.0-6-gf8a5