import { AvailableThermalPalettes } from "@labir/core"; import { provide } from "@lit/context"; import { t } from "i18next"; import { css, CSSResultGroup, html, nothing, PropertyValues } from "lit"; import { customElement, property, queryAssignedElements, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { createRef, Ref, ref } from "lit/directives/ref.js"; import { BaseElement } from "../../hierarchy/BaseElement"; import { RegistryProviderElement } from "../../hierarchy/providers/RegistryProvider"; import { T } from "../../translations/Languages"; import { initLocalesInTopLevelElement, localeContext, localeConverter, Locales } from "../../translations/localeContext"; import { BaseAppWithPngExportContext, pngExportFsContext, pngExportFsSetterContext, pngExportWidthContext, pngExportWidthSetterContext } from "../../utils/converters/pngExportContext"; import { ThermalFileElement } from "../../utils/multipleFiles/ThermalFile"; import { ThermalGroup } from "../../utils/multipleFiles/ThermalGroup"; type ParsedFile = { lrc: string; png?: string; label?: string; }; type ParsedGroup = { label?: string, description?: string, lat?: string, lon?: string, files: ParsedFile[] }; type ParsedStructure = ParsedGroup[]; enum STATE { MAIN = "main", GROUP = "group", FILE = "file" } @customElement("thermal-gallery-app") export class GalleryApp extends BaseAppWithPngExportContext { // Presentational attributes @property({ type: String, reflect: true }) public author?: string; @property({ type: String, reflect: true }) public label: string = "Gallery of IR images"; @property({ type: String, reflect: true }) public license?: string; @property({ type: String, reflect: true, attribute: true }) public palette: AvailableThermalPalettes = "jet"; @state() @queryAssignedElements({ flatten: true }) protected slottedElements!: Array; @state() protected structure?: ParsedStructure; @state() protected state: STATE = STATE.MAIN; @state() protected group?: ParsedGroup; @state() protected file?: ParsedFile; protected registryRef: Ref = createRef(); @provide({ context: pngExportWidthContext }) protected pngExportWidth: number = 1200; @provide({ context: pngExportWidthSetterContext }) protected pngExportWidthSetterContext = (value: number) => { this.pngExportWidth = value; } @provide({ context: pngExportFsContext }) protected pngExportFs: number = 20; @provide({ context: pngExportFsSetterContext }) protected pngExportFsSetterContext = (value: number) => { this.pngExportFs = value; } @provide({ context: localeContext }) @property({ reflect: true, converter: localeConverter }) public locale!: Locales; @state() columns: number = 3; connectedCallback(): void { super.connectedCallback(); initLocalesInTopLevelElement(this); this.addEventListener("slotchange", () => { this.processSlots(); }); } protected firstUpdated(_changedProperties: PropertyValues): void { super.firstUpdated(_changedProperties); this.processSlots(); this.resetRegistry(); if (this.registryRef.value) { this.registryRef.value?.registry.palette.setPalette(this.palette); this.registryRef.value.registry.batch.onBatchComplete.set(this.UUID, () => { if (this.registryRef.value) { const registry = this.registryRef.value.registry; registry.range.applyMinmax(); } }); this.registryRef.value.registry.groups.addListener(this.UUID, () => { if (this.registryRef.value) { const registry = this.registryRef.value.registry; registry.range.applyMinmax(); } }); } } protected processSlots(): void { setTimeout(() => { this.structure = this.slottedElements .filter(element => element instanceof ThermalGroup) .map(element => { return { label: element.getAttribute("label"), description: element.getAttribute("description"), lat: element.getAttribute("lat"), lon: element.getAttribute("lon"), files: Array.from(element.children) .filter(child => { return child instanceof ThermalFileElement && child.hasAttribute("lrc") }) .map(child => { return { lrc: child.getAttribute("lrc"), png: child.getAttribute("png"), label: child.getAttribute("label") } as ParsedFile }) } as ParsedGroup; }) .filter(element => element.files.length > 0); }, 1000); } protected actionMainOpen() { this.state = STATE.MAIN; this.resetRegistry(); setTimeout(() => { this.group = undefined; this.file = undefined; }, 0); } protected actionGroupOpen(group: ParsedGroup) { this.resetRegistry(); setTimeout(() => { this.group = group; this.columns = Math.min(4, group.files.length); if (group.files.length > 1) { this.state = STATE.GROUP; } else { this.state = STATE.FILE; this.file = group.files[0]; } }, 0); } protected actionDetailOpen(file: ParsedFile) { if (this.group === undefined) { throw new Error("Group not yet set"); } this.state = STATE.FILE; this.resetRegistry(); setTimeout(() => { this.file = file; }, 0); } protected actionDetailClose() { this.state = STATE.GROUP; this.resetRegistry(); setTimeout(() => { this.file = undefined; }, 0); } protected resetRegistry() { if (this.registryRef.value) { this.registryRef.value.registry.forEveryInstance(instance => instance.unmountFromDom()); // this.registryRef.value.registry.reset(); this.registryRef.value.registry.batch.onBatchComplete.set(this.UUID, () => { this.registryRef.value?.registry.range.applyMinmax(); }); } } protected renderMain() { if (this.structure === undefined) { return html``; } const preview = this.structure.map(group => { const { files, ...rest } = group; return { ...rest, file: files[0], group } }); const groups = preview.map((group, index) => { const slug = group.label ?? `group_preview_${index}`; return html` `; }); return html`
${groups}
`; } protected renderGroup() { if (this.structure === undefined || this.group === undefined) { return html`
x ${this.group.label} ${this.structure.filter(group => group.label !== this.group?.label).map(group => { return html`
${group.label}
`; })}
{ const target = event.target as null | { value: string } const value = target?.value; if (value !== undefined) { this.columns = parseInt(value); } }}>
${t(T.columns, { num: this.columns })}
${this.group.description ? html`

${this.group.description}

` : nothing }
${this.group.files.map(file => html` `)}
` ); } protected renderFile() { if (this.structure === undefined || this.group === undefined || this.file === undefined) { return html` `); } protected renderBrowser(content: ReturnType) { return html`
${content}
`; } public static styles?: CSSResultGroup | undefined = css` .group-thumbnail { margin: 0; padding: 0; cursor: pointer; overflow: hidden; width: 100%; border-radius: var(--thermal-radius); border: 1px solid var(--thermal-slate); background: var(--thermal-slate-light); color: var(--thermal-foreground); transition: all .4s ease-in-out; box-shadow: 0 0 5px var(--thermal-slate); div.header { display: grid; grid-template-columns: auto 1.5em; padding: var(--thermal-gap); gap: var(--thermal-gap); text-align: left; .title { font-weight: bold; font-size: 1.2em; margin-bottom: .5em; } .count { font-size: .9em; opacity: .8; } .button { opacity: .8; } } file-provider { overflow: hidden; display: block; } file-canvas { overflow: hidden; display: block; transition: all .4s ease-in-out; } &:hover, &:focus { background: var(--thermal-background); box-shadow: 0 0 15px var(--thermal-slate-dark); file-canvas { transform: scale(1.1); } } } .browser { display: grid; grid-template-columns: 3em 1fr; &.state_file { file-provider { display: block; background: var(--thermal-background); padding: var(--thermal-gap); border-radius: var(--thermal-radius); border: 1px solid var(--thermal-slate); } } &.state_group { group-provider { display: block; width: 100%; box-sizing: border-box; padding: var(--thermal-gap); border: 1px solid var(--thermal-slate); border-radius: var(--thermal-radius); header { display: flex; gap: 5px; margin-bottom: var(--thermal-gap); } .group-description { border: 1px solid var(--thermal-slate); border-radius: var(--thermal-radius); padding: var(--thermal-gap); box-sizing: border-box; width: 100%; display: flex; align-items: center; gap: var(--thermal-gap); margin-bottom: var(--thermal-gap); svg { opacity: .8; width: 1em; } p { margin: 0; padding: 0; font-size: calc(var(--thermal-gap) * .8); } } .files { display: grid; margin: calc( var(--thermal-gap) * .5 * -1); &.columns_1 { grid-template-columns: 100%; } &.columns_2 { grid-template-columns: repeat( 2, 50% ); } &.columns_3 { grid-template-columns: repeat( 3, calc(100% / 3) ); } &.columns_4 { grid-template-columns: repeat( 4, calc(100% / 4) ); } &.columns_5 { grid-template-columns: repeat( 5, calc(100% / 5) ); } &.columns_6 { grid-template-columns: repeat( 6, calc(100% / 6) ); } &.columns_7 { grid-template-columns: repeat( 7, calc(100% / 7) ); } &.columns_8 { grid-template-columns: repeat( 8, calc(100% / 8) ); } &.columns_9 { grid-template-columns: repeat( 9, calc(100% / 9) ); } &.columns_10 { grid-template-columns: repeat( 10, calc(100% / 10) ); } file-thumbnail { padding: calc( var(--thermal-gap) * .5); } } } } } .main { display: grid; grid-template-columns: repeat( auto-fill, minmax(300px, 1fr) ); gap: var(--thermal-gap); group-provider { width: 100%; } } `; protected render(): unknown { return html` ${this.structure !== undefined ? html` ` : nothing }
${this.state === STATE.MAIN ? this.renderMain() : nothing} ${this.state === STATE.GROUP ? this.renderGroup() : nothing} ${this.state === STATE.FILE ? this.renderFile() : nothing}
`; } }