From e35a72336a00792b51243c390497fd129a2cdb0f Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 19 Aug 2026 08:17:43 +0200 Subject: [PATCH] Upgrade from the card, behind a three-dot menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An installed title's version line now reads `0.1 → 0.3` where the catalog has moved on, so a card answers both questions somebody brings to it: what is installed, and is there anything better. Which version is installed was already recorded — `state.json` has always carried it — what was missing was anywhere to act on it. The card leads with Play (or Open, for a hosted title) and puts the rest behind a ⋮ button: Upgrade, which fetches whatever the catalog now has, and Uninstall. Upgrade stays visible while disabled rather than appearing and disappearing — a menu whose items come and go makes a person hunt for the one they used last time, and greyed out already says "not now". Playing stays the headline even with an upgrade waiting: the build on the disk still runs, and wanting to play it is not the same as wanting to wait for a download. The menu is a `
`, so its open state is the DOM's and the keyboard needs no teaching. Closing it on an outside click is the grid's job, not a card's: cards are rebuilt on every render, so a listener per card would be a listener per render. Package names lose their spaces — `WarpEngineClient-2.3.0-arm64.dmg` — because a space in a release asset is a space in every curl, script and shell command that touches it. Set per target rather than globally: nsis and portable would otherwise resolve to the same .exe name and overwrite each other. `productName` is untouched, so the app is still called WarpEngine Client where a person sees it — in the Dock and in /Applications. Tested on a sandbox store by rewriting one state record to claim an older build, which is what the engine actually compares: the window then offered `Upgrade:on` for that title and `Upgrade:off` for the current one, and pressing it took the record from 0.1 to 0.2 with the old payload removed first. The self-test asserts that pairing on every installed card, because a closed menu photographs identically whether or not its items are right. In Hungarian the catalog refresh and the new Upgrade both wanted "Frissítés"; the refresh is an icon with a tooltip, and a tooltip can afford to say *Katalógus frissítése*. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 11 ++- RELEASE_NOTES.md | 81 ++++++++-------- package.json | 17 +++- scripts/ci-upload.sh | 7 +- scripts/release.sh | 7 +- src/main/diagnostics/SelfTestRunner.ts | 21 ++++- src/renderer/RendererApplication.ts | 3 + src/renderer/style.css | 50 ++++++++++ src/renderer/views/CatalogGridView.ts | 21 ++++- src/renderer/views/GameCardView.ts | 122 +++++++++++++++++++++---- src/shared/i18n/EnglishMessages.ts | 7 +- src/shared/i18n/HungarianMessages.ts | 7 +- 12 files changed, 275 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 66221b6..dbb25f7 100644 --- a/README.md +++ b/README.md @@ -272,9 +272,14 @@ on its own: publishing 1.2.0 got *"invalid username, password or token"* on the second package while the first had just gone up with the same token, and the same command succeeded immediately afterwards. -Package names contain a space — `WarpEngine Client-1.5.0-arm64.dmg` — so the list of -files is passed one path per line rather than as one string; splitting it on -whitespace is what broke the first attempt at publishing 1.1.0. +Package names have no spaces in them — `WarpEngineClient-2.3.0-arm64.dmg` — because a +space in a release asset is a space in every `curl`, script and shell command that ever +touches it. The app itself is still called **WarpEngine Client**: that name is what +appears in the Dock and in `/Applications`, and only the file names were the problem. + +The list of files is still passed one path per line rather than as one string, since a +path given on the command line can contain a space even when a built one cannot; +splitting it on whitespace is what broke the first attempt at publishing 1.1.0. It needs `tea` installed and logged in — the devarea repo has `make tea` for that. Overridable: `TAG`, `REPO`, `TEA_LOGIN`, `NOTES`, `DIST`. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d9005af..3659b27 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,41 +1,39 @@ -# WarpEngine Client 2.2.0 +# WarpEngine Client 2.3.0 -**A registry record is a name and a catalog.** Nothing else. The `config` field added in -2.1.0 is gone, and so is `storeRepositoryUrl` — along with the two store repositories they -pointed at, which have been deleted. +**A card says which version you have, and offers the newer one.** Where the catalog has +moved on, an installed title's version line reads `0.1 → 0.3` instead of just the number +on your disk, so the card answers both questions a person came to it with: what is +installed, and is there anything better. -2.1.0 had the registry say how each store behaves. That was the wrong shape. How a store -behaves is fixed per installed client: this application carries its own store engine and -is the only thing that can see the machine it is running on. A copy of that on a server -was a second authority over decisions this side had already made correctly — including -which directories the store is allowed to delete from — and a second authority is a way -for the two to disagree. +Which version is installed was already recorded — that is what `state.json` has always +been for. What was missing was somewhere to act on it. -Keeping two stores on one machine out of each other's files needs none of it. It is a -subfolder, and the client derives it: +**The actions moved into a three-dot menu.** Installed cards now lead with **Play** (or +**Open** for a hosted title) and put the rest behind the ⋮ button beside it: | | | |---|---| -| **store id** | a slug of the catalog host — `teletypegames.org` becomes `teletypegames` | -| **store home** | `-desktop`, under the usual store root | -| **games folder** | ``, inside the OS's usual place for programs | -| **delete boundary** | that same folder, and nothing outside it | +| **Upgrade** | fetches whatever the catalog now has, replacing the old payload and menu entry. Greyed out when there is nothing newer | +| **Uninstall** | as before — the payload, the icon and the menu entry, and nothing else | -Derived from the *catalog* on purpose: the catalog is what a store is, so two records -naming the same one are the same store and land in the same place. Installing the same -store twice is idempotent rather than a way to orphan what is already there. +Upgrade stays visible while disabled rather than appearing and disappearing: a menu whose +items come and go makes a person hunt for the one they used last time, and greyed out +already says "not now". -**Existing installations are untouched.** A store already on the machine is recognised by -the `config.json` in its own home and keeps the identity written there — a store installed -as `ttg` stays `ttg`, in `ttg-desktop`, with its games where they are. Only a *new* install -derives its id from the catalog. +Playing stays the headline action even when an upgrade is waiting. The version on the disk +still runs, and wanting to play it is not the same as wanting to wait for a download. + +**Package names have no spaces.** `WarpEngineClient-2.3.0-arm64.dmg` rather than +`WarpEngine Client-2.3.0-arm64.dmg`, and the same for the AppImage and both Windows +builds — a space in a release asset is a space in every `curl`, script and shell command +that ever touches it. The app is still called **WarpEngine Client**, which is what appears +in the Dock and in `/Applications`; only the file names changed. ### Also -The public `/stores` page loses its desktop card, which advertised a `curl … | sh` for a -repository that no longer exists; an ordinary computer is served by the app. The `/desktop` -URL now lands on the app rather than on a device tab, so what someone typing it wants is -what they get. +The catalog refresh control's tooltip now says *Refresh the catalog*, because in Hungarian +it and the new Upgrade both wanted the word "Frissítés" and only one of them reloads a +list. ### Opening it on macOS @@ -54,18 +52,21 @@ Windows (installer, portable) packages the pipeline builds when the tag is pushe `make check` is clean: typecheck, lint, the headless smoke test and the window self-test. -The install path was measured end to end against a local registry serving exactly the new -record shape. The slug came out `teletypegames`, the home `teletypegames-desktop`, the -games subfolder `teletypegames`, and installing the same record twice landed in the same -home. A record carrying `config` and `storeRepositoryUrl` — what a stale registry or a -tampering renderer might still send — changed nothing on disk, because neither field exists -in the model any more. +The upgrade path was tested on a sandbox store rather than reasoned about. Two titles were +installed, then one of their state records was rewritten to claim an older build — which is +exactly what the engine compares — and the window was asked what it would offer: -On this machine the check also shows both halves of the compatibility claim at once: the -registry's new slug reads `teletypegames`, while the store that was installed before today -is still found as `ttg` in `ttg-desktop`, listing its ten installed titles from -`Application Support/teletypegames`. +``` +BombExpert [newer] Upgrade:on Uninstall:on +Rabbit Roller [current] Upgrade:off Uninstall:on +``` -The site side was migrated and its specs re-run, and the frontend was built — which first -required removing a dead `engines` list that had been failing `vue-tsc` on master, so that -page could not be built to check at all. +Pressing Upgrade runs the same call the window makes, and the record went from `0.1` to +`0.2` with the old payload removed first. The self-test now asserts that pairing on every +installed card, because a closed menu photographs identically whether or not its items are +right. + +The new package names were read off a real build (`WarpEngineClient-2.3.0-arm64.dmg`, +`…-arm64-mac.zip`) and checked against the release script's own file filter, which finds +both. The Windows names are `-Setup-` and `-Portable-` so the two `.exe` targets cannot +resolve to one name and overwrite each other. diff --git a/package.json b/package.json index d741356..e7e60b3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "warp-engine-client", "productName": "WarpEngine Client", - "version": "2.2.0", + "version": "2.3.0", "description": "Graphical client for WarpEngine stores: install a catalog into your own application menu.", "license": "MIT", "author": "Teletype Games ", @@ -44,7 +44,11 @@ "target": [ "dmg", "zip" - ] + ], + "artifactName": "WarpEngineClient-${version}-${arch}-mac.${ext}" + }, + "dmg": { + "artifactName": "WarpEngineClient-${version}-${arch}.${ext}" }, "win": { "target": [ @@ -52,6 +56,12 @@ "portable" ] }, + "nsis": { + "artifactName": "WarpEngineClient-Setup-${version}-${arch}.${ext}" + }, + "portable": { + "artifactName": "WarpEngineClient-Portable-${version}-${arch}.${ext}" + }, "linux": { "category": "Game", "target": [ @@ -59,6 +69,9 @@ "deb" ] }, + "appImage": { + "artifactName": "WarpEngineClient-${version}-${arch}.${ext}" + }, "afterPack": "scripts/after-pack.js" }, "allowScripts": { diff --git a/scripts/ci-upload.sh b/scripts/ci-upload.sh index 4679928..11a366e 100755 --- a/scripts/ci-upload.sh +++ b/scripts/ci-upload.sh @@ -53,9 +53,10 @@ api() { curl -fsS -X "$method" -H "$AUTH" "$FORGE$path" "$@" } -# Package names contain spaces — "WarpEngine Client Setup 1.5.0.exe" does — so the list -# lives one path per line in a file and is read with `while IFS= read -r`. A single -# variable looped over with $list splits on the space and uploads nothing. +# The list lives one path per line in a file and is read with `while IFS= read -r`. The +# built package names have no spaces in them any more, but a path given on the command +# line still can — and a single variable looped over with $list splits on the space and +# uploads nothing, which is a silent way to publish a release with no assets. LIST="$(mktemp)" trap 'rm -f "$LIST"' EXIT if [ "$#" -gt 0 ]; then diff --git a/scripts/release.sh b/scripts/release.sh index 1339728..d8f9c0f 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -43,9 +43,10 @@ REPO="${REPO:-$(git remote get-url origin 2>/dev/null | # # - the version filter, because dist/ keeps whatever earlier builds left there # and a release would quietly get the previous version's files attached; -# - the spaces. "WarpEngine Client-1.5.0-arm64.dmg" has one, so the list lives one -# path per line in a file and is read with `while IFS= read -r`. Holding it in -# a single variable and looping over $list splits it on the space. +# - the spaces. The built names have none since 2.3.0 — `WarpEngineClient-2.3.0-arm64.dmg` +# — but a path given as an argument still can, so the list stays one path per line in +# a file, read with `while IFS= read -r`. Holding it in a single variable and looping +# over $list splits it on the space, and publishes nothing. LIST="$(mktemp)" trap 'rm -f "$LIST"' EXIT if [ "$#" -gt 0 ]; then diff --git a/src/main/diagnostics/SelfTestRunner.ts b/src/main/diagnostics/SelfTestRunner.ts index 1b843bd..16cd844 100644 --- a/src/main/diagnostics/SelfTestRunner.ts +++ b/src/main/diagnostics/SelfTestRunner.ts @@ -28,6 +28,8 @@ interface SelfTestReport { readonly locales: readonly string[] /** `:` per icon-only control in the footer. */ readonly iconControls: readonly string[] + /** ` [current|newer] Upgrade:on|off Uninstall:on|off` per installed card. */ + readonly cardMenus: readonly string[] } /** What changed after clicking a store that was not open. */ @@ -74,7 +76,11 @@ export class SelfTestRunner { // picker are the only way to reach those two actions now that neither has a label. const iconsNamed = report.iconControls.length === 2 && report.iconControls.every((control: string): boolean => /^.+:1$/.test(control)) - const rendered = report.locales.length > 1 && iconsNamed && ( + // Every installed card offers both actions, and Upgrade is enabled exactly when the + // version line says there is something newer. Uninstall is always available. + const menusAgree = report.cardMenus.every((entry: string): boolean => + /\[newer\] \S+:on \S+:on$/.test(entry) || /\[current\] \S+:off \S+:on$/.test(entry)) + const rendered = report.locales.length > 1 && iconsNamed && menusAgree && ( (report.cards > 0 && !report.gateVisible && report.stores.length > 0 && report.categories.length > 0 && report.activeCategory !== null) || (report.gateVisible && report.gateAction.length > 0)) @@ -107,6 +113,16 @@ export class SelfTestRunner { locales: [...document.getElementById('locale').options].map((option) => option.value), // The two icon-only controls: a glyph with no accessible name is a button nobody // can identify, and the failure is silent because the icon still draws. + // One entry per installed card: its title, whether the version line shows an + // upgrade, and the menu's two items with their disabled state. This is the only + // way to see that Upgrade is offered exactly when there is something newer — + // a screenshot shows a closed menu. + cardMenus: [...document.querySelectorAll('.card.is-installed')].map((card) => { + const items = [...card.querySelectorAll('.menu-item')] + .map((item) => item.textContent + (item.disabled ? ':off' : ':on')) + const arrow = card.querySelector('.version.has-update') === null ? 'current' : 'newer' + return (card.querySelector('h2') || {}).textContent + ' [' + arrow + '] ' + items.join(' ') + }), iconControls: [...document.querySelectorAll('.side-tools .icon-btn')] .map((control) => { const named = control.getAttribute('aria-label') || control.getAttribute('title') || @@ -132,7 +148,8 @@ export class SelfTestRunner { paths: readString(record, 'paths'), logLines: readNumber(record, 'logLines'), locales: readStringArray(record, 'locales'), - iconControls: readStringArray(record, 'iconControls') + iconControls: readStringArray(record, 'iconControls'), + cardMenus: readStringArray(record, 'cardMenus') } } diff --git a/src/renderer/RendererApplication.ts b/src/renderer/RendererApplication.ts index 6b7332b..6d0a926 100644 --- a/src/renderer/RendererApplication.ts +++ b/src/renderer/RendererApplication.ts @@ -47,6 +47,9 @@ export class RendererApplication { this.grid = new CatalogGridView(new GameCardView({ onInstall: (name: string): void => { void this.catalog.syncGames([name]) }, + // The same call as an install: a sync of one name fetches whatever the catalog + // now has for it, and the engine replaces the old payload and menu entry. + onUpgrade: (name: string): void => { void this.catalog.syncGames([name]) }, onLaunch: (name: string): void => { void this.catalog.launchGame(name) }, onRemove: (name: string): void => { void this.catalog.removeGame(name) } })) diff --git a/src/renderer/style.css b/src/renderer/style.css index 70c6f6d..e45d3df 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -357,6 +357,56 @@ body.nav-closed .side { margin-left: calc(-1 * var(--side-width)); } } .actions { display: flex; gap: 8px; margin-top: auto; } +/* --- the card's actions menu -------------------------------------------- */ +/* + * A <details> holding the two actions that are not the card's headline. The card is the + * positioning context, and the panel is pinned to the button's right edge so it opens + * inward rather than off the side of the grid. + */ +.menu { position: relative; margin-left: auto; } +.menu-toggle { list-style: none; } +/* Safari and Chrome each draw their own marker on a summary; both have to go. */ +.menu-toggle::-webkit-details-marker { display: none; } +.menu-toggle::marker { content: ''; } +.menu[open] .menu-toggle { color: var(--ink); background: var(--panel-2); border-color: var(--line); } + +.menu-items { + position: absolute; + right: 0; + bottom: calc(100% + 6px); + z-index: 20; + min-width: 148px; + display: flex; + flex-direction: column; + padding: 4px; + gap: 2px; + background: var(--panel-2); + border: 1px solid var(--line); + border-radius: 10px; + box-shadow: 0 10px 28px rgb(0 0 0 / .45); +} +.menu-item { + font: inherit; + font-size: 13px; + text-align: left; + padding: 7px 10px; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--ink); + cursor: pointer; + white-space: nowrap; +} +.menu-item:hover:not(:disabled) { background: #2b3746; } +.menu-item:disabled { color: var(--ink-dim); opacity: .5; cursor: default; } + +.icon-dots { fill: currentColor; stroke: none; } + +/* An installed version with a newer one behind it: the arrow carries the news, so the + colour only has to make it findable in a grid. */ +.version.has-update { color: var(--accent); font-weight: 600; } + + /* --- log ---------------------------------------------------------------- No permanent footer: the panel is in the flow only while it is open, and the switch for it sits in the side menu with everything else that is not a title. */ diff --git a/src/renderer/views/CatalogGridView.ts b/src/renderer/views/CatalogGridView.ts index b4f5063..d1a922b 100644 --- a/src/renderer/views/CatalogGridView.ts +++ b/src/renderer/views/CatalogGridView.ts @@ -9,7 +9,26 @@ export class CatalogGridView { private readonly grid = requireElement('grid', HTMLElement) private readonly empty = requireElement('empty', HTMLElement) - public constructor (private readonly cards: GameCardView) {} + public constructor (private readonly cards: GameCardView) { + this.closeMenusOnOutsideClick() + } + + /** + * One listener for every card's actions menu. + * + * A `<details>` does not close when the pointer goes elsewhere, and a card cannot own + * this: cards are rebuilt on every render, so a listener per card would be a listener + * per render. The grid is created once, which makes it the right place for it. + */ + private closeMenusOnOutsideClick (): void { + document.addEventListener('click', (event: MouseEvent): void => { + const target = event.target + const clicked = target instanceof Node ? target : null + for (const menu of this.grid.querySelectorAll('details.menu[open]')) { + if (clicked === null || !menu.contains(clicked)) menu.removeAttribute('open') + } + }) + } public render (state: AppState): void { const shown = state.games.filter((game: GameDto): boolean => matchesFilter(game, state.filter)) diff --git a/src/renderer/views/GameCardView.ts b/src/renderer/views/GameCardView.ts index 1b58f54..a946840 100644 --- a/src/renderer/views/GameCardView.ts +++ b/src/renderer/views/GameCardView.ts @@ -4,6 +4,7 @@ import { createElement } from '../dom/Dom' export interface GameCardViewCallbacks { readonly onInstall: (name: string) => void + readonly onUpgrade: (name: string) => void readonly onLaunch: (name: string) => void readonly onRemove: (name: string) => void } @@ -11,8 +12,11 @@ export interface GameCardViewCallbacks { /** * One card. * - * A card is a function of a title and the strings: it holds no state of its own, so - * the grid can throw the lot away and rebuild after every listing. + * A card is a function of a title and the strings: it holds no state of its own, so the + * grid can throw the lot away and rebuild after every listing. The one exception is the + * actions menu, whose open/closed state lives in a `<details>` element — and being + * thrown away is exactly what should happen to an open menu when the catalog changes + * under it. */ export class GameCardView { public constructor (private readonly callbacks: GameCardViewCallbacks) {} @@ -71,13 +75,30 @@ export class GameCardView { meta.appendChild(badge) } meta.appendChild(createElement('span', 'badge badge-plain', game.platform)) - meta.appendChild(createElement('span', 'version', - game.installed && game.installedVersion !== null - ? `${game.installedVersion} · ${messages.installed}` - : game.version)) + meta.appendChild(this.createVersion(game, messages)) return meta } + /** + * The version, and what the catalog has that this machine does not. + * + * Both numbers, when they differ: which version is installed is the thing a person + * came to the card to find out, and "there is a newer one" is only meaningful next to + * it. Where they agree, the second number would be noise. + */ + private createVersion (game: GameDto, messages: MessageBundle): HTMLElement { + if (!game.installed || game.installedVersion === null) { + return createElement('span', 'version', game.version) + } + if (!game.updateAvailable) { + return createElement('span', 'version', `${game.installedVersion} · ${messages.installed}`) + } + const version = createElement('span', 'version has-update', + `${game.installedVersion} → ${game.version}`) + version.title = messages.updateAvailable + return version + } + private createActions (game: GameDto, messages: MessageBundle, busy: boolean): HTMLElement { const actions = createElement('div', 'actions') @@ -89,25 +110,88 @@ export class GameCardView { return actions } - const primary = createElement('button', 'btn btn-primary') - primary.disabled = busy + actions.appendChild(this.createPrimary(game, messages, busy)) + // Only an installed title has anything in the menu: nothing to upgrade and nothing + // to uninstall until there is something on the disk. + if (game.installed) actions.appendChild(this.createMenu(game, messages, busy)) + return actions + } - if (game.installed && !game.updateAvailable) { + /** + * The one action a card leads with. + * + * For an installed title that is playing it — including when an upgrade is waiting, + * because the version on the disk still runs and wanting to play it is not the same + * as wanting to wait for a download. + */ + private createPrimary (game: GameDto, messages: MessageBundle, busy: boolean): HTMLElement { + const primary = createElement('button', 'btn btn-primary') + if (game.installed) { primary.textContent = game.mode === 'web' ? messages.open : messages.play primary.disabled = busy || !game.launchable primary.addEventListener('click', (): void => { this.callbacks.onLaunch(game.name) }) - } else { - primary.textContent = game.updateAvailable ? messages.update : messages.install - primary.addEventListener('click', (): void => { this.callbacks.onInstall(game.name) }) + return primary } - actions.appendChild(primary) + primary.textContent = messages.install + primary.disabled = busy + primary.addEventListener('click', (): void => { this.callbacks.onInstall(game.name) }) + return primary + } - if (game.installed) { - const remove = createElement('button', 'btn btn-ghost', messages.remove) - remove.disabled = busy - remove.addEventListener('click', (): void => { this.callbacks.onRemove(game.name) }) - actions.appendChild(remove) + /** + * The three-dot menu: upgrade, and uninstall. + * + * A `<details>` rather than a scripted popover, so the open state is the DOM's and the + * keyboard works without being taught to. Upgrade is present but disabled when there + * is nothing newer — a menu whose items appear and disappear makes a person hunt for + * the one they used last time, and "greyed out" already says "not now". + */ + private createMenu (game: GameDto, messages: MessageBundle, busy: boolean): HTMLElement { + const menu = createElement('details', 'menu') + const toggle = createElement('summary', 'icon-btn menu-toggle') + toggle.title = messages.moreActions + toggle.setAttribute('aria-label', messages.moreActions) + toggle.appendChild(this.createDotsIcon()) + menu.appendChild(toggle) + + const items = createElement('div', 'menu-items') + items.appendChild(this.createMenuItem(messages.upgrade, busy || !game.updateAvailable, + (): void => { this.callbacks.onUpgrade(game.name) })) + items.appendChild(this.createMenuItem(messages.uninstall, busy, + (): void => { this.callbacks.onRemove(game.name) })) + menu.appendChild(items) + return menu + } + + private createMenuItem ( + label: string, + disabled: boolean, + perform: () => void + ): HTMLButtonElement { + const item = createElement('button', 'menu-item', label) + item.disabled = disabled + item.addEventListener('click', (): void => { + // Close before acting: the click starts work that re-renders the grid, and a menu + // left open would vanish mid-gesture rather than answer the press. + item.closest('details')?.removeAttribute('open') + perform() + }) + return item + } + + private createDotsIcon (): SVGSVGElement { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('class', 'icon icon-dots') + svg.setAttribute('viewBox', '0 0 16 16') + svg.setAttribute('aria-hidden', 'true') + svg.setAttribute('focusable', 'false') + for (const y of [3.5, 8, 12.5]) { + const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + dot.setAttribute('cx', '8') + dot.setAttribute('cy', String(y)) + dot.setAttribute('r', '1.35') + svg.appendChild(dot) } - return actions + return svg } } diff --git a/src/shared/i18n/EnglishMessages.ts b/src/shared/i18n/EnglishMessages.ts index 0550e87..b7f2bf0 100644 --- a/src/shared/i18n/EnglishMessages.ts +++ b/src/shared/i18n/EnglishMessages.ts @@ -6,12 +6,13 @@ */ export const ENGLISH_MESSAGES = { appName: 'WarpEngine Client', - refresh: 'Refresh', + refresh: 'Refresh the catalog', install: 'Install', - update: 'Update', + upgrade: 'Upgrade', play: 'Play', open: 'Open', - remove: 'Remove', + uninstall: 'Uninstall', + moreActions: 'More actions', installed: 'installed', native: 'native', hosted: 'hosted', diff --git a/src/shared/i18n/HungarianMessages.ts b/src/shared/i18n/HungarianMessages.ts index 0abcbc8..1381a17 100644 --- a/src/shared/i18n/HungarianMessages.ts +++ b/src/shared/i18n/HungarianMessages.ts @@ -6,12 +6,13 @@ import type { MessageBundle } from './MessageBundle' */ export const HUNGARIAN_MESSAGES: MessageBundle = { appName: 'WarpEngine Client', - refresh: 'Frissítés', + refresh: 'Katalógus frissítése', install: 'Telepítés', - update: 'Frissítés', + upgrade: 'Frissítés', play: 'Indítás', open: 'Megnyitás', - remove: 'Eltávolítás', + uninstall: 'Eltávolítás', + moreActions: 'További műveletek', installed: 'telepítve', native: 'natív', hosted: 'hosztolt',