ci/woodpecker/manual/woodpecker Pipeline was successful
A store no longer needs a repository of its own. The engine's built-in defaults already cover the host-to-asset mapping, the install modes, the platforms and the behaviour; what they cannot know is identity — a slug, a name and a catalog URL — and that is exactly what a registry record carries. So `storeRepositoryUrl` is optional: a record with a name and a catalog is a complete store, the id falls back from the repository name to the catalog host (`teletypegames.org` becomes `teletypegames`) to the display name, and the client writes a three-section config. Given a repository it still reads it, and that file stays the authority on how the store behaves; a repository without a config.json is treated as no repository at all. Measured end to end against a local registry serving one record with a null repository: the engine and the core downloaded, engine 1.1.0 accepted the written config, it listed the same ten titles the configured store does, and a hosted title synced into a sandbox with its menu entry written. The "Install all" button is gone, and with it the string it used. Titles are installed one at a time from their own cards. No footer. The window carried a bar at the bottom at all times — a toggle and a line of absolute paths — for something most sessions never need. The log is still there, folder buttons included, behind a quiet switch at the bottom of the side menu; it takes no room until it is opened, and an arriving line does not open it, because the store logs on every refresh and a window that unfolds panels by itself is worse than one that keeps quiet. Three faults that every automated count had passed, found by photographing the setup screen: the store badge rendered as an empty pill with no store open; the gate's picker showed as an empty dropdown stub, because an explicit `display` beats the browser's own `[hidden]` rule; and the gate went up while the empty-catalog line stayed on screen underneath it. The last was a design fault — whether the gate is up was a call on a view rather than state, so the two could disagree. The setup screen is now a field in the state store, and that one field decides which of the gate and the grid is drawn. The window test's gate assertion was wrong too: it demanded a store picker, which only appears when the registry offers more than one store, so one store — the ordinary case — failed it. CI builds the packages this machine cannot. `.woodpecker.yaml` runs the checks on every push and, on a tag or by hand, builds the Linux packages in `electronuserland/builder:22` and the Windows ones in `:22-wine`, then attaches them to the release with scripts/ci-upload.sh. The pipeline lives here rather than in the update server's `/build/config` extension, which serves game-platform pipelines publishing into the site's catalog — a different product with a different target. macOS stays a local build: Apple's toolchain and its signing exist only on a Mac. Both build steps verify what they produced, because a half-finished Wine build leaves a 162 KB stub named like the real installer and `ls` is happy with it. The Linux step was rehearsed locally in the same image (AppImage 128 MB, deb 100 MB); the Wine step cannot be rehearsed on Apple Silicon, where 16 KB host pages break Wine's 4 KB assumption, so the runner is where it is proven. The size check was tested against both outcomes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
import { createElement, requireElement, setHidden, setText } from '../dom/Dom'
|
|
import type { AppState } from '../state/AppStore'
|
|
|
|
const MAX_LOG_LINES = 400
|
|
|
|
export interface LogDrawerViewCallbacks {
|
|
readonly onOpenFolder: (directory: string) => void
|
|
}
|
|
|
|
/**
|
|
* The store's own output, verbatim, and the folders everything lands in.
|
|
*
|
|
* Off screen until asked for. The window used to carry a footer with a toggle and a
|
|
* line of absolute paths at all times; both were clutter next to the one thing the
|
|
* window is for, which is the titles. The switch lives in the side menu with the rest
|
|
* of what is not a title, and the panel appears above the grid only while it is on.
|
|
*/
|
|
export class LogDrawerView {
|
|
private readonly toggle = requireElement('log-toggle', HTMLButtonElement)
|
|
private readonly panel = requireElement('log-panel', HTMLElement)
|
|
private readonly lines = requireElement('log-lines', HTMLElement)
|
|
private readonly pathsBox = requireElement('log-paths', HTMLElement)
|
|
private open = false
|
|
|
|
public constructor (private readonly callbacks: LogDrawerViewCallbacks) {
|
|
this.toggle.addEventListener('click', (): void => { this.setOpen(!this.open) })
|
|
}
|
|
|
|
public render (state: AppState): void {
|
|
setText(this.toggle, state.messages.log)
|
|
this.pathsBox.replaceChildren()
|
|
const paths = state.paths
|
|
if (paths === null) return
|
|
|
|
this.pathsBox.appendChild(createElement('div', 'paths-line',
|
|
`${state.messages.paths}: ${paths.storeFolder} · ${paths.menuGroup}`))
|
|
|
|
const folders: readonly (readonly [string, string])[] = [
|
|
[state.messages.openStoreFolder, paths.storeFolder],
|
|
[state.messages.openMenuFolder, paths.menuGroup]
|
|
]
|
|
for (const [label, directory] of folders) {
|
|
const button = createElement('button', 'btn btn-tiny', label)
|
|
button.addEventListener('click', (): void => { this.callbacks.onOpenFolder(directory) })
|
|
this.pathsBox.appendChild(button)
|
|
}
|
|
}
|
|
|
|
private setOpen (open: boolean): void {
|
|
this.open = open
|
|
setHidden(this.panel, !open)
|
|
this.toggle.setAttribute('aria-expanded', String(open))
|
|
this.toggle.classList.toggle('is-active', open)
|
|
if (open) this.lines.scrollTop = this.lines.scrollHeight
|
|
}
|
|
|
|
/**
|
|
* A line arriving while the panel is shut does not open it: the store logs on every
|
|
* refresh, and a window that unfolded a panel by itself would be worse than one that
|
|
* kept quiet. The lines are kept, so opening it later shows what happened.
|
|
*/
|
|
public appendLine (line: string): void {
|
|
this.lines.appendChild(createElement('div', 'log-line', line))
|
|
while (this.lines.childElementCount > MAX_LOG_LINES) {
|
|
const first = this.lines.firstElementChild
|
|
if (first === null) break
|
|
first.remove()
|
|
}
|
|
this.lines.scrollTop = this.lines.scrollHeight
|
|
}
|
|
}
|