From 6336ed0b6247af9d76cb9db037f27d9a8b644ac5 Mon Sep 17 00:00:00 2001 From: Andrii Shevchuk-Yuhanets Date: Wed, 28 Oct 2026 14:01:50 -0400 Subject: [PATCH 1/2] feat: X/Y shortcuts on Search results, title the page Search & Install Y on a result's Install shows the latest release's notes and X downloads its ZIP, like on Discover cards. The Quick Access button stays "Search"; the page itself is titled "Search & Install". Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- devtools/decky-mock/e2e/deckyhub.spec.ts | 27 ++++++++++++ src/i18n/de.ts | 1 + src/i18n/en.ts | 1 + src/i18n/es.ts | 1 + src/i18n/fr.ts | 1 + src/i18n/ja.ts | 1 + src/i18n/ru.ts | 1 + src/i18n/uk.ts | 1 + src/i18n/zh.ts | 1 + src/pages/SearchPage.tsx | 56 +++++++++++++++++++----- 11 files changed, 81 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 05210ad..a621e25 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ No setup, no configuration — the curated list shows up in Discover the moment ### Controller buttons -DeckyHub uses the standard Steam Deck controls, plus two extra shortcuts on every **Install** / **Update** button (on Discover and Updates cards, in the [Manage](#manage) window, and for DeckyHub's own update): +DeckyHub uses the standard Steam Deck controls, plus two extra shortcuts on every **Install** / **Update** button (on Discover and Updates cards, on **Search** results, in the [Manage](#manage) window, and for DeckyHub's own update): | Button | What it does | | --- | --- | diff --git a/devtools/decky-mock/e2e/deckyhub.spec.ts b/devtools/decky-mock/e2e/deckyhub.spec.ts index db57995..abba7b8 100644 --- a/devtools/decky-mock/e2e/deckyhub.spec.ts +++ b/devtools/decky-mock/e2e/deckyhub.spec.ts @@ -803,6 +803,7 @@ test("Search installs a plugin straight from its GitHub release", async ({ page await page.goto("/?bridge=http://127.0.0.1:8643"); const app = mock(page); await app.locator(".qam-content").getByRole("button", { name: "Search", exact: true }).click(); + await expect(app.getByRole("heading", { name: "Search & Install", exact: true })).toBeVisible(); await app.getByLabel("Search GitHub").fill("mako"); await app.getByRole("button", { name: "Search", exact: true }).click(); // Already in the curated list, so Add shows as done; Install still works. @@ -815,6 +816,32 @@ test("Search installs a plugin straight from its GitHub release", async ({ page ]); }); +test("Y and X on a search result's Install show the release notes and download the ZIP", async ({ page }) => { + await mockMakoReleases(page, [{ ...makoRelease("plugin-v1.2.3", "e".repeat(64)), body: "Faster frame pacing." }]); + await page.route("https://api.github.com/search/repositories**", (route) => + route.fulfill({ json: { items: [{ full_name: "eugeniosegala/MAKO", description: "Frame generation", stargazers_count: 1 }] } }), + ); + await recordInstalls(page); + await page.goto("/?preview=/deckyhub/search&bridge=http://127.0.0.1:8643"); + const app = mock(page); + await app.getByLabel("Search GitHub").fill("mako"); + await app.getByRole("button", { name: "Search", exact: true }).click(); + const install = app.getByRole("button", { name: "Install", exact: true }); + await install.focus(); + await expect(app.locator(".steam-legend")).toContainText("Ⓨ Release Info"); + await expect(app.locator(".steam-legend")).toContainText("Ⓧ Download ZIP"); + + await install.press("y"); + await expect(app.locator(".steam-modal", { hasText: "MAKO Decky v1.2.3" }).getByText("Faster frame pacing.")).toBeVisible(); + await page.reload(); + await app.getByLabel("Search GitHub").fill("mako"); + await app.getByRole("button", { name: "Search", exact: true }).click(); + await install.press("x"); + await expect(app.locator(".steam-modal", { hasText: "mako-decky.zip" })).toBeVisible(); + // Neither key installs anything. + expect(await pluginFrame(page).evaluate(() => (window as unknown as { __installCalls: unknown[][] }).__installCalls)).toEqual([]); +}); + test("The download window spins until Decky's own install dialog is up", async ({ page }) => { await mockMakoReleases(page, [makoRelease("plugin-v1.2.3", "a".repeat(64))]); await recordInstalls(page); diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 25818c9..d4b2d42 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -12,6 +12,7 @@ export const de: Record = { "view.discoverDescription": "Unterstützte GitHub-Projekte und ihre ZIP-Releases durchsuchen.", "view.discoverEmpty": "Keine Repositories verfügbar.", "view.searchDescription": "GitHub nach Decky-Plugins durchsuchen. Direkt installieren oder hinzufügen, um Updates zu verfolgen.", + "view.searchTitle": "Suchen & Installieren", "search.warning": "Suchergebnisse kommen direkt von GitHub. Niemand hat sie geprüft: weder DeckyHub noch der Decky-Store. Ein Plugin kann alles auf deinem Deck tun, installiere also nur Plugins von Autoren, denen du vertraust.", "filter.search": "Suche", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 94d61f8..9ced21a 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -11,6 +11,7 @@ export const en = { "view.discoverDescription": "Browse supported GitHub projects and their ZIP releases.", "view.discoverEmpty": "No repositories are available.", "view.searchDescription": "Search GitHub for Decky plugins. Install one straight away, or add it to track its updates.", + "view.searchTitle": "Search & Install", "search.warning": "Search results come straight from GitHub. Nobody has reviewed them: not DeckyHub, not the Decky store. A plugin can do anything on your Deck, so only install plugins from authors you trust.", "filter.search": "Search", diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 20425ab..fd75fd9 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -12,6 +12,7 @@ export const es: Record = { "view.discoverDescription": "Explora proyectos de GitHub compatibles y sus versiones ZIP.", "view.discoverEmpty": "No hay repositorios disponibles.", "view.searchDescription": "Busca plugins de Decky en GitHub. Instálalo al momento o añádelo para seguir sus actualizaciones.", + "view.searchTitle": "Buscar e instalar", "search.warning": "Los resultados vienen directamente de GitHub. Nadie los ha revisado: ni DeckyHub ni la tienda de Decky. Un plugin puede hacer cualquier cosa en tu Deck, así que instala solo plugins de autores en los que confíes.", "filter.search": "Buscar", diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts index 50daed9..d5636f3 100644 --- a/src/i18n/fr.ts +++ b/src/i18n/fr.ts @@ -12,6 +12,7 @@ export const fr: Record = { "view.discoverDescription": "Parcourez les projets GitHub pris en charge et leurs versions ZIP.", "view.discoverEmpty": "Aucun dépôt disponible.", "view.searchDescription": "Cherchez des plugins Decky sur GitHub. Installez-les tout de suite ou ajoutez-les pour suivre leurs mises à jour.", + "view.searchTitle": "Rechercher et installer", "search.warning": "Les résultats viennent directement de GitHub. Personne ne les a vérifiés : ni DeckyHub, ni la boutique Decky. Un plugin peut tout faire sur votre Deck, n'installez donc que des plugins d'auteurs de confiance.", "filter.search": "Rechercher", diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 7ec8969..751f12c 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -12,6 +12,7 @@ export const ja: Record = { "view.discoverDescription": "対応しているGitHubプロジェクトとそのZIPリリースを見つけます。", "view.discoverEmpty": "利用できるリポジトリがありません。", "view.searchDescription": "GitHub で Decky プラグインを検索します。すぐにインストールするか、追加して更新を追跡できます。", + "view.searchTitle": "検索とインストール", "search.warning": "検索結果は GitHub から直接取得されます。DeckyHub も Decky ストアも審査していません。プラグインは Deck 上で何でもできるため、信頼できる作者のプラグインだけをインストールしてください。", "filter.search": "検索", diff --git a/src/i18n/ru.ts b/src/i18n/ru.ts index 06514ac..6086ac1 100644 --- a/src/i18n/ru.ts +++ b/src/i18n/ru.ts @@ -12,6 +12,7 @@ export const ru: Record = { "view.discoverDescription": "Просмотр поддерживаемых проектов GitHub и их ZIP-релизов.", "view.discoverEmpty": "Нет доступных репозиториев.", "view.searchDescription": "Ищите плагины Decky на GitHub. Установите сразу или добавьте, чтобы следить за обновлениями.", + "view.searchTitle": "Поиск и установка", "search.warning": "Результаты поиска приходят прямо с GitHub. Их никто не проверял: ни DeckyHub, ни магазин Decky. Плагин может сделать на вашем Deck что угодно, поэтому устанавливайте только плагины авторов, которым доверяете.", "filter.search": "Поиск", diff --git a/src/i18n/uk.ts b/src/i18n/uk.ts index 8e10482..439c26d 100644 --- a/src/i18n/uk.ts +++ b/src/i18n/uk.ts @@ -12,6 +12,7 @@ export const uk: Record = { "view.discoverDescription": "Перегляд підтримуваних проєктів GitHub та їхніх ZIP-релізів.", "view.discoverEmpty": "Немає доступних репозиторіїв.", "view.searchDescription": "Шукайте плагіни Decky на GitHub. Встановіть одразу або додайте, щоб стежити за оновленнями.", + "view.searchTitle": "Пошук і встановлення", "search.warning": "Результати пошуку надходять просто з GitHub. Їх ніхто не перевіряв: ні DeckyHub, ні магазин Decky. Плагін може зробити на вашому Deck будь-що, тож встановлюйте лише плагіни авторів, яким довіряєте.", "filter.search": "Пошук", diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index ec6f6ca..318c4c1 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -12,6 +12,7 @@ export const zh: Record = { "view.discoverDescription": "浏览受支持的 GitHub 项目及其 ZIP 发行版。", "view.discoverEmpty": "没有可用的仓库。", "view.searchDescription": "在 GitHub 上搜索 Decky 插件。可直接安装,或添加以跟踪更新。", + "view.searchTitle": "搜索与安装", "search.warning": "搜索结果直接来自 GitHub,未经任何审核:DeckyHub 和 Decky 商店都没有审核过。插件可以在你的 Deck 上做任何事,请只安装你信任的作者的插件。", "filter.search": "搜索", diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx index 0b270fc..37e03ba 100644 --- a/src/pages/SearchPage.tsx +++ b/src/pages/SearchPage.tsx @@ -5,11 +5,13 @@ import { Card, PageHeader } from "../components/Card"; import { Pager } from "../components/Pager"; import { RateLimitBanner } from "../components/RateLimitBanner"; import { startPluginInstall } from "../components/AppDetailsModal"; +import { showReleaseInfo } from "../components/AppCard"; +import { showDownloadModal } from "../components/DownloadProgress"; import { useEffect, useState } from "react"; import { FaCheck, FaDownload, FaPlus, FaSearch, FaTimes } from "react-icons/fa"; -import { addCustomRepo, getApps, REGISTRY_UPDATED } from "../api"; +import { addCustomRepo, downloadAsset, getApps, getSettings, REGISTRY_UPDATED } from "../api"; import { useT } from "../i18n"; -import type { SearchRepo } from "../types"; +import type { HideableButton, SearchRepo } from "../types"; import { displayVersion } from "../version"; import { compactButtonStyle, disclaimerStyle, githubFetch, githubResponseError, hydrate, installAction, installTypeLabel, log, surfaceStyle } from "../utils"; @@ -35,9 +37,12 @@ export function SearchPage() { // The repo whose install is being prepared, so a second tap can't start another. const [installing, setInstalling] = useState(null); - const refreshRepos = () => void getApps().then((result) => setExistingRepos(new Set(result.apps.map((app) => app.repo.toLowerCase())))); + const [hiddenButtons, setHiddenButtons] = useState([]); - useEffect(refreshRepos, []); + useEffect(() => { + void getApps().then((result) => setExistingRepos(new Set(result.apps.map((app) => app.repo.toLowerCase())))); + void getSettings().then((settings) => setHiddenButtons(settings.hiddenButtons ?? [])); + }, []); const search = async () => { if (!query.trim() || searching) return; @@ -80,16 +85,37 @@ export function SearchPage() { toaster.toast({ title: "DeckyHub", body: t(added ? "repos.addedToast" : "repos.alreadyAdded", { repo }) }); }; + // The repo's latest stable release, looked up the way a Discover card does. A repo + // that isn't listed gets the entry the backend makes for an added one (_custom_apps). + const latestRelease = async (repo: string) => { + const listed = (await getApps()).apps.find((app) => app.repo.toLowerCase() === repo.toLowerCase()); + const app = await hydrate(listed ?? { id: `custom-${repo.replace("/", "-")}`, name: repo, repo, category: "Custom", detect: { type: "decky-plugin" }, asset: { include: [], exclude: ["source"] }, assets: [] }, false); + if (!app.latestVersion || !app.assets[0]) throw new Error(app.error ?? t("release.noMatch")); + return app as typeof app & { latestVersion: string }; + }; + + // Y: that release's notes, without adding or installing anything. + const releaseInfo = async (repo: string) => { + const app = await latestRelease(repo); + showReleaseInfo(t, `${app.name} ${displayVersion(app.latestVersion)}`, app.publishedAt, app.releaseNotes); + }; + + // X: that release's ZIP to the download folder instead of installing it. + const downloadZip = async (repo: string) => { + const asset = (await latestRelease(repo)).assets[0]; + const result = await downloadAsset(asset, repo); + if (result.error) throw new Error(result.error); + if (result.jobId) showDownloadModal(t, result.jobId, { state: "queued", filename: asset.name, total: asset.size }, () => {}); + }; + // Adds the repo, then installs its latest stable release the way a Discover card does. const install = async (repo: string) => { setInstalling(repo); try { await track(repo); - const listed = (await getApps()).apps.find((app) => app.repo.toLowerCase() === repo.toLowerCase()); - if (!listed) throw new Error(t("release.noMatch")); - const app = await hydrate(listed, false); - const action = app.latestVersion ? installAction(app, { tag: app.latestVersion, assets: app.assets }) : null; - if (!action || !app.latestVersion) throw new Error(app.error ?? t("release.noMatch")); + const app = await latestRelease(repo); + const action = installAction(app, { tag: app.latestVersion, assets: app.assets }); + if (!action) throw new Error(t("release.noMatch")); const label = installTypeLabel(t, action.type, displayVersion(app.latestVersion).replace(/^v/, "")); // Decky's own dialog takes over, so leave the page like a card's Install does. await startPluginInstall(t, app.name, app.repo, app.assets[0], app.latestVersion, action, label, () => Navigation.NavigateBack()); @@ -101,7 +127,7 @@ export function SearchPage() { return ( <> - } title={t("nav.search")} description={t("view.searchDescription")} /> + } title={t("view.searchTitle")} description={t("view.searchDescription")} />
{t("search.warning")}
@@ -153,7 +179,15 @@ export function SearchPage() {
-
From 577f6a976aad8a52f723ccc3d535ffe2e8769477 Mon Sep 17 00:00:00 2001 From: Andrii Shevchuk-Yuhanets Date: Wed, 28 Oct 2026 14:02:43 -0400 Subject: [PATCH 2/2] chore: bump version to 2.6.0-rc.2 Co-Authored-By: Claude Opus 5.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 07c5d5e..a2c7cef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "deckyhub", - "version": "2.6.0-rc.1", + "version": "2.6.0-rc.2", "description": "Discover, track, and safely download GitHub releases for Steam Deck tools from Gaming Mode.", "author": "Andrii Shevchuk-Yuhanets", "private": true,