From af4ca039b42da9968e82087560eb398f3b3bbd17 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Thu, 17 Jul 2025 15:12:07 -0300 Subject: content, data, client, css: style selector first pass --- src/content/dependencies/generateCoverGrid.js | 10 ++ .../dependencies/generateGroupGalleryPage.js | 5 +- .../generateGroupGalleryPageAlbumGrid.js | 6 + .../generateGroupGalleryPageAlbumsByDateView.js | 22 +++- .../generateGroupGalleryPageStyleSelector.js | 62 +++++++++++ src/data/things/group.js | 6 + src/static/css/site.css | 41 ++++++- src/static/js/client/gallery-style-selector.js | 123 +++++++++++++++++++++ src/static/js/client/index.js | 2 + src/strings-default.yaml | 11 ++ 10 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 src/content/dependencies/generateGroupGalleryPageStyleSelector.js create mode 100644 src/static/js/client/gallery-style-selector.js diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index e1f13af3..e7113091 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -32,6 +32,12 @@ export default { v.isString))), }, + itemAttributes: { + validate: v => + v.strictArrayOf( + v.optional(v.isAttributes)), + }, + lazy: {validate: v => v.anyOf(v.isWholeNumber, v.isBoolean)}, actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, @@ -44,6 +50,7 @@ export default { [ stitchArrays({ classes: slots.classes, + attributes: slots.itemAttributes, image: slots.images, link: slots.links, name: slots.names, @@ -54,6 +61,7 @@ export default { Array.from(slots.links).fill(null) }).map(({ classes, + attributes, image, link, name, @@ -66,6 +74,8 @@ export default { {class: ['grid-item', 'box']}, + attributes, + (classes ? {class: classes} : null), diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js index dfdad0e8..8e11f9e5 100644 --- a/src/content/dependencies/generateGroupGalleryPage.js +++ b/src/content/dependencies/generateGroupGalleryPage.js @@ -183,7 +183,10 @@ export default { }))), */ - relations.albumsByDateView, + relations.albumsByDateView.slots({ + showTitle: + !html.isBlank(relations.albumsBySeriesView), + }), relations.albumsBySeriesView.slots({ attributes: [ diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js index 96cadb03..4f8aaf3b 100644 --- a/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js +++ b/src/content/dependencies/generateGroupGalleryPageAlbumGrid.js @@ -33,6 +33,9 @@ export default { tracks: albums.map(album => album.tracks.length), + styles: + albums.map(album => album.style), + notFromThisGroup: albums.map(album => !album.groups.includes(group)), }), @@ -56,6 +59,9 @@ export default { }), })), + itemAttributes: + data.styles.map(style => ({'data-style': style})), + info: stitchArrays({ tracks: data.tracks, diff --git a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js index b7d01eb5..58375f3e 100644 --- a/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js +++ b/src/content/dependencies/generateGroupGalleryPageAlbumsByDateView.js @@ -1,7 +1,11 @@ import {sortChronologically} from '#sort'; export default { - contentDependencies: ['generateGroupGalleryPageAlbumGrid'], + contentDependencies: [ + 'generateGroupGalleryPageAlbumGrid', + 'generateGroupGalleryPageStyleSelector', + ], + extraDependencies: ['html', 'language'], query: (group) => ({ @@ -10,6 +14,11 @@ export default { }), relations: (relation, query, group) => ({ + styleSelector: + (group.divideAlbumsByStyle + ? relation('generateGroupGalleryPageStyleSelector', group) + : null), + albumGrid: relation('generateGroupGalleryPageAlbumGrid', query.albums, @@ -17,6 +26,10 @@ export default { }), slots: { + showTitle: { + type: 'boolean', + }, + attributes: { type: 'attributes', mutable: false, @@ -31,8 +44,11 @@ export default { {[html.onlyIfContent]: true}, html.tag('section', [ - html.tag('h2', - language.$(capsule, 'title')), + slots.showTitle && + html.tag('h2', + language.$(capsule, 'title')), + + relations.styleSelector, relations.albumGrid, ]))), diff --git a/src/content/dependencies/generateGroupGalleryPageStyleSelector.js b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js new file mode 100644 index 00000000..4f9d02a9 --- /dev/null +++ b/src/content/dependencies/generateGroupGalleryPageStyleSelector.js @@ -0,0 +1,62 @@ +import {unique} from '#sugar'; + +export default { + extraDependencies: ['html', 'language'], + + query: (group) => ({ + styles: + unique(group.albums.map(album => album.style)), + }), + + data: (query, group) => ({ + albums: + group.albums.length, + + styles: + query.styles, + }), + + generate: (data, {html, language}) => + language.encapsulate('groupGalleryPage', pageCapsule => + (data.styles.length <= 1 + ? html.blank() + : html.tag('p', {class: 'gallery-style-selector'}, + {class: ['drop', 'shiny']}, + + language.encapsulate(pageCapsule, 'albumStyleSwitcher', capsule => [ + html.tag('span', + language.$(capsule)), + + html.tag('br'), + + html.tag('span', {class: 'styles'}, + data.styles.map(style => + html.tag('label', {'data-style': style}, [ + html.tag('input', {type: 'checkbox'}, + {checked: true}), + + html.tag('span', + language.$(capsule, style)), + ]))), + + html.tag('br'), + + html.tag('span', {class: ['count', 'all']}, + language.$(capsule, 'count.all', { + total: data.albums, + })), + + html.tag('span', {class: ['count', 'filtered']}, + {style: 'display: none'}, + + language.$(capsule, 'count.filtered', { + count: html.tag('span'), + total: data.albums, + })), + + html.tag('span', {class: ['count', 'none']}, + {style: 'display: none'}, + + language.$(capsule, 'count.none')), + ])))), +}; diff --git a/src/data/things/group.js b/src/data/things/group.js index fc33a9d0..bc2e915c 100644 --- a/src/data/things/group.js +++ b/src/data/things/group.js @@ -15,6 +15,7 @@ import { color, contentString, directory, + flag, name, referenceList, soupyFind, @@ -32,6 +33,8 @@ export class Group extends Thing { name: name('Unnamed Group'), directory: directory(), + divideAlbumsByStyle: flag(false), + description: contentString(), urls: urls(), @@ -141,6 +144,9 @@ export class Group extends Thing { fields: { 'Group': {property: 'name'}, 'Directory': {property: 'directory'}, + + 'Divide Albums By Style': {property: 'divideAlbumsByStyle'}, + 'Description': {property: 'description'}, 'URLs': {property: 'urls'}, diff --git a/src/static/css/site.css b/src/static/css/site.css index e647dd83..2c5d6ce1 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -1130,6 +1130,15 @@ a .normal-content { text-decoration: none !important; } +label:hover span { + text-decoration: underline; + text-decoration-style: solid; +} + +label > input[type=checkbox]:not(:checked) + span { + opacity: 0.8; +} + #secondary-nav { text-align: center; @@ -2021,13 +2030,32 @@ ul.quick-info li:not(:last-child)::after { text-align: center; } -.gallery-view-switcher { +.gallery-view-switcher, +.gallery-style-selector { margin-left: auto; margin-right: auto; text-align: center; line-height: 1.4; } +.gallery-style-selector .styles { + display: inline-flex; + justify-content: center; +} + +.gallery-style-selector .styles label:not(:last-child) { + margin-right: 1.25ch; +} + +.gallery-style-selector .count { + font-size: 0.85em; + + position: relative; + bottom: -0.25em; + + opacity: 0.9; +} + #content.top-index section { margin-bottom: 1.5em; } @@ -3028,6 +3056,13 @@ video.pixelate, .pixelate video { box-sizing: border-box; } +.grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) { + padding-bottom: 140px; + background: #cccccc07; + border-radius: 10px; + border: 1px dashed #fff3; +} + .grid-item { font-size: 0.9em; } @@ -3042,6 +3077,10 @@ video.pixelate, .pixelate video { margin: 10px; } +.grid-item[class*="hidden-by-"] { + display: none; +} + .grid-item .image-container { width: 100%; } diff --git a/src/static/js/client/gallery-style-selector.js b/src/static/js/client/gallery-style-selector.js new file mode 100644 index 00000000..c7086eae --- /dev/null +++ b/src/static/js/client/gallery-style-selector.js @@ -0,0 +1,123 @@ +/* eslint-env browser */ + +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'galleryStyleSelectorInfo', + + selectors: null, + sections: null, + + selectorStyleInputs: null, + selectorStyleInputStyles: null, + + selectorReleaseItems: null, + selectorReleaseItemStyles: null, + + selectorCountAll: null, + selectorCountFiltered: null, + selectorCountFilteredCount: null, + selectorCountNone: null, +}; + +export function getPageReferences() { + info.selectors = + Array.from(document.querySelectorAll('.gallery-style-selector')); + + info.sections = + info.selectors + .map(selector => selector.closest('section')); + + info.selectorStyleInputs = + info.selectors + .map(selector => selector.querySelectorAll('.styles input')) + .map(inputs => Array.from(inputs)); + + info.selectorStyleInputStyles = + info.selectorStyleInputs + .map(inputs => inputs + .map(input => input.closest('label').dataset.style)); + + info.selectorReleaseItems = + info.sections + .map(section => section.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.selectorReleaseItemStyles = + info.selectorReleaseItems + .map(items => items + .map(item => item.dataset.style)); + + info.selectorCountAll = + info.selectors + .map(selector => selector.querySelector('.count.all')); + + info.selectorCountFiltered = + info.selectors + .map(selector => selector.querySelector('.count.filtered')); + + info.selectorCountFilteredCount = + info.selectorCountFiltered + .map(selector => selector.querySelector('span')); + + info.selectorCountNone = + info.selectors + .map(selector => selector.querySelector('.count.none')); +} + +export function addPageListeners() { + for (const index of info.selectors.keys()) { + for (const input of info.selectorStyleInputs[index]) { + input.addEventListener('input', () => updateVisibleReleases(index)); + } + } +} + +function updateVisibleReleases(index) { + const inputs = info.selectorStyleInputs[index]; + const inputStyles = info.selectorStyleInputStyles[index]; + + const selectedStyles = + stitchArrays({input: inputs, style: inputStyles}) + .filter(({input}) => input.checked) + .map(({style}) => style); + + const releases = info.selectorReleaseItems[index]; + const releaseStyles = info.selectorReleaseItemStyles[index]; + + let visible = 0; + + stitchArrays({ + release: releases, + style: releaseStyles, + }).forEach(({release, style}) => { + if (selectedStyles.includes(style)) { + release.classList.remove('hidden-by-style-mismatch'); + visible++; + } else { + release.classList.add('hidden-by-style-mismatch'); + } + }); + + const countAll = info.selectorCountAll[index]; + const countFiltered = info.selectorCountFiltered[index]; + const countFilteredCount = info.selectorCountFilteredCount[index]; + const countNone = info.selectorCountNone[index]; + + if (visible === releases.length) { + cssProp(countAll, 'display', null); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', 'none'); + } else if (visible === 0) { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', null); + } else { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', null); + cssProp(countNone, 'display', 'none'); + countFilteredCount.innerHTML = visible; + } +} diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 9d7eae86..4ca4700e 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -12,6 +12,7 @@ import * as cssCompatibilityAssistantModule from './css-compatibility-assistant. import * as datetimestampTooltipModule from './datetimestamp-tooltip.js'; import * as draggedLinkModule from './dragged-link.js'; import * as expandableGallerySectionModule from './expandable-gallery-section.js'; +import * as galleryStyleSelectorModule from './gallery-style-selector.js'; import * as hashLinkModule from './hash-link.js'; import * as hoverableTooltipModule from './hoverable-tooltip.js'; import * as imageOverlayModule from './image-overlay.js'; @@ -36,6 +37,7 @@ export const modules = [ datetimestampTooltipModule, draggedLinkModule, expandableGallerySectionModule, + galleryStyleSelectorModule, hashLinkModule, hoverableTooltipModule, imageOverlayModule, diff --git a/src/strings-default.yaml b/src/strings-default.yaml index eed7b9b7..026b88c9 100644 --- a/src/strings-default.yaml +++ b/src/strings-default.yaml @@ -1756,6 +1756,17 @@ groupGalleryPage: bySeries: "By series" byDate: "By date" + albumStyleSwitcher: + _: "Showing these releases:" + + album: "Albums" + single: "Singles" + + count: + all: "all {TOTAL}" + filtered: "{COUNT} of {TOTAL}" + none: "none at all" + albumsByDate: title: "All albums" -- cgit 1.3.0-6-gf8a5