diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2025-05-04 19:16:15 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2025-05-06 15:20:24 -0300 |
commit | 72f6fc9659fe4cc6ea60229e636421a78fe98f29 (patch) | |
tree | f5f073bec80c68e543015aadd6d6a4384ac5a8b7 | |
parent | fedd22c523ec5c20b098cc478170ef096ab1b58e (diff) |
content, client, css: generateGroupGalleryPage: series view
-rw-r--r-- | src/content/dependencies/generateExpandableGallerySection.js | 92 | ||||
-rw-r--r-- | src/content/dependencies/generateGroupGalleryPage.js | 93 | ||||
-rw-r--r-- | src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js | 37 | ||||
-rw-r--r-- | src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js | 26 | ||||
-rw-r--r-- | src/content/dependencies/generateGroupGalleryPageSeriesSection.js | 129 | ||||
-rw-r--r-- | src/data/things/language.js | 43 | ||||
-rw-r--r-- | src/static/css/site.css | 42 | ||||
-rw-r--r-- | src/static/js/client/expandable-gallery-section.js | 77 | ||||
-rw-r--r-- | src/static/js/client/index.js | 2 | ||||
-rw-r--r-- | src/static/js/client/quick-description.js | 2 | ||||
-rw-r--r-- | src/strings-default.yaml | 30 |
11 files changed, 544 insertions, 29 deletions
diff --git a/src/content/dependencies/generateExpandableGallerySection.js b/src/content/dependencies/generateExpandableGallerySection.js new file mode 100644 index 00000000..122ca4b1 --- /dev/null +++ b/src/content/dependencies/generateExpandableGallerySection.js @@ -0,0 +1,92 @@ +export default { + contentDependencies: ['generateContentHeading'], + extraDependencies: ['html', 'language'], + + relations: (relation) => ({ + contentHeading: + relation('generateContentHeading'), + }), + + slots: { + title: { + type: 'html', + mutable: false, + }, + + contentAboveCut: { + type: 'html', + mutable: false, + }, + + contentBelowCut: { + type: 'html', + mutable: false, + }, + + caption: { + type: 'html', + mutable: false, + }, + + expandCue: { + type: 'html', + mutable: false, + }, + + collapseCue: { + type: 'html', + mutable: false, + }, + }, + + generate: (relations, slots, {html, language}) => + html.tag('section', {class: 'expandable-gallery-section'}, [ + relations.contentHeading.slots({ + tag: 'h2', + title: slots.title, + }), + + html.tag('div', {class: 'section-content-above-cut'}, + {[html.onlyIfContent]: true}, + + slots.contentAboveCut), + + html.tag('div', {class: 'section-content-below-cut'}, + {[html.onlyIfContent]: true}, + + !html.isBlank(slots.contentBelowCut) && + {style: 'display: none'}, + + slots.contentBelowCut), + + html.tag('div', {class: 'section-expando'}, + {[html.onlyIfSiblings]: true}, + + html.tag('div', {class: 'section-expando-content'}, + {[html.joinChildren]: html.tag('br')}, + + [ + html.tag('span', {class: 'section-caption'}, + slots.caption), + + !html.isBlank(slots.contentBelowCut) && + language.$('misc.coverGrid.expandCollapseCue', { + cue: + html.tag('a', {class: 'section-expando-toggle'}, + {href: '#'}, + + {[html.joinChildren]: ''}, + {[html.noEdgeWhitespace]: true}, + + [ + html.tag('span', {class: 'section-expand-cue'}, + slots.expandCue), + + html.tag('span', {class: 'section-collapse-cue'}, + {style: 'display: none'}, + slots.collapseCue), + ]), + }), + ])), + ]), +}; diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index 453155cb..535a2bc9 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -4,10 +4,11 @@ import {filterItemsForCarousel, getTotalDuration} from '#wiki-data'; export default { contentDependencies: [ 'generateCoverCarousel', - 'generateGroupGalleryPageAlbumGrid', + 'generateGroupGalleryPageAlbumsByDateView', + 'generateGroupGalleryPageAlbumsBySeriesView', 'generateGroupNavLinks', 'generateGroupSecondaryNav', - 'generateGroupSidebar', + 'generateIntrapageDotSwitcher', 'generatePageLayout', 'generateQuickDescription', 'image', @@ -47,11 +48,6 @@ export default { ? relation('generateGroupSecondaryNav', group) : null), - sidebar: - (sprawl.enableGroupUI - ? relation('generateGroupSidebar', group) - : null), - coverCarousel: relation('generateCoverCarousel'), @@ -66,8 +62,14 @@ export default { quickDescription: relation('generateQuickDescription', group), - albumGrid: - relation('generateGroupGalleryPageAlbumGrid', query.allAlbums), + albumViewSwitcher: + relation('generateIntrapageDotSwitcher'), + + albumsBySeriesView: + relation('generateGroupGalleryPageAlbumsBySeriesView', group), + + albumsByDateView: + relation('generateGroupGalleryPageAlbumsByDateView', group), }), data: (query, _sprawl, group) => ({ @@ -125,23 +127,76 @@ export default { })), })), - relations.albumGrid, + ([ + !html.isBlank(relations.albumsBySeriesView), + !html.isBlank(relations.albumsByDateView) + ]).filter(Boolean).length > 1 && + + language.encapsulate(pageCapsule, 'albumViewSwitcher', capsule => + html.tag('p', {class: 'gallery-view-switcher'}, + {[html.onlyIfContent]: true}, + {[html.joinChildren]: html.tag('br')}, + + [ + language.$(capsule), + + relations.albumViewSwitcher.slots({ + initialOptionIndex: 0, + + titles: [ + !html.isBlank(relations.albumsBySeriesView) && + language.$(capsule, 'bySeries'), + + !html.isBlank(relations.albumsByDateView) && + language.$(capsule, 'byDate'), + ].filter(Boolean), + + targetIDs: [ + !html.isBlank(relations.albumsBySeriesView) && + 'group-album-gallery-by-series', + + !html.isBlank(relations.albumsByDateView) && + 'group-album-gallery-by-date', + ].filter(Boolean), + }), + ])), + + /* + data.trackGridLabels.some(value => value !== null) && + html.tag('p', {class: 'gallery-set-switcher'}, + language.encapsulate(pageCapsule, 'setSwitcher', switcherCapsule => + language.$(switcherCapsule, { + sets: + relations.setSwitcher.slots({ + initialOptionIndex: 0, + + titles: + data.trackGridLabels.map(label => + label ?? + language.$(switcherCapsule, 'unlabeledSet')), + + targetIDs: + data.trackGridIDs, + }), + }))), + */ + + relations.albumsBySeriesView, + + relations.albumsByDateView.slots({ + attributes: [ + !html.isBlank(relations.albumsBySeriesView) && + {style: 'display: none'}, + ], + }), ], - leftSidebar: - (relations.sidebar - ? relations.sidebar - .slot('currentExtra', 'gallery') - .content /* TODO: Kludge. */ - : null), - navLinkStyle: 'hierarchical', navLinks: relations.navLinks .slot('currentExtra', 'gallery') .content, - secondaryNav: - relations.secondaryNav ?? null, + secondaryNav: relations.secondaryNav, })), }; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js new file mode 100644 index 00000000..54f4b8cb --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js @@ -0,0 +1,37 @@ +import {sortChronologically} from '#sort'; + +export default { + contentDependencies: ['generateGroupGalleryPageAlbumGrid'], + extraDependencies: ['html', 'language'], + + query: (group) => ({ + albums: + sortChronologically(group.albums, {latestFirst: true}), + }), + + relations: (relation, query, _group) => ({ + albumGrid: + relation('generateGroupGalleryPageAlbumGrid', query.albums), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (relations, slots, {html, language}) => + language.encapsulate('groupGalleryPage.albumsByDate', capsule => + html.tag('div', {id: 'group-album-gallery-by-date'}, + slots.attributes, + + {[html.onlyIfContent]: true}, + + html.tag('section', [ + html.tag('h2', + language.$(capsule, 'title')), + + relations.albumGrid, + ]))), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js new file mode 100644 index 00000000..0337275f --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsBySeriesView.js @@ -0,0 +1,26 @@ +export default { + contentDependencies: ['generateGroupGalleryPageSeriesSection'], + extraDependencies: ['html'], + + relations: (relation, group) => ({ + seriesSections: + group.serieses + .map(series => + relation('generateGroupGalleryPageSeriesSection', series)), + }), + + slots: { + attributes: { + type: 'attributes', + mutable: false, + }, + }, + + generate: (relations, slots, {html}) => + html.tag('div', {id: 'group-album-gallery-by-series'}, + slots.attributes, + + {[html.onlyIfContent]: true}, + + relations.seriesSections), +}; diff --git a/src/content/dependencies/generateGroupGalleryPageSeriesSection.js b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js new file mode 100644 index 00000000..3fe3fb45 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageSeriesSection.js @@ -0,0 +1,129 @@ +import {sortChronologically} from '#sort'; + +export default { + contentDependencies: [ + 'generateExpandableGallerySection', + 'generateGroupGalleryPageAlbumGrid', + ], + + extraDependencies: ['html', 'language'], + + query(series) { + const query = {}; + + // Includes undated albums. + const albumsLatestFirst = + sortChronologically(series.albums, {latestFirst: true}); + + query.albumsAboveCut = albumsLatestFirst.slice(0, 4); + query.albumsBelowCut = albumsLatestFirst.slice(4); + + query.allAlbumsDated = + series.albums.every(album => album.date); + + query.latestAlbum = + albumsLatestFirst + .filter(album => album.date) + .at(0) ?? + null; + + query.earliestAlbum = + albumsLatestFirst + .filter(album => album.date) + .at(-1) ?? + null; + + return query; + }, + + relations: (relation, query, _series) => ({ + gallerySection: + relation('generateExpandableGallerySection'), + + gridAboveCut: + relation('generateGroupGalleryPageAlbumGrid', query.albumsAboveCut), + + gridBelowCut: + relation('generateGroupGalleryPageAlbumGrid', query.albumsBelowCut), + }), + + data: (query, series) => ({ + name: + series.name, + + albums: + series.albums.length, + + tracks: + series.albums + .flatMap(album => album.tracks) + .length, + + allAlbumsDated: + query.allAlbumsDated, + + earliestAlbumDate: + (query.earliestAlbum + ? query.earliestAlbum.date + : null), + + latestAlbumDate: + (query.latestAlbum + ? query.latestAlbum.date + : null), + }), + + generate: (data, relations, {html, language}) => + language.encapsulate('groupGalleryPage.albumSection', capsule => + relations.gallerySection.slots({ + title: data.name, + + contentAboveCut: relations.gridAboveCut, + contentBelowCut: relations.gridBelowCut, + + caption: + language.encapsulate(capsule, 'caption', workingCapsule => { + const workingOptions = {}; + + workingOptions.tracks = + html.tag('b', + language.countTracks(data.tracks, {unit: true})); + + workingOptions.albums = + html.tag('b', + language.countAlbums(data.albums, {unit: true})); + + if (data.allAlbumsDated) { + const earliestDate = data.earliestAlbumDate; + const latestDate = data.latestAlbumDate; + + const earliestYear = earliestDate.getFullYear(); + const latestYear = latestDate.getFullYear(); + + if (earliestYear === latestYear) { + if (data.albums === 1) { + workingCapsule += '.withDate'; + workingOptions.date = + language.formatDate(earliestDate); + } else { + workingCapsule += '.withYear'; + workingOptions.year = + language.formatYear(earliestDate); + } + } else { + workingCapsule += '.withYearRange'; + workingOptions.yearRange = + language.formatYearRange(earliestDate, latestDate); + } + } + + return language.$(workingCapsule, workingOptions); + }), + + expandCue: + language.$(capsule, 'expand'), + + collapseCue: + language.$(capsule, 'collapse'), + })), +}; diff --git a/src/data/things/language.js b/src/data/things/language.js index a3f861bd..4e23cf7f 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -135,6 +135,7 @@ export class Language extends Thing { }, intl_date: this.#intlHelper(Intl.DateTimeFormat, {full: true}), + intl_dateYear: this.#intlHelper(Intl.DateTimeFormat, {year: 'numeric'}), intl_number: this.#intlHelper(Intl.NumberFormat), intl_listConjunction: this.#intlHelper(Intl.ListFormat, {type: 'conjunction'}), intl_listDisjunction: this.#intlHelper(Intl.ListFormat, {type: 'disjunction'}), @@ -488,22 +489,44 @@ export class Language extends Thing { // or both are undefined, that's just blank content. const hasStart = startDate !== null && startDate !== undefined; const hasEnd = endDate !== null && endDate !== undefined; - if (!hasStart || !hasEnd) { - if (startDate === endDate) { - return html.blank(); - } else if (hasStart) { - throw new Error(`Expected both start and end of date range, got only start`); - } else if (hasEnd) { - throw new Error(`Expected both start and end of date range, got only end`); - } else { - throw new Error(`Got mismatched ${startDate}/${endDate} for start and end`); - } + if (!hasStart && !hasEnd) { + return html.blank(); + } else if (hasStart && !hasEnd) { + throw new Error(`Expected both start and end of date range, got only start`); + } else if (!hasStart && hasEnd) { + throw new Error(`Expected both start and end of date range, got only end`); } this.assertIntlAvailable('intl_date'); return this.intl_date.formatRange(startDate, endDate); } + formatYear(date) { + if (date === null || date === undefined) { + return html.blank(); + } + + this.assertIntlAvailable('intl_dateYear'); + return this.intl_dateYear.format(date); + } + + formatYearRange(startDate, endDate) { + // formatYearRange expects both values to be present, but if both are null + // or both are undefined, that's just blank content. + const hasStart = startDate !== null && startDate !== undefined; + const hasEnd = endDate !== null && endDate !== undefined; + if (!hasStart && !hasEnd) { + return html.blank(); + } else if (hasStart && !hasEnd) { + throw new Error(`Expected both start and end of date range, got only start`); + } else if (!hasStart && hasEnd) { + throw new Error(`Expected both start and end of date range, got only end`); + } + + this.assertIntlAvailable('intl_dateYear'); + return this.intl_dateYear.formatRange(startDate, endDate); + } + formatDateDuration({ years: numYears = 0, months: numMonths = 0, diff --git a/src/static/css/site.css b/src/static/css/site.css index 655d095a..6fa4da38 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -1913,6 +1913,48 @@ ul.quick-info li:not(:last-child)::after { text-align: center; } +.gallery-view-switcher { + text-align: center; + line-height: 1.4; +} + +#content.top-index section { + margin-bottom: 1.5em; +} + +.expandable-gallery-section .section-expando { + margin-top: 1em; + margin-bottom: 2em; + + display: flex; + flex-direction: row; + justify-content: space-around; +} + +.expandable-gallery-section .section-expando-content { + text-align: center; + line-height: 1.5; +} + +.expandable-gallery-section .section-expando-toggle { + text-decoration: underline; + text-decoration-style: dotted; +} + +.expandable-gallery-section.expanded .section-content-below-cut { + animation: expand-gallery-section 0.8s forwards; +} + +@keyframes expand-gallery-section { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + .quick-description:not(.has-external-links-only) { --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06); margin-left: auto; diff --git a/src/static/js/client/expandable-gallery-section.js b/src/static/js/client/expandable-gallery-section.js new file mode 100644 index 00000000..dc83e8b7 --- /dev/null +++ b/src/static/js/client/expandable-gallery-section.js @@ -0,0 +1,77 @@ +/* eslint-env browser */ + +// TODO: Combine this and quick-description.js + +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'expandableGallerySectionInfo', + + sections: null, + + sectionContentBelowCut: null, + + sectionExpandoToggles: null, + + sectionExpandCues: null, + sectionCollapseCues: null, +}; + +export function getPageReferences() { + info.sections = + Array.from(document.querySelectorAll('.expandable-gallery-section')) + .filter(section => section.querySelector('.section-expando-toggle')); + + info.sectionContentBelowCut = + info.sections + .map(section => section.querySelector('.section-content-below-cut')); + + info.sectionExpandoToggles = + info.sections + .map(section => section.querySelector('.section-expando-toggle')); + + info.sectionExpandCues = + info.sections + .map(section => section.querySelector('.section-expand-cue')); + + info.sectionCollapseCues = + info.sections + .map(section => section.querySelector('.section-collapse-cue')); +} + +export function addPageListeners() { + for (const { + section, + contentBelowCut, + expandoToggle, + expandCue, + collapseCue, + } of stitchArrays({ + section: info.sections, + contentBelowCut: info.sectionContentBelowCut, + expandoToggle: info.sectionExpandoToggles, + expandCue: info.sectionExpandCues, + collapseCue: info.sectionCollapseCues, + })) { + expandoToggle.addEventListener('click', domEvent => { + domEvent.preventDefault(); + + const collapsed = + cssProp(contentBelowCut, 'display') === 'none'; + + if (collapsed) { + section.classList.add('expanded'); + cssProp(contentBelowCut, 'display', null); + cssProp(expandCue, 'display', 'none'); + cssProp(collapseCue, 'display', null); + } else { + section.classList.remove('expanded'); + cssProp(contentBelowCut, 'display', 'none'); + cssProp(expandCue, 'display', null); + cssProp(collapseCue, 'display', 'none'); + } + }); + } +} diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 81ea3415..aeb9264a 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -10,6 +10,7 @@ import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js'; import * as datetimestampTooltipModule from './datetimestamp-tooltip.js'; import * as draggedLinkModule from './dragged-link.js'; +import * as expandableGallerySectionModule from './expandable-gallery-section.js'; import * as hashLinkModule from './hash-link.js'; import * as hoverableTooltipModule from './hoverable-tooltip.js'; import * as imageOverlayModule from './image-overlay.js'; @@ -32,6 +33,7 @@ export const modules = [ cssCompatibilityAssistantModule, datetimestampTooltipModule, draggedLinkModule, + expandableGallerySectionModule, hashLinkModule, hoverableTooltipModule, imageOverlayModule, diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js index cff82252..6a7a6023 100644 --- a/src/static/js/client/quick-description.js +++ b/src/static/js/client/quick-description.js @@ -1,5 +1,7 @@ /* eslint-env browser */ +// TODO: Combine this and expandable-gallery-section.js + import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { diff --git a/src/strings-default.yaml b/src/strings-default.yaml index 16687905..0f9bd54a 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -982,6 +982,8 @@ misc: otherCoverArtists: "With {ARTISTS}" + expandCollapseCue: "({CUE})" + albumGalleryGrid: noCoverArt: "{NAME}" @@ -1665,6 +1667,34 @@ groupGalleryPage: infoLine: >- {TRACKS} across {ALBUMS}, totaling {TIME}. + albumViewSwitcher: + _: "Showing albums:" + + bySeries: "By series" + byDate: "By date" + + albumsByDate: + title: "All albums" + + albumSection: + caption: >- + {TRACKS} across {ALBUMS}. + + caption.withDate: >- + {TRACKS} across {ALBUMS}, released {DATE}. + + caption.withYear: >- + {TRACKS} across {ALBUMS}, released during {YEAR}. + + caption.withYearRange: >- + {TRACKS} across {ALBUMS}, released {YEAR_RANGE}. + + expand: >- + Show the rest! + + collapse: >- + Collapse these + # # listingIndex: # The listing index page shows all available listings on the wiki, |