diff --git a/frontend/src/components/configure/ArchitectureUpdate.ts b/frontend/src/components/configure/ArchitectureUpdate.ts new file mode 100644 index 0000000..43b84d9 --- /dev/null +++ b/frontend/src/components/configure/ArchitectureUpdate.ts @@ -0,0 +1,14 @@ +import type {Architecture} from '@eighty4/install-template' + +export const ARCHITECTURE_UPDATE_EVENT_TYPE = 'architecture' + +export type ArchitectureUpdateEvent = CustomEvent + +export interface ArchitectureUpdate { + arch: Architecture + filename: string +} + +export function createArchitectureUpdate(arch: Architecture, filename: string): CustomEvent { + return new CustomEvent(ARCHITECTURE_UPDATE_EVENT_TYPE, {detail: {arch, filename}}) +} diff --git a/frontend/src/components/configure/ConfigureBinaries.css b/frontend/src/components/configure/ConfigureBinaries.css index 96bccee..8bb3e36 100644 --- a/frontend/src/components/configure/ConfigureBinaries.css +++ b/frontend/src/components/configure/ConfigureBinaries.css @@ -1,5 +1,5 @@ .container { - margin: 2.5rem; + margin: 2.5rem 0; } .os { diff --git a/frontend/src/components/configure/ConfigureBinaries.ts b/frontend/src/components/configure/ConfigureBinaries.ts index c688f79..7d0f4c2 100644 --- a/frontend/src/components/configure/ConfigureBinaries.ts +++ b/frontend/src/components/configure/ConfigureBinaries.ts @@ -1,7 +1,12 @@ import type {Binary} from '@eighty4/install-github' import {type OperatingSystem, operatingSystemLabel} from '@eighty4/install-template' -import ConfigureBinary from './ConfigureBinary.ts' +import { + ARCHITECTURE_UPDATE_EVENT_TYPE, + type ArchitectureUpdateEvent, + createArchitectureUpdate, +} from './ArchitectureUpdate.ts' import css from './ConfigureBinaries.css?inline' +import ConfigureBinary from './ConfigureBinary.ts' import SystemLogo from '../SystemLogo.ts' import {cloneTemplate, removeChildNodes} from '../../dom.ts' @@ -26,6 +31,8 @@ export default class ConfigureBinaries extends HTMLElement { readonly #bins: Array + readonly #container: HTMLElement + readonly #shadow: ShadowRoot constructor(bins: Array, os: OperatingSystem) { @@ -35,14 +42,25 @@ export default class ConfigureBinaries extends HTMLElement { this.#shadow.appendChild(cloneTemplate(ConfigureBinaries.TEMPLATE_ID)) this.#shadow.querySelector('.os .logo')!.appendChild(new SystemLogo({os, color: '#111', size: '1.5rem'})) this.#shadow.querySelector('.os')!.appendChild(document.createTextNode(`${operatingSystemLabel(os)} binaries`)) - this.update() + this.#container = this.#shadow.querySelector('.bins')! } - update() { - const container = this.#shadow.querySelector('.bins')! - removeChildNodes(container) + connectedCallback() { for (const bin of this.#bins) { - container.appendChild(new ConfigureBinary(bin)) + this.#container.appendChild(new ConfigureBinary(bin)) + .addEventListener(ARCHITECTURE_UPDATE_EVENT_TYPE, this.#onArchUpdate as EventListener) + } + } + + disconnectedCallback() { + for (const configureBinary of this.querySelectorAll('configure-binary')) { + configureBinary.removeEventListener(ARCHITECTURE_UPDATE_EVENT_TYPE, this.#onArchUpdate as EventListener) } + removeChildNodes(this.#container) + } + + #onArchUpdate = (e: ArchitectureUpdateEvent) => { + const {arch, filename} = e.detail + this.dispatchEvent(createArchitectureUpdate(arch, filename)) } } diff --git a/frontend/src/components/configure/ConfigureBinary.ts b/frontend/src/components/configure/ConfigureBinary.ts index 27c2c39..19ac71f 100644 --- a/frontend/src/components/configure/ConfigureBinary.ts +++ b/frontend/src/components/configure/ConfigureBinary.ts @@ -1,4 +1,6 @@ import type {Binary} from '@eighty4/install-github' +import {type Architecture, ARCHITECTURES} from '@eighty4/install-template' +import {createArchitectureUpdate} from './ArchitectureUpdate.ts' import css from './ConfigureBinary.css?inline' import {cloneTemplate} from '../../dom.ts' @@ -27,11 +29,32 @@ export default class ConfigureBinary extends HTMLElement { this.#bin = bin this.#shadow = this.attachShadow({mode: 'open'}) this.#shadow.appendChild(cloneTemplate(ConfigureBinary.TEMPLATE_ID)) - this.update() } - update() { + connectedCallback() { this.#shadow.querySelector('.filename')!.textContent = this.#bin.filename - this.#shadow.querySelector('.arch')!.textContent = this.#bin.arch || '?' + const archContainer = this.#shadow.querySelector('.arch')! + if (this.#bin.arch) { + archContainer.textContent = this.#bin.arch + } else { + archContainer.replaceWith(this.#createArchSelect()) + } + } + + #createArchSelect(): HTMLSelectElement { + const archSelect = document.createElement('select') + archSelect.innerHTML = '' + ARCHITECTURES.map(arch => ``).join() + archSelect.addEventListener('input', this.#onArchUpdate) + return archSelect + } + + disconnectedCallback() { + this.#shadow.querySelector('select')?.removeEventListener('change', this.#onArchUpdate) + } + + #onArchUpdate = () => { + this.#shadow.querySelector('option[value=""]')?.remove() + const arch = this.#shadow.querySelector('select')!.value as Architecture + this.dispatchEvent(createArchitectureUpdate(arch, this.#bin.filename)) } } diff --git a/frontend/src/components/configure/ConfigureScript.css b/frontend/src/components/configure/ConfigureScript.css index 53120e5..63c2a31 100644 --- a/frontend/src/components/configure/ConfigureScript.css +++ b/frontend/src/components/configure/ConfigureScript.css @@ -1,7 +1,3 @@ -.header { - width: 45vw; -} - .row { display: flex; gap: .75rem; @@ -11,7 +7,7 @@ .name { flex: 1; - font-size: 1.2rem; + font-size: 1.5rem; font-weight: 600; } @@ -25,7 +21,11 @@ } .buttons { - margin-left: 2.75rem; + display: flex; + justify-content: center; + align-items: center; + height: 5rem; + gap: 2rem; } .download { @@ -47,10 +47,6 @@ color: #687; } -.download + .download { - margin-left: 1.5rem; -} - .download svg { margin-right: .25rem; } diff --git a/frontend/src/components/configure/ConfigureScript.ts b/frontend/src/components/configure/ConfigureScript.ts index aed56dd..bd2151e 100644 --- a/frontend/src/components/configure/ConfigureScript.ts +++ b/frontend/src/components/configure/ConfigureScript.ts @@ -1,10 +1,19 @@ import type {Binary, Repository} from '@eighty4/install-github' -import type {Distribution, GenerateScriptOptions, OperatingSystem} from '@eighty4/install-template' +import { + type Architecture, + type Distribution, + type GenerateScriptOptions, + type OperatingSystem, +} from '@eighty4/install-template' +import {ARCHITECTURE_UPDATE_EVENT_TYPE, type ArchitectureUpdateEvent} from './ArchitectureUpdate.ts' import ConfigureBinaries from './ConfigureBinaries.ts' import css from './ConfigureScript.css?inline' -import {cloneTemplate} from '../../dom.ts' +import {cloneTemplate, removeChildNodes} from '../../dom.ts' import {downloadScript} from '../../download.ts' +// todo links to gh commit, repo and release pages +// todo release date +// todo repo languages export default class ConfigureScript extends HTMLElement { private static readonly TEMPLATE_ID = 'tmpl-configure-script' @@ -15,7 +24,6 @@ export default class ConfigureScript extends HTMLElement {
-
@@ -32,6 +40,10 @@ export default class ConfigureScript extends HTMLElement { ` } + readonly #architectureResolved: Record = {} + + readonly #architectureUnresolved: Record = {} + readonly #binaries: Record> = { Linux: [], MacOS: [], @@ -49,12 +61,14 @@ export default class ConfigureScript extends HTMLElement { if (repo.latestRelease?.binaries) { for (const binary of repo.latestRelease.binaries!) { this.#binaries[binary.os].push(binary) + if (!binary.arch) { + this.#architectureUnresolved[binary.filename] = binary.os + } } } this.#repo = repo this.#shadow = this.attachShadow({mode: 'open'}) this.#shadow.appendChild(cloneTemplate(ConfigureScript.TEMPLATE_ID)) - this.#shadow.querySelector('profile-picture')!.setAttribute('owner', repo.owner) if (this.#binaries['Linux'].length || this.#binaries['MacOS'].length) { this.#downloadButtons['Linux'] = this.#downloadButtons['MacOS'] = this.#shadow.querySelector('.buttons')! .appendChild(this.#createDownloadButton('Linux')) @@ -63,35 +77,44 @@ export default class ConfigureScript extends HTMLElement { this.#downloadButtons['Windows'] = this.#shadow.querySelector('.buttons')! .appendChild(this.#createDownloadButton('Windows')) } - this.update() + this.#render() } disconnectedCallback() { for (const os of Object.keys(this.#downloadButtons)) { this.#downloadButtons[os as OperatingSystem]!.removeEventListener('click', this.#onDownloadButtonClick) } + for (const configureBinaries of this.querySelectorAll('configure-binaries')) { + configureBinaries.removeEventListener(ARCHITECTURE_UPDATE_EVENT_TYPE, this.#onArchUpdate as EventListener) + } + removeChildNodes(this.#shadow) } - update() { + #render() { this.#shadow.querySelector('.commit')!.textContent = this.#repo.latestRelease?.commitHash || '' this.#shadow.querySelector('.header')!.style.viewTransitionName = `repo-${this.#repo.owner}-${this.#repo.name}` this.#shadow.querySelector('.name')!.textContent = `${this.#repo.owner}/${this.#repo.name}` this.#shadow.querySelector('.version')!.textContent = this.#repo.latestRelease?.tag || '' const binariesContainer = this.#shadow.querySelector('.files') as HTMLElement if (this.#repo.latestRelease?.binaries.length) { - if (this.#binaries['Linux'].length) { - binariesContainer.appendChild(new ConfigureBinaries(this.#binaries['Linux'], 'Linux')) - } - if (this.#binaries['MacOS'].length) { - binariesContainer.appendChild(new ConfigureBinaries(this.#binaries['MacOS'], 'MacOS')) - } - if (this.#binaries['Windows'].length) { - binariesContainer.appendChild(new ConfigureBinaries(this.#binaries['Windows'], 'Windows')) - } + this.#renderConfigureBinaries(binariesContainer, 'Linux') + this.#renderConfigureBinaries(binariesContainer, 'MacOS') + this.#renderConfigureBinaries(binariesContainer, 'Windows') + } else { + // todo will somebody think of the lack of content for repos without binaries? } + // todo nice to have render non binary file assets console.log('ConfigureScript', this.#repo.latestRelease?.binaries) } + #renderConfigureBinaries(container: HTMLElement, os: OperatingSystem) { + const bins = this.#binaries[os] + if (bins.length) { + container.appendChild(new ConfigureBinaries(bins, os)) + .addEventListener(ARCHITECTURE_UPDATE_EVENT_TYPE, this.#onArchUpdate as EventListener) + } + } + #createDownloadButton(os: OperatingSystem): HTMLButtonElement { const downloadButton = document.createElement('button') downloadButton.classList.add('download') @@ -105,38 +128,75 @@ export default class ConfigureScript extends HTMLElement { ` downloadButton.appendChild(document.createTextNode(`for ${os === 'Windows' ? 'Windows' : 'Linux and MacOS'}`)) downloadButton.addEventListener('click', this.#onDownloadButtonClick) - downloadButton.setAttribute('data-os', os) + downloadButton.disabled = !this.#isDownloadButtonEnabled(os) + return downloadButton + } + + #isDownloadButtonEnabled(os: OperatingSystem): boolean { if (os === 'Windows') { - downloadButton.disabled = true + return false + } else { + for (const filename of Object.keys(this.#architectureUnresolved)) { + if (this.#architectureUnresolved[filename] !== 'Windows') { + return false + } + } + return true + } + } + + #updateDownloadButtonEnabled(os: OperatingSystem) { + this.#downloadButtons[os]!.disabled = !(os === 'Windows' + ? this.#isDownloadButtonEnabled(os) + : (['Linux', 'MacOS'] as Array).every(os => this.#isDownloadButtonEnabled(os))) + } + + #onArchUpdate = (e: ArchitectureUpdateEvent) => { + this.#architectureResolved[e.detail.filename] = e.detail.arch + const resolvedBinOs = this.#architectureUnresolved[e.detail.filename] + if (resolvedBinOs) { + delete this.#architectureUnresolved[e.detail.filename] + this.#updateDownloadButtonEnabled(resolvedBinOs) } - return downloadButton } #onDownloadButtonClick = (/*e: MouseEvent*/) => { - // (e.target as HTMLButtonElement).getAttribute('data-os') + // todo support windows downloadScript(this.#buildGenerateScriptOptions()) + // todo save generated script and template version to db + // todo save resolved architecture values to db } // todo binaryName needs a ui input to override default of repo name #buildGenerateScriptOptions(): GenerateScriptOptions { + return { + binaryName: this.#repo.name, + files: this.#collectBinaryDistributions(), + repository: { + owner: this.#repo.owner, + name: this.#repo.name, + }, + } + } + + #collectBinaryDistributions(): Record { const files: Record = {} for (const os of Object.keys(this.#binaries)) { for (const binary of this.#binaries[os as OperatingSystem]) { - if (binary.arch) { - files[binary.filename] = { - arch: binary.arch, - os: binary.os, - } + files[binary.filename] = { + arch: binary.arch || this.#expectResolvedArchitecture(binary.filename), + os: binary.os, } } } - return { - repository: { - owner: this.#repo.owner, - name: this.#repo.name, - }, - binaryName: this.#repo.name, - files, + return files + } + + #expectResolvedArchitecture(filename: string): Architecture | never { + const arch = this.#architectureResolved[filename] + if (!arch) { + throw new Error('expected resolved architecture for ' + filename) } + return arch } } diff --git a/frontend/src/components/search/RepositoryLink.css b/frontend/src/components/search/RepositoryLink.css index 650157f..b714059 100644 --- a/frontend/src/components/search/RepositoryLink.css +++ b/frontend/src/components/search/RepositoryLink.css @@ -9,6 +9,7 @@ padding: 1rem; box-sizing: border-box; cursor: pointer; + transform-origin: center; transition: all .3s ease-in-out; } @@ -18,7 +19,11 @@ border-radius: 15px; padding: 1rem 1.5rem; transform: scale(.9); - transform-origin: center; +} + +.link:active { + transform: scale(.85) translateY(3px); + transition: all .075s ease-in-out; } .deets, .header { diff --git a/frontend/src/graphPaper.css b/frontend/src/graphPaper.css index dfe264d..efc0f73 100644 --- a/frontend/src/graphPaper.css +++ b/frontend/src/graphPaper.css @@ -11,9 +11,15 @@ html.out:not(.reader) #triangle { clip-path: polygon(0 0, 100% 0, 100% 10%, 90% 10%, 80% 20%, 20% 20%, 20% 80%, 10% 90%, 0 90%); } -#graph-paper { +#graph-paper-positioning { position: relative; left: 20vw; top: 20vh; width: 60vw; } + +#graph-paper { + margin: 10vw auto 0; + max-width: 38rem; + width: 40vw; +} diff --git a/frontend/src/graphPaper.ts b/frontend/src/graphPaper.ts index 599ab99..2fb6c25 100644 --- a/frontend/src/graphPaper.ts +++ b/frontend/src/graphPaper.ts @@ -33,7 +33,7 @@ function appendTriangle2() { } function createGraphPaper(): HTMLElement { - document.body.insertAdjacentHTML('beforeend', `
`) + document.body.insertAdjacentHTML('beforeend', `
`) return document.getElementById('graph-paper')! } diff --git a/frontend/src/routes/configure.css b/frontend/src/routes/configure.css deleted file mode 100644 index 97d3c9c..0000000 --- a/frontend/src/routes/configure.css +++ /dev/null @@ -1,4 +0,0 @@ -#graph-paper.configure { - box-sizing: border-box; - padding: 3rem; -} diff --git a/frontend/src/routes/configure.ts b/frontend/src/routes/configure.ts index 997ecca..72fca0a 100644 --- a/frontend/src/routes/configure.ts +++ b/frontend/src/routes/configure.ts @@ -1,7 +1,6 @@ import {type Repository} from '@eighty4/install-github' import ConfigureScript from '../components/configure/ConfigureScript.ts' import createGitHubGraphApiClient from '../createGitHubGraphApiClient.ts' -import './configure.css' import {showGraphPaper} from '../graphPaper.ts' import {removeChildNodes} from '../dom.ts' import {configureRepoCache} from '../sessionCache.ts' diff --git a/frontend/src/routes/search.css b/frontend/src/routes/search.css index c83cd6a..c348055 100644 --- a/frontend/src/routes/search.css +++ b/frontend/src/routes/search.css @@ -1,6 +1,9 @@ -#graph-paper.search { - box-sizing: border-box; - padding: 3rem; +#graph-paper.search-route h3 { + color: var(--card-text-color); + font-weight: 500; + font-size: 1.5rem; + padding-bottom: 2.25rem; + text-align: center; } repository-section + repository-section { diff --git a/frontend/src/routes/search.ts b/frontend/src/routes/search.ts index 8be6f6c..59bdccf 100644 --- a/frontend/src/routes/search.ts +++ b/frontend/src/routes/search.ts @@ -4,9 +4,8 @@ import emptyHtml from './search.empty.html?raw' import errorHtml from './search.error.html?raw' import createGitHubGraphApiClient from '../createGitHubGraphApiClient.ts' import {removeChildNodes} from '../dom.ts' -import {onClearGraphPaper, showGraphPaper} from '../graphPaper.ts' +import {showGraphPaper} from '../graphPaper.ts' import {createSessionCache, gitHubUserCache} from '../sessionCache.ts' -import {clearPageHeader, setPageHeader} from '../ui.ts' import RepositorySection from '../components/search/RepositorySection.ts' type RepoSectionType = 'generated' | 'released' | 'compatible' @@ -26,12 +25,10 @@ const projectsCache = createSessionCache>('search.projects') export function findProgramRepository() { showGraphPaper((graphPaper) => { - graphPaper.classList.add('search') - setPageHeader(`${gitHubUserCache.read()!.login}'s repos`) - onClearGraphPaper(clearPageHeader) + graphPaper.classList.add('search-route') + let loading: boolean = true let projects = projectsCache.read() - if (projects?.length) { showProjects(projects) } else { @@ -67,6 +64,7 @@ export function findProgramRepository() { }) } if (primaryRepoGroups.length) { + graphPaper.innerHTML = `

${gitHubUserCache.read()!.login}'s repos

` showPrimaryRepoSections(primaryRepoGroups) } else { showGuideOnEmptyProjects() diff --git a/frontend/src/ui.css b/frontend/src/ui.css index 5e79aec..2b91a58 100644 --- a/frontend/src/ui.css +++ b/frontend/src/ui.css @@ -1,13 +1,3 @@ -#page-header { - grid-area: 2 / 3 / 3 / -3; - color: var(--head-text-color); - font-size: 1.45rem; - display: flex; - justify-content: center; - align-items: flex-end; - padding-bottom: 2vh; -} - :root { --out-transition-duration: 400ms; } diff --git a/frontend/src/ui.ts b/frontend/src/ui.ts index 2ab399a..ba2231e 100644 --- a/frontend/src/ui.ts +++ b/frontend/src/ui.ts @@ -19,17 +19,3 @@ export async function toggleReaderMode(openOrClose: boolean): Promise { }, {once: true}) }) } - -export function setPageHeader(header: string) { - (document.getElementById('page-header') || createPageHeader()).innerText = header -} - -function createPageHeader(): HTMLDivElement { - const pageHeader = document.createElement('div') - pageHeader.id = 'page-header' - return document.getElementById('grid')!.appendChild(pageHeader) -} - -export function clearPageHeader() { - document.getElementById('page-header')?.remove() -} diff --git a/offline/src/data.ts b/offline/src/data.ts index 941f5dd..2e46141 100644 --- a/offline/src/data.ts +++ b/offline/src/data.ts @@ -163,7 +163,7 @@ export const repositories: Record = { createdAt: '2024-01-01', binaries: [{ contentType: 'application/x-executable', - filename: 'maestro-linux-amd64', + filename: 'maestro-linux', os: 'Linux', }], otherAssets: [], diff --git a/template/src/Distrubtions.ts b/template/src/Distrubtions.ts index d2a82f4..23d257b 100644 --- a/template/src/Distrubtions.ts +++ b/template/src/Distrubtions.ts @@ -1,6 +1,8 @@ // https://en.wikipedia.org/wiki/Uname#Examples export type Architecture = 'aarch64' | 'arm' | 'x86_64' +export const ARCHITECTURES: Array = ['aarch64', 'arm', 'x86_64'] + export type OperatingSystem = 'Linux' | 'MacOS' | 'Windows' export function operatingSystemLabel(os: OperatingSystem): string { diff --git a/template/src/Template.ts b/template/src/Template.ts index 32f4087..58d3ddd 100644 --- a/template/src/Template.ts +++ b/template/src/Template.ts @@ -1,4 +1,4 @@ export type {Architecture, Distribution, OperatingSystem} from './Distrubtions.js' -export {operatingSystemLabel} from './Distrubtions.js' +export {ARCHITECTURES, operatingSystemLabel} from './Distrubtions.js' export {generateScript, type GenerateScriptOptions} from './Generate.js' export {resolveDistribution} from './Resolution.js'